fix(provider): 修复 Windsurf 原生工具桥接

This commit is contained in:
Entropy.Xu
2026-05-20 23:42:12 +08:00
parent 82637ad882
commit 129c7c90c0
30 changed files with 9090 additions and 194 deletions

View File

@@ -9,6 +9,7 @@ use axum::{
response::{IntoResponse, Response},
Json,
};
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
use serde_json::{json, Value};
use std::collections::BTreeMap;
@@ -548,16 +549,69 @@ fn admin_monitoring_trace_response_data(
return None;
}
let body = admin_monitoring_trace_response_body(headers, body);
Some(json!({
"source": source,
"status_code": status_code,
"headers": headers.cloned().unwrap_or(Value::Null),
"body": body.cloned().unwrap_or(Value::Null),
"body": body.unwrap_or(Value::Null),
"body_ref": body_ref,
"body_state": body_state.map(|state| state.as_str()),
}))
}
fn admin_monitoring_trace_response_body(
headers: Option<&Value>,
body: Option<&Value>,
) -> Option<Value> {
let body = body?;
admin_monitoring_decode_connect_json_error_body(headers, body).or_else(|| Some(body.clone()))
}
fn admin_monitoring_decode_connect_json_error_body(
headers: Option<&Value>,
body: &Value,
) -> Option<Value> {
if !admin_monitoring_headers_indicate_connect_json(headers) {
return None;
}
let body_base64 = match body {
Value::String(value) => Some(value.as_str()),
Value::Object(object) => object
.get("encoding")
.and_then(Value::as_str)
.is_some_and(|value| value.eq_ignore_ascii_case("base64"))
.then(|| object.get("data").and_then(Value::as_str))
.flatten(),
_ => None,
}?
.trim();
if body_base64.is_empty() {
return None;
}
let body_bytes = BASE64_STANDARD.decode(body_base64).ok()?;
aether_ai_formats::api::extract_provider_private_stream_error_body(None, &body_bytes)
}
fn admin_monitoring_headers_indicate_connect_json(headers: Option<&Value>) -> bool {
headers
.and_then(Value::as_object)
.and_then(|object| {
object.iter().find_map(|(key, value)| {
key.eq_ignore_ascii_case("content-type")
.then(|| value.as_str())
.flatten()
})
})
.map(str::trim)
.is_some_and(|value| {
let value = value.to_ascii_lowercase();
value.contains("application/connect+json") || value.contains("+connect+json")
})
}
fn merge_admin_monitoring_trace_response(
extra_object: &mut serde_json::Map<String, Value>,
key: &str,

View File

@@ -211,10 +211,10 @@ pub use crate::provider_compat::kiro_stream::{
KiroToClaudeCliStreamState, KIRO_MAX_THINKING_BUFFER,
};
pub use crate::provider_compat::private_envelope::{
maybe_build_provider_private_stream_normalizer, normalize_provider_private_report_context,
normalize_provider_private_response_value, provider_private_response_allows_sync_finalize,
stream_body_contains_error_event, transform_provider_private_stream_line,
ProviderPrivateStreamNormalizer,
extract_provider_private_stream_error_body, maybe_build_provider_private_stream_normalizer,
normalize_provider_private_report_context, normalize_provider_private_response_value,
provider_private_response_allows_sync_finalize, stream_body_contains_error_event,
transform_provider_private_stream_line, ProviderPrivateStreamNormalizer,
};
pub use crate::provider_compat::surfaces::{
provider_adaptation_allows_sync_finalize_envelope, provider_adaptation_anchor_api_format,

View File

@@ -1299,6 +1299,7 @@ struct OpenAIResponsesClientToolState {
name: String,
arguments: String,
output_index: Option<usize>,
web_search: bool,
}
#[derive(Clone, Default)]
@@ -1310,6 +1311,23 @@ struct OpenAIResponsesClientToolResultState {
item_started: bool,
}
fn is_responses_web_search_tool(name: &str) -> bool {
matches!(name, "web_search" | "web_search_preview")
}
fn web_search_query_from_arguments(arguments: &str) -> String {
serde_json::from_str::<Value>(arguments)
.ok()
.and_then(|value| {
value
.get("query")
.and_then(Value::as_str)
.map(ToOwned::to_owned)
.or_else(|| value.as_str().map(ToOwned::to_owned))
})
.unwrap_or_default()
}
#[derive(Default)]
pub struct OpenAIResponsesClientEmitter {
response_id: Option<String>,
@@ -1985,6 +2003,26 @@ impl OpenAIResponsesClientEmitter {
} else {
state.name.clone()
};
if state.web_search {
out.extend(self.encode_response_event(
"response.output_item.done",
json!({
"type": "response.output_item.done",
"response_id": self.response_id(),
"output_index": output_index,
"item": {
"type": "web_search_call",
"id": item_id,
"status": "completed",
"action": {
"type": "search",
"query": web_search_query_from_arguments(&state.arguments),
},
}
}),
)?);
continue;
}
out.extend(self.encode_response_event(
"response.function_call_arguments.done",
json!({
@@ -2143,20 +2181,32 @@ impl OpenAIResponsesClientEmitter {
}
for (index, state) in &self.tool_calls {
if let Some(output_index) = state.output_index {
let item_id = if state.call_id.is_empty() {
build_generated_tool_call_id(*index)
} else {
state.call_id.clone()
};
if state.web_search {
ordered_output.push((
output_index,
json!({
"type": "web_search_call",
"id": item_id,
"status": "completed",
"action": {
"type": "search",
"query": web_search_query_from_arguments(&state.arguments),
},
}),
));
continue;
}
ordered_output.push((
output_index,
json!({
"type": "function_call",
"id": if state.call_id.is_empty() {
build_generated_tool_call_id(*index)
} else {
state.call_id.clone()
},
"call_id": if state.call_id.is_empty() {
build_generated_tool_call_id(*index)
} else {
state.call_id.clone()
},
"id": item_id.clone(),
"call_id": item_id,
"name": if state.name.is_empty() {
"unknown".to_string()
} else {
@@ -2322,22 +2372,36 @@ impl OpenAIResponsesClientEmitter {
let state = self.tool_calls.entry(index).or_default();
state.call_id = call_id.clone();
state.name = name.clone();
state.web_search = is_responses_web_search_tool(&name);
let emitted_call_id = state.call_id.clone();
let emitted_name = state.name.clone();
let item = if state.web_search {
json!({
"type": "web_search_call",
"id": emitted_call_id,
"status": "in_progress",
"action": {
"type": "search",
"query": "",
},
})
} else {
json!({
"type": "function_call",
"id": call_id,
"call_id": emitted_call_id,
"name": emitted_name,
"arguments": "",
"status": "in_progress",
})
};
out.extend(self.encode_response_event(
"response.output_item.added",
json!({
"type": "response.output_item.added",
"response_id": response_id,
"output_index": output_index,
"item": {
"type": "function_call",
"id": call_id,
"call_id": emitted_call_id,
"name": emitted_name,
"arguments": "",
"status": "in_progress",
}
"item": item
}),
)?);
Ok(out)
@@ -2348,6 +2412,9 @@ impl OpenAIResponsesClientEmitter {
let response_id = self.response_id().to_string();
let state = self.tool_calls.entry(index).or_default();
state.arguments.push_str(&arguments);
if state.web_search {
return Ok(out);
}
let item_id = if state.call_id.is_empty() {
build_generated_tool_call_id(index)
} else {
@@ -3167,6 +3234,56 @@ mod tests {
assert!(sse.contains("\"output\":\"{\\\"ok\\\":true}\""));
}
#[test]
fn openai_responses_client_emitter_emits_web_search_call_item() {
let mut emitter = OpenAIResponsesClientEmitter::default();
let mut bytes = emitter
.emit(CanonicalStreamFrame {
id: "resp_123".to_string(),
model: "gpt-5-5-low".to_string(),
event: CanonicalStreamEvent::ToolCallStart {
index: 0,
call_id: "call_ws_1".to_string(),
name: "web_search".to_string(),
},
})
.expect("tool start should encode");
bytes.extend(
emitter
.emit(CanonicalStreamFrame {
id: "resp_123".to_string(),
model: "gpt-5-5-low".to_string(),
event: CanonicalStreamEvent::ToolCallArgumentsDelta {
index: 0,
arguments: r#"{"query":"today tech"}"#.to_string(),
},
})
.expect("arguments should encode"),
);
bytes.extend(
emitter
.emit(CanonicalStreamFrame {
id: "resp_123".to_string(),
model: "gpt-5-5-low".to_string(),
event: CanonicalStreamEvent::Finish {
finish_reason: Some("tool_calls".to_string()),
usage: None,
},
})
.expect("finish should encode"),
);
let sse = String::from_utf8(bytes).expect("sse should be utf8");
assert!(sse.contains("event: response.output_item.added\n"));
assert!(sse.contains(r#""type":"web_search_call""#));
assert!(sse.contains(r#""status":"in_progress""#));
assert!(sse.contains(r#""query":"""#));
assert!(sse.contains(r#""type":"search""#));
assert!(sse.contains("event: response.output_item.done\n"));
assert!(sse.contains(r#""query":"today tech""#));
assert!(!sse.contains("response.function_call_arguments.delta"));
}
#[test]
fn openai_responses_provider_state_accepts_legacy_outtext_delta_alias() {
let mut state = OpenAIResponsesProviderState::default();

View File

@@ -166,13 +166,25 @@ pub fn to_raw(canonical: &CanonicalResponse, report_context: &Value, _compact: b
&response_id,
&mut message_index,
);
output.push(json!({
"type": "function_call",
"id": id,
"call_id": id,
"name": name,
"arguments": canonicalize_tool_arguments(input),
}));
if is_responses_web_search_tool(name) {
output.push(json!({
"type": "web_search_call",
"id": id,
"status": "completed",
"action": {
"type": "search",
"query": web_search_query_from_value(input),
},
}));
} else {
output.push(json!({
"type": "function_call",
"id": id,
"call_id": id,
"name": name,
"arguments": canonicalize_tool_arguments(input),
}));
}
}
CanonicalContentBlock::ToolResult {
tool_use_id,
@@ -325,3 +337,73 @@ fn openai_responses_output_format_from_mime_type(mime_type: &str) -> String {
}
.to_string()
}
fn is_responses_web_search_tool(name: &str) -> bool {
matches!(name, "web_search" | "web_search_preview")
}
fn web_search_query_from_value(input: &Value) -> String {
input
.get("query")
.and_then(Value::as_str)
.or_else(|| input.as_str())
.unwrap_or_default()
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn responses_response_builder_emits_web_search_call_for_web_search_tool_use() {
let response = CanonicalResponse {
id: "resp_test".to_string(),
model: "gpt-5-5-low".to_string(),
content: vec![CanonicalContentBlock::ToolUse {
id: "call_ws_1".to_string(),
name: "web_search".to_string(),
input: json!({"query": "today tech"}),
extensions: BTreeMap::new(),
}],
outputs: Vec::new(),
stop_reason: Some(CanonicalStopReason::ToolUse),
usage: None,
extensions: BTreeMap::new(),
};
let body = to_raw(&response, &json!({}), false);
assert_eq!(body["output"][0]["type"], "web_search_call");
assert_eq!(body["output"][0]["id"], "call_ws_1");
assert_eq!(body["output"][0]["status"], "completed");
assert_eq!(body["output"][0]["action"]["type"], "search");
assert_eq!(body["output"][0]["action"]["query"], "today tech");
}
#[test]
fn responses_response_parser_reads_web_search_call_as_tool_use() {
let body = json!({
"id": "resp_test",
"model": "gpt-5-5-low",
"status": "incomplete",
"output": [{
"type": "web_search_call",
"id": "call_ws_1",
"status": "completed",
"action": {"type": "search", "query": "today tech"}
}]
});
let canonical = from_raw(&body).expect("response should parse");
assert!(
matches!(canonical.content.first(), Some(CanonicalContentBlock::ToolUse {
id,
name,
input,
..
}) if id == "call_ws_1" && name == "web_search" && input["query"] == "today tech")
);
}
}

View File

@@ -1556,6 +1556,39 @@ pub(crate) fn openai_responses_input_to_canonical_messages(
extensions: BTreeMap::new(),
});
}
"web_search_call" => {
let id = item_object
.get("id")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
.unwrap_or_else(|| {
let generated =
format!("call_auto_{next_generated_tool_call_index}");
next_generated_tool_call_index += 1;
generated
});
let query = item_object
.get("action")
.and_then(Value::as_object)
.and_then(|action| action.get("query"))
.and_then(Value::as_str)
.unwrap_or_default();
messages.push(CanonicalMessage {
role: CanonicalRole::Assistant,
content: vec![CanonicalContentBlock::ToolUse {
id,
name: "web_search".to_string(),
input: json!({ "query": query }),
extensions: openai_responses_extensions(
item_object,
&["type", "id", "status", "action"],
),
}],
extensions: BTreeMap::new(),
});
}
"function_call_output" => {
let id = item_object
.get("call_id")
@@ -1729,6 +1762,30 @@ pub(crate) fn openai_responses_output_to_canonical_blocks(
),
});
}
"web_search_call" => {
let id = item_object
.get("id")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
.unwrap_or_else(|| format!("call_auto_{index}"));
let query = item_object
.get("action")
.and_then(Value::as_object)
.and_then(|action| action.get("query"))
.and_then(Value::as_str)
.unwrap_or_default();
blocks.push(CanonicalContentBlock::ToolUse {
id,
name: "web_search".to_string(),
input: json!({ "query": query }),
extensions: openai_responses_extensions(
item_object,
&["type", "id", "status", "action"],
),
});
}
"function_call_output" => {
let id = item_object
.get("call_id")

View File

@@ -325,6 +325,19 @@ pub fn maybe_build_provider_private_stream_normalizer<'a>(
})
}
pub fn extract_provider_private_stream_error_body(
report_context: Option<&Value>,
body: &[u8],
) -> Option<Value> {
if report_context.is_none_or(report_context_is_windsurf_envelope) {
if let Some(error_body) = extract_windsurf_connect_json_error_body(body) {
return Some(error_body);
}
}
extract_stream_error_event_body(body)
}
impl ProviderPrivateStreamNormalizer<'_> {
pub fn push_chunk(&mut self, chunk: &[u8]) -> Result<Vec<u8>, AiSurfaceFinalizeError> {
match &mut self.mode {
@@ -536,8 +549,62 @@ fn build_openai_chat_response_from_text(source: &Value, text: String) -> Value {
}
pub fn stream_body_contains_error_event(body: &[u8]) -> bool {
if extract_windsurf_connect_json_error_body(body).is_some() {
return true;
}
extract_stream_error_event_body(body).is_some()
}
fn extract_windsurf_connect_json_error_body(body: &[u8]) -> Option<Value> {
if !buffer_looks_like_connect_frame(body) {
return None;
}
let mut offset = 0usize;
while body.len().saturating_sub(offset) >= CONNECT_FRAME_HEADER_BYTES {
let flags = body[offset];
if flags & !0x03 != 0 {
return None;
}
let len = u32::from_be_bytes([
body[offset + 1],
body[offset + 2],
body[offset + 3],
body[offset + 4],
]) as usize;
if len > MAX_CONNECT_JSON_FRAME_BYTES {
return None;
}
let frame_end = offset + CONNECT_FRAME_HEADER_BYTES + len;
if body.len() < frame_end {
return None;
}
if flags & 0x01 != 0 {
return None;
}
let payload = &body[offset + CONNECT_FRAME_HEADER_BYTES..frame_end];
offset = frame_end;
if payload.is_empty() {
continue;
}
let parsed: Value = serde_json::from_slice(payload).ok()?;
if flags & 0x02 != 0 {
if let Some(error) = parsed.get("error").filter(|value| !value.is_null()) {
return Some(normalize_provider_private_error_body(error.clone()));
}
continue;
}
if looks_like_windsurf_error(&parsed) {
return Some(normalize_provider_private_error_body(parsed));
}
}
None
}
fn extract_stream_error_event_body(body: &[u8]) -> Option<Value> {
let Ok(text) = std::str::from_utf8(body) else {
return false;
return None;
};
let mut current_event_type: Option<String> = None;
for raw_line in text.lines() {
@@ -572,11 +639,35 @@ pub fn stream_body_contains_error_event(body: &[u8]) -> bool {
.and_then(Value::as_str)
.is_some_and(|value| value.eq_ignore_ascii_case("error"))
{
return true;
return Some(normalize_provider_private_error_body(event));
}
current_event_type = None;
}
false
None
}
fn normalize_provider_private_error_body(error: Value) -> Value {
let mut error = if error.get("error").is_some_and(|value| !value.is_null()) {
error
} else {
serde_json::json!({ "error": error })
};
if let Some(error_object) = error.get_mut("error").and_then(Value::as_object_mut) {
if !error_object.contains_key("type") {
if let Some(kind) = error_object
.get("code")
.or_else(|| error_object.get("status"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
{
error_object.insert("type".to_string(), Value::String(kind.to_string()));
}
}
}
error
}
fn clear_private_envelope_context(report_context: &Value) -> Value {
@@ -717,9 +808,9 @@ mod tests {
use serde_json::json;
use super::{
maybe_build_provider_private_stream_normalizer, normalize_provider_private_report_context,
normalize_provider_private_response_value, stream_body_contains_error_event,
transform_provider_private_stream_line,
extract_provider_private_stream_error_body, maybe_build_provider_private_stream_normalizer,
normalize_provider_private_report_context, normalize_provider_private_response_value,
stream_body_contains_error_event, transform_provider_private_stream_line,
};
#[test]
@@ -853,6 +944,30 @@ mod tests {
assert!(text.contains(r#""content":"frame chunk""#));
}
#[test]
fn detects_windsurf_connect_json_trailer_error_frame() {
let framed = connect_json_frame(
2,
br#"{"error":{"code":"resource_exhausted","message":"quota exhausted"}}"#,
);
assert!(stream_body_contains_error_event(&framed));
}
#[test]
fn extracts_connect_json_trailer_error_without_report_context() {
let framed = connect_json_frame(
2,
br#"{"error":{"code":"resource_exhausted","message":"quota exhausted"}}"#,
);
let body = extract_provider_private_stream_error_body(None, &framed)
.expect("Connect trailer error should decode without report context");
assert_eq!(body["error"]["code"], json!("resource_exhausted"));
assert_eq!(body["error"]["message"], json!("quota exhausted"));
}
fn connect_json_frame(flags: u8, payload: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(5 + payload.len());
out.push(flags);

View File

@@ -496,6 +496,31 @@ pub fn json_string_list(value: Option<&Value>) -> Vec<String> {
.unwrap_or_default()
}
fn api_format_priority(api_format: &str) -> Option<(usize, usize)> {
MODEL_FETCH_FORMAT_PRIORITY
.iter()
.enumerate()
.find_map(|(group_index, group)| {
group
.iter()
.position(|candidate| candidate.eq_ignore_ascii_case(api_format))
.map(|format_index| (group_index, format_index))
})
}
fn sorted_api_formats(formats: BTreeSet<String>) -> Vec<String> {
let mut formats = formats.into_iter().collect::<Vec<_>>();
formats.sort_by(
|left, right| match (api_format_priority(left), api_format_priority(right)) {
(Some(left_priority), Some(right_priority)) => left_priority.cmp(&right_priority),
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => left.cmp(right),
},
);
formats
}
pub fn aggregate_models_for_cache(models: &[Value]) -> Vec<Value> {
let mut aggregated = BTreeMap::<String, serde_json::Map<String, Value>>::new();
@@ -557,7 +582,7 @@ pub fn aggregate_models_for_cache(models: &[Value]) -> Vec<Value> {
if let Some(api_format) = legacy_api_format {
merged_formats.insert(api_format);
}
let merged_formats = merged_formats
let merged_formats = sorted_api_formats(merged_formats)
.into_iter()
.map(Value::String)
.collect::<Vec<_>>();
@@ -877,6 +902,20 @@ mod tests {
);
}
#[test]
fn aggregate_models_for_cache_orders_api_formats_by_canonical_priority() {
let aggregated = aggregate_models_for_cache(&[
json!({"id":"claude-sonnet-4-6","api_formats":["claude:messages"]}),
json!({"id":"claude-sonnet-4-6","api_formats":["openai:responses"]}),
json!({"id":"claude-sonnet-4-6","api_formats":["openai:chat"]}),
]);
assert_eq!(aggregated.len(), 1);
assert_eq!(
aggregated[0]["api_formats"],
json!(["openai:chat", "openai:responses", "claude:messages"])
);
}
#[test]
fn aggregate_models_for_cache_preserves_legacy_api_format_field() {
let aggregated = aggregate_models_for_cache(&[json!({

View File

@@ -22,7 +22,6 @@ use crate::vertex::{
build_vertex_service_account_gemini_embedding_url, resolve_local_vertex_api_key_query_auth,
resolve_local_vertex_service_account_auth_config,
};
#[derive(Debug, Clone, Copy)]
pub struct TransportRequestUrlParams<'a> {
pub provider_api_format: &'a str,

View File

@@ -15,6 +15,10 @@ use crate::{
transport_proxy_is_locally_supported,
};
pub mod cascade;
pub mod models;
pub mod proto;
pub const PROVIDER_TYPE: &str = "windsurf";
pub const WINDSURF_ENVELOPE_NAME: &str = "windsurf:GetChatMessage";
pub const GET_CHAT_MESSAGE_PATH: &str = "/exa.api_server_pb.ApiServerService/GetChatMessage";
@@ -122,7 +126,8 @@ pub fn build_windsurf_cascade_request_body(
}
let conversation_id =
extract_conversation_id(body_json).unwrap_or_else(|| Uuid::new_v4().to_string());
let message_text = last_user_message_text(&messages).unwrap_or_else(|| "Continue.".to_string());
let message_text =
latest_message_snapshot_text(&messages).unwrap_or_else(|| "Continue.".to_string());
let mut provider_request_body = json!({
"metadata": windsurf_metadata_from_auth(auth_value),
"model": mapped_model,
@@ -151,6 +156,19 @@ pub fn build_windsurf_cascade_request_body(
.as_object_mut()?
.insert("topP".to_string(), top_p.clone());
}
for field in [
"tools",
"tool_choice",
"toolChoice",
"parallel_tool_calls",
"response_format",
] {
if let Some(value) = body_json.get(field) {
provider_request_body
.as_object_mut()?
.insert(field.to_string(), value.clone());
}
}
if !apply_local_body_rules_with_request_headers(
&mut provider_request_body,
@@ -268,20 +286,36 @@ fn string_value(value: Option<&Value>) -> Option<String> {
.map(ToOwned::to_owned)
}
fn last_user_message_text(messages: &[Value]) -> Option<String> {
fn latest_message_snapshot_text(messages: &[Value]) -> Option<String> {
messages
.iter()
.rev()
.filter_map(Value::as_object)
.find(|message| {
message
.get("role")
.and_then(Value::as_str)
.is_some_and(|role| role == "user")
.find_map(|message| {
let role = message.get("role").and_then(Value::as_str)?;
match role {
"user" => openai_content_to_text(message.get("content"))
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty()),
"tool" => {
let content = openai_content_to_text(message.get("content"))
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())?;
let tool_call_id = message
.get("tool_call_id")
.and_then(Value::as_str)
.unwrap_or("unknown");
Some(format!(
"<tool_result tool_call_id=\"{}\">\n{content}\n</tool_result>",
escape_xml_attr(tool_call_id)
))
}
"assistant" => openai_content_to_text(message.get("content"))
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty()),
_ => None,
}
})
.and_then(|message| openai_content_to_text(message.get("content")))
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
}
fn openai_content_to_text(value: Option<&Value>) -> Option<String> {
@@ -304,6 +338,14 @@ fn openai_content_to_text(value: Option<&Value>) -> Option<String> {
}
}
fn escape_xml_attr(value: &str) -> String {
value
.replace('&', "&amp;")
.replace('"', "&quot;")
.replace('<', "&lt;")
.replace('>', "&gt;")
}
#[cfg(test)]
mod tests {
use http::HeaderMap;
@@ -416,6 +458,79 @@ mod tests {
assert_eq!(body["maxTokens"], json!(128));
}
#[test]
fn preserves_openai_tool_fields_for_native_windsurf_runtime() {
let body = build_windsurf_cascade_request_body(
&json!({
"model": "gpt-5-5-low",
"messages": [
{"role": "user", "content": "read Cargo.toml"}
],
"tools": [{
"type": "function",
"function": {
"name": "Read",
"parameters": {
"type": "object",
"properties": {"file_path": {"type": "string"}}
}
}
}],
"tool_choice": "required",
"parallel_tool_calls": false,
"response_format": {"type": "json_object"}
}),
"gpt-5-5-low",
"Bearer devin-session-token$abc",
None,
None,
false,
)
.expect("body should build");
assert_eq!(body["tools"][0]["function"]["name"], json!("Read"));
assert_eq!(body["tool_choice"], json!("required"));
assert_eq!(body["parallel_tool_calls"], json!(false));
assert_eq!(body["response_format"]["type"], json!("json_object"));
}
#[test]
fn builds_cascade_request_body_message_snapshot_from_latest_tool_result() {
let body = build_windsurf_cascade_request_body(
&json!({
"model": "gpt-5-5-low",
"messages": [
{"role": "user", "content": "read Cargo.toml"},
{
"role": "assistant",
"content": null,
"tool_calls": [{
"id": "call_1",
"type": "function",
"function": {"name": "Read", "arguments": "{\"file_path\":\"Cargo.toml\"}"}
}]
},
{"role": "tool", "tool_call_id": "call_1", "content": "workspace Cargo.toml content"}
]
}),
"gpt-5-5-low",
"Bearer devin-session-token$abc",
None,
None,
false,
)
.expect("body should build");
assert!(body["message"]
.as_str()
.expect("message should be a string")
.contains(r#"<tool_result tool_call_id="call_1">"#));
assert!(body["message"]
.as_str()
.expect("message should be a string")
.contains("workspace Cargo.toml content"));
}
#[test]
fn builds_cascade_headers_with_connect_protocol_and_auth() {
let headers = build_windsurf_cascade_headers(

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,390 @@
#[derive(Debug, Clone, PartialEq)]
pub struct WindsurfModel {
pub canonical_name: &'static str,
pub enum_value: u32,
pub model_uid: Option<&'static str>,
pub credit_multiplier: f32,
pub provider: &'static str,
pub deprecated: bool,
}
#[rustfmt::skip]
const MODELS: &[WindsurfModel] = &[
WindsurfModel { canonical_name: "claude-3.5-sonnet", enum_value: 166, model_uid: None, credit_multiplier: 2.0, provider: "anthropic", deprecated: true },
WindsurfModel { canonical_name: "claude-3.7-sonnet", enum_value: 226, model_uid: None, credit_multiplier: 2.0, provider: "anthropic", deprecated: true },
WindsurfModel { canonical_name: "claude-3.7-sonnet-thinking", enum_value: 227, model_uid: None, credit_multiplier: 3.0, provider: "anthropic", deprecated: true },
WindsurfModel { canonical_name: "claude-4-sonnet", enum_value: 281, model_uid: Some("MODEL_CLAUDE_4_SONNET"), credit_multiplier: 2.0, provider: "anthropic", deprecated: false },
WindsurfModel { canonical_name: "claude-4-sonnet-thinking", enum_value: 282, model_uid: Some("MODEL_CLAUDE_4_SONNET_THINKING"), credit_multiplier: 3.0, provider: "anthropic", deprecated: false },
WindsurfModel { canonical_name: "claude-4-opus", enum_value: 290, model_uid: Some("MODEL_CLAUDE_4_OPUS"), credit_multiplier: 4.0, provider: "anthropic", deprecated: false },
WindsurfModel { canonical_name: "claude-4-opus-thinking", enum_value: 291, model_uid: Some("MODEL_CLAUDE_4_OPUS_THINKING"), credit_multiplier: 5.0, provider: "anthropic", deprecated: false },
WindsurfModel { canonical_name: "claude-4.1-opus", enum_value: 328, model_uid: Some("MODEL_CLAUDE_4_1_OPUS"), credit_multiplier: 4.0, provider: "anthropic", deprecated: false },
WindsurfModel { canonical_name: "claude-4.1-opus-thinking", enum_value: 329, model_uid: Some("MODEL_CLAUDE_4_1_OPUS_THINKING"), credit_multiplier: 5.0, provider: "anthropic", deprecated: false },
WindsurfModel { canonical_name: "claude-4.5-haiku", enum_value: 0, model_uid: Some("MODEL_PRIVATE_11"), credit_multiplier: 1.0, provider: "anthropic", deprecated: false },
WindsurfModel { canonical_name: "claude-4.5-sonnet", enum_value: 353, model_uid: Some("MODEL_PRIVATE_2"), credit_multiplier: 2.0, provider: "anthropic", deprecated: false },
WindsurfModel { canonical_name: "claude-4.5-sonnet-thinking", enum_value: 354, model_uid: Some("MODEL_PRIVATE_3"), credit_multiplier: 3.0, provider: "anthropic", deprecated: false },
WindsurfModel { canonical_name: "claude-4.5-opus", enum_value: 391, model_uid: Some("MODEL_CLAUDE_4_5_OPUS"), credit_multiplier: 4.0, provider: "anthropic", deprecated: false },
WindsurfModel { canonical_name: "claude-4.5-opus-thinking", enum_value: 392, model_uid: Some("MODEL_CLAUDE_4_5_OPUS_THINKING"), credit_multiplier: 5.0, provider: "anthropic", deprecated: false },
WindsurfModel { canonical_name: "claude-sonnet-4.6", enum_value: 0, model_uid: Some("claude-sonnet-4-6"), credit_multiplier: 4.0, provider: "anthropic", deprecated: false },
WindsurfModel { canonical_name: "claude-sonnet-4.6-thinking", enum_value: 0, model_uid: Some("claude-sonnet-4-6-thinking"), credit_multiplier: 6.0, provider: "anthropic", deprecated: false },
WindsurfModel { canonical_name: "claude-sonnet-4.6-1m", enum_value: 0, model_uid: Some("claude-sonnet-4-6-1m"), credit_multiplier: 12.0, provider: "anthropic", deprecated: false },
WindsurfModel { canonical_name: "claude-sonnet-4.6-thinking-1m", enum_value: 0, model_uid: Some("claude-sonnet-4-6-thinking-1m"), credit_multiplier: 16.0, provider: "anthropic", deprecated: false },
WindsurfModel { canonical_name: "claude-opus-4.6", enum_value: 0, model_uid: Some("claude-opus-4-6"), credit_multiplier: 6.0, provider: "anthropic", deprecated: false },
WindsurfModel { canonical_name: "claude-opus-4.6-thinking", enum_value: 0, model_uid: Some("claude-opus-4-6-thinking"), credit_multiplier: 8.0, provider: "anthropic", deprecated: false },
WindsurfModel { canonical_name: "claude-opus-4-7-medium", enum_value: 0, model_uid: Some("claude-opus-4-7-medium"), credit_multiplier: 8.0, provider: "anthropic", deprecated: false },
WindsurfModel { canonical_name: "claude-opus-4-7-low", enum_value: 0, model_uid: Some("claude-opus-4-7-low"), credit_multiplier: 6.0, provider: "anthropic", deprecated: false },
WindsurfModel { canonical_name: "claude-opus-4-7-high", enum_value: 0, model_uid: Some("claude-opus-4-7-high"), credit_multiplier: 10.0, provider: "anthropic", deprecated: false },
WindsurfModel { canonical_name: "claude-opus-4-7-xhigh", enum_value: 0, model_uid: Some("claude-opus-4-7-xhigh"), credit_multiplier: 12.0, provider: "anthropic", deprecated: false },
WindsurfModel { canonical_name: "claude-opus-4-7-medium-thinking", enum_value: 0, model_uid: Some("claude-opus-4-7-medium-thinking"), credit_multiplier: 10.0, provider: "anthropic", deprecated: false },
WindsurfModel { canonical_name: "claude-opus-4-7-high-thinking", enum_value: 0, model_uid: Some("claude-opus-4-7-high-thinking"), credit_multiplier: 12.0, provider: "anthropic", deprecated: false },
WindsurfModel { canonical_name: "claude-opus-4-7-xhigh-thinking", enum_value: 0, model_uid: Some("claude-opus-4-7-xhigh-thinking"), credit_multiplier: 16.0, provider: "anthropic", deprecated: false },
WindsurfModel { canonical_name: "claude-opus-4-7-max", enum_value: 0, model_uid: Some("claude-opus-4-7-max"), credit_multiplier: 16.0, provider: "anthropic", deprecated: false },
WindsurfModel { canonical_name: "gpt-4o", enum_value: 109, model_uid: Some("MODEL_CHAT_GPT_4O_2024_08_06"), credit_multiplier: 1.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-4o-mini", enum_value: 113, model_uid: None, credit_multiplier: 0.5, provider: "openai", deprecated: true },
WindsurfModel { canonical_name: "gpt-4.1", enum_value: 259, model_uid: Some("MODEL_CHAT_GPT_4_1_2025_04_14"), credit_multiplier: 1.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-4.1-mini", enum_value: 260, model_uid: None, credit_multiplier: 0.5, provider: "openai", deprecated: true },
WindsurfModel { canonical_name: "gpt-4.1-nano", enum_value: 261, model_uid: None, credit_multiplier: 0.25, provider: "openai", deprecated: true },
WindsurfModel { canonical_name: "gpt-5", enum_value: 340, model_uid: Some("MODEL_PRIVATE_6"), credit_multiplier: 0.5, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5-medium", enum_value: 0, model_uid: Some("MODEL_PRIVATE_7"), credit_multiplier: 1.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5-high", enum_value: 0, model_uid: Some("MODEL_PRIVATE_8"), credit_multiplier: 2.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5-mini", enum_value: 337, model_uid: None, credit_multiplier: 0.25, provider: "openai", deprecated: true },
WindsurfModel { canonical_name: "gpt-5-codex", enum_value: 346, model_uid: Some("MODEL_CHAT_GPT_5_CODEX"), credit_multiplier: 0.5, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.1", enum_value: 0, model_uid: Some("MODEL_PRIVATE_12"), credit_multiplier: 0.5, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.1-low", enum_value: 0, model_uid: Some("MODEL_PRIVATE_13"), credit_multiplier: 0.5, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.1-medium", enum_value: 0, model_uid: Some("MODEL_PRIVATE_14"), credit_multiplier: 1.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.1-high", enum_value: 0, model_uid: Some("MODEL_PRIVATE_15"), credit_multiplier: 2.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.1-fast", enum_value: 0, model_uid: Some("MODEL_PRIVATE_20"), credit_multiplier: 1.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.1-low-fast", enum_value: 0, model_uid: Some("MODEL_PRIVATE_21"), credit_multiplier: 1.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.1-medium-fast", enum_value: 0, model_uid: Some("MODEL_PRIVATE_22"), credit_multiplier: 2.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.1-high-fast", enum_value: 0, model_uid: Some("MODEL_PRIVATE_23"), credit_multiplier: 4.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.1-codex-low", enum_value: 0, model_uid: Some("MODEL_GPT_5_1_CODEX_LOW"), credit_multiplier: 0.5, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.1-codex-medium", enum_value: 0, model_uid: Some("MODEL_PRIVATE_9"), credit_multiplier: 1.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.1-codex-mini-low", enum_value: 0, model_uid: Some("MODEL_GPT_5_1_CODEX_MINI_LOW"), credit_multiplier: 0.25, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.1-codex-mini", enum_value: 0, model_uid: Some("MODEL_PRIVATE_19"), credit_multiplier: 0.5, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.1-codex-max-low", enum_value: 0, model_uid: Some("MODEL_GPT_5_1_CODEX_MAX_LOW"), credit_multiplier: 1.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.1-codex-max-medium", enum_value: 0, model_uid: Some("MODEL_GPT_5_1_CODEX_MAX_MEDIUM"), credit_multiplier: 1.25, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.1-codex-max-high", enum_value: 0, model_uid: Some("MODEL_GPT_5_1_CODEX_MAX_HIGH"), credit_multiplier: 1.5, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.2", enum_value: 401, model_uid: Some("MODEL_GPT_5_2_MEDIUM"), credit_multiplier: 2.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.2-none", enum_value: 0, model_uid: Some("MODEL_GPT_5_2_NONE"), credit_multiplier: 1.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.2-low", enum_value: 400, model_uid: Some("MODEL_GPT_5_2_LOW"), credit_multiplier: 1.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.2-high", enum_value: 402, model_uid: Some("MODEL_GPT_5_2_HIGH"), credit_multiplier: 3.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.2-xhigh", enum_value: 403, model_uid: Some("MODEL_GPT_5_2_XHIGH"), credit_multiplier: 8.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.2-none-fast", enum_value: 0, model_uid: Some("MODEL_GPT_5_2_NONE_PRIORITY"), credit_multiplier: 2.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.2-low-fast", enum_value: 0, model_uid: Some("MODEL_GPT_5_2_LOW_PRIORITY"), credit_multiplier: 2.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.2-medium-fast", enum_value: 0, model_uid: Some("MODEL_GPT_5_2_MEDIUM_PRIORITY"), credit_multiplier: 4.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.2-high-fast", enum_value: 0, model_uid: Some("MODEL_GPT_5_2_HIGH_PRIORITY"), credit_multiplier: 6.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.2-xhigh-fast", enum_value: 0, model_uid: Some("MODEL_GPT_5_2_XHIGH_PRIORITY"), credit_multiplier: 16.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.2-codex-low", enum_value: 0, model_uid: Some("MODEL_GPT_5_2_CODEX_LOW"), credit_multiplier: 1.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.2-codex-medium", enum_value: 0, model_uid: Some("MODEL_GPT_5_2_CODEX_MEDIUM"), credit_multiplier: 1.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.2-codex-high", enum_value: 0, model_uid: Some("MODEL_GPT_5_2_CODEX_HIGH"), credit_multiplier: 2.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.2-codex-xhigh", enum_value: 0, model_uid: Some("MODEL_GPT_5_2_CODEX_XHIGH"), credit_multiplier: 3.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.2-codex-low-fast", enum_value: 0, model_uid: Some("MODEL_GPT_5_2_CODEX_LOW_PRIORITY"), credit_multiplier: 2.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.2-codex-medium-fast", enum_value: 0, model_uid: Some("MODEL_GPT_5_2_CODEX_MEDIUM_PRIORITY"), credit_multiplier: 2.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.2-codex-high-fast", enum_value: 0, model_uid: Some("MODEL_GPT_5_2_CODEX_HIGH_PRIORITY"), credit_multiplier: 4.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.2-codex-xhigh-fast", enum_value: 0, model_uid: Some("MODEL_GPT_5_2_CODEX_XHIGH_PRIORITY"), credit_multiplier: 6.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.3-codex", enum_value: 0, model_uid: Some("gpt-5-3-codex-medium"), credit_multiplier: 1.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.4-none", enum_value: 0, model_uid: Some("gpt-5-4-none"), credit_multiplier: 0.5, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.4-low", enum_value: 0, model_uid: Some("gpt-5-4-low"), credit_multiplier: 1.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.4-medium", enum_value: 0, model_uid: Some("gpt-5-4-medium"), credit_multiplier: 2.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.4-high", enum_value: 0, model_uid: Some("gpt-5-4-high"), credit_multiplier: 4.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.4-xhigh", enum_value: 0, model_uid: Some("gpt-5-4-xhigh"), credit_multiplier: 8.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.4-mini-low", enum_value: 0, model_uid: Some("gpt-5-4-mini-low"), credit_multiplier: 1.5, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.4-mini-medium", enum_value: 0, model_uid: Some("gpt-5-4-mini-medium"), credit_multiplier: 1.5, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.4-mini-high", enum_value: 0, model_uid: Some("gpt-5-4-mini-high"), credit_multiplier: 4.5, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.4-mini-xhigh", enum_value: 0, model_uid: Some("gpt-5-4-mini-xhigh"), credit_multiplier: 12.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.5", enum_value: 0, model_uid: Some("gpt-5-5-medium"), credit_multiplier: 2.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.5-none", enum_value: 0, model_uid: Some("gpt-5-5-none"), credit_multiplier: 1.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.5-low", enum_value: 0, model_uid: Some("gpt-5-5-low"), credit_multiplier: 1.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.5-medium", enum_value: 0, model_uid: Some("gpt-5-5-medium"), credit_multiplier: 2.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.5-high", enum_value: 0, model_uid: Some("gpt-5-5-high"), credit_multiplier: 4.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.5-xhigh", enum_value: 0, model_uid: Some("gpt-5-5-xhigh"), credit_multiplier: 8.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.5-none-fast", enum_value: 0, model_uid: Some("gpt-5-5-none-priority"), credit_multiplier: 2.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.5-low-fast", enum_value: 0, model_uid: Some("gpt-5-5-low-priority"), credit_multiplier: 2.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.5-medium-fast", enum_value: 0, model_uid: Some("gpt-5-5-medium-priority"), credit_multiplier: 4.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.5-high-fast", enum_value: 0, model_uid: Some("gpt-5-5-high-priority"), credit_multiplier: 8.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.5-xhigh-fast", enum_value: 0, model_uid: Some("gpt-5-5-xhigh-priority"), credit_multiplier: 16.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.3-codex-low", enum_value: 0, model_uid: Some("gpt-5-3-codex-low"), credit_multiplier: 0.5, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.3-codex-high", enum_value: 0, model_uid: Some("gpt-5-3-codex-high"), credit_multiplier: 2.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.3-codex-xhigh", enum_value: 0, model_uid: Some("gpt-5-3-codex-xhigh"), credit_multiplier: 4.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.3-codex-low-fast", enum_value: 0, model_uid: Some("gpt-5-3-codex-low-priority"), credit_multiplier: 1.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.3-codex-medium-fast", enum_value: 0, model_uid: Some("gpt-5-3-codex-medium-priority"), credit_multiplier: 2.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.3-codex-high-fast", enum_value: 0, model_uid: Some("gpt-5-3-codex-high-priority"), credit_multiplier: 4.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-5.3-codex-xhigh-fast", enum_value: 0, model_uid: Some("gpt-5-3-codex-xhigh-priority"), credit_multiplier: 6.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gpt-oss-120b", enum_value: 0, model_uid: Some("MODEL_GPT_OSS_120B"), credit_multiplier: 0.25, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "o3-mini", enum_value: 207, model_uid: None, credit_multiplier: 0.5, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "o3", enum_value: 218, model_uid: Some("MODEL_CHAT_O3"), credit_multiplier: 1.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "o3-high", enum_value: 0, model_uid: Some("MODEL_CHAT_O3_HIGH"), credit_multiplier: 1.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "o3-pro", enum_value: 294, model_uid: None, credit_multiplier: 4.0, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "o4-mini", enum_value: 264, model_uid: None, credit_multiplier: 0.5, provider: "openai", deprecated: false },
WindsurfModel { canonical_name: "gemini-2.5-pro", enum_value: 246, model_uid: Some("MODEL_GOOGLE_GEMINI_2_5_PRO"), credit_multiplier: 1.0, provider: "google", deprecated: false },
WindsurfModel { canonical_name: "gemini-2.5-flash", enum_value: 312, model_uid: Some("MODEL_GOOGLE_GEMINI_2_5_FLASH"), credit_multiplier: 0.5, provider: "google", deprecated: false },
WindsurfModel { canonical_name: "gemini-3.0-pro", enum_value: 412, model_uid: Some("MODEL_GOOGLE_GEMINI_3_0_PRO_LOW"), credit_multiplier: 1.0, provider: "google", deprecated: false },
WindsurfModel { canonical_name: "gemini-3.0-flash-minimal", enum_value: 0, model_uid: Some("MODEL_GOOGLE_GEMINI_3_0_FLASH_MINIMAL"), credit_multiplier: 0.75, provider: "google", deprecated: false },
WindsurfModel { canonical_name: "gemini-3.0-flash-low", enum_value: 0, model_uid: Some("MODEL_GOOGLE_GEMINI_3_0_FLASH_LOW"), credit_multiplier: 1.0, provider: "google", deprecated: false },
WindsurfModel { canonical_name: "gemini-3.0-flash", enum_value: 415, model_uid: Some("MODEL_GOOGLE_GEMINI_3_0_FLASH_MEDIUM"), credit_multiplier: 1.0, provider: "google", deprecated: false },
WindsurfModel { canonical_name: "gemini-3.0-flash-high", enum_value: 0, model_uid: Some("MODEL_GOOGLE_GEMINI_3_0_FLASH_HIGH"), credit_multiplier: 1.75, provider: "google", deprecated: false },
WindsurfModel { canonical_name: "gemini-3.1-pro-low", enum_value: 0, model_uid: Some("gemini-3-1-pro-low"), credit_multiplier: 1.0, provider: "google", deprecated: false },
WindsurfModel { canonical_name: "gemini-3.1-pro-high", enum_value: 0, model_uid: Some("gemini-3-1-pro-high"), credit_multiplier: 2.0, provider: "google", deprecated: false },
WindsurfModel { canonical_name: "deepseek-v3", enum_value: 205, model_uid: None, credit_multiplier: 0.5, provider: "deepseek", deprecated: true },
WindsurfModel { canonical_name: "deepseek-v3-2", enum_value: 409, model_uid: None, credit_multiplier: 0.5, provider: "deepseek", deprecated: true },
WindsurfModel { canonical_name: "deepseek-r1", enum_value: 206, model_uid: None, credit_multiplier: 1.0, provider: "deepseek", deprecated: true },
WindsurfModel { canonical_name: "grok-3", enum_value: 217, model_uid: Some("MODEL_XAI_GROK_3"), credit_multiplier: 1.0, provider: "xai", deprecated: false },
WindsurfModel { canonical_name: "grok-3-mini", enum_value: 234, model_uid: None, credit_multiplier: 0.5, provider: "xai", deprecated: true },
WindsurfModel { canonical_name: "grok-3-mini-thinking", enum_value: 0, model_uid: Some("MODEL_XAI_GROK_3_MINI_REASONING"), credit_multiplier: 0.125, provider: "xai", deprecated: false },
WindsurfModel { canonical_name: "grok-code-fast-1", enum_value: 0, model_uid: Some("MODEL_PRIVATE_4"), credit_multiplier: 0.5, provider: "xai", deprecated: false },
WindsurfModel { canonical_name: "qwen-3", enum_value: 324, model_uid: None, credit_multiplier: 0.5, provider: "alibaba", deprecated: true },
WindsurfModel { canonical_name: "kimi-k2", enum_value: 323, model_uid: Some("MODEL_KIMI_K2"), credit_multiplier: 0.5, provider: "moonshot", deprecated: false },
WindsurfModel { canonical_name: "kimi-k2-thinking", enum_value: 394, model_uid: Some("MODEL_KIMI_K2_THINKING"), credit_multiplier: 1.0, provider: "moonshot", deprecated: false },
WindsurfModel { canonical_name: "kimi-k2.5", enum_value: 0, model_uid: Some("kimi-k2-5"), credit_multiplier: 1.0, provider: "moonshot", deprecated: false },
WindsurfModel { canonical_name: "kimi-k2-6", enum_value: 0, model_uid: Some("kimi-k2-6"), credit_multiplier: 1.0, provider: "moonshot", deprecated: false },
WindsurfModel { canonical_name: "glm-4.7", enum_value: 417, model_uid: Some("MODEL_GLM_4_7"), credit_multiplier: 0.25, provider: "zhipu", deprecated: false },
WindsurfModel { canonical_name: "glm-4.7-fast", enum_value: 418, model_uid: Some("MODEL_GLM_4_7_FAST"), credit_multiplier: 0.5, provider: "zhipu", deprecated: false },
WindsurfModel { canonical_name: "glm-5", enum_value: 0, model_uid: Some("glm-5"), credit_multiplier: 1.5, provider: "zhipu", deprecated: false },
WindsurfModel { canonical_name: "glm-5.1", enum_value: 0, model_uid: Some("glm-5-1"), credit_multiplier: 1.5, provider: "zhipu", deprecated: false },
WindsurfModel { canonical_name: "minimax-m2.5", enum_value: 419, model_uid: Some("MODEL_MINIMAX_M2_1"), credit_multiplier: 1.0, provider: "minimax", deprecated: false },
WindsurfModel { canonical_name: "swe-1.5", enum_value: 377, model_uid: Some("MODEL_SWE_1_5_SLOW"), credit_multiplier: 0.5, provider: "windsurf", deprecated: false },
WindsurfModel { canonical_name: "swe-1.5-fast", enum_value: 359, model_uid: Some("MODEL_SWE_1_5"), credit_multiplier: 0.5, provider: "windsurf", deprecated: false },
WindsurfModel { canonical_name: "swe-1.5-thinking", enum_value: 369, model_uid: Some("MODEL_SWE_1_5_THINKING"), credit_multiplier: 0.75, provider: "windsurf", deprecated: false },
WindsurfModel { canonical_name: "swe-1.6", enum_value: 420, model_uid: Some("MODEL_SWE_1_6"), credit_multiplier: 0.5, provider: "windsurf", deprecated: false },
WindsurfModel { canonical_name: "swe-1.6-fast", enum_value: 421, model_uid: Some("MODEL_SWE_1_6_FAST"), credit_multiplier: 0.5, provider: "windsurf", deprecated: false },
WindsurfModel { canonical_name: "adaptive", enum_value: 0, model_uid: Some("adaptive"), credit_multiplier: 1.0, provider: "windsurf", deprecated: true },
WindsurfModel { canonical_name: "arena-fast", enum_value: 0, model_uid: Some("arena-fast"), credit_multiplier: 0.5, provider: "windsurf", deprecated: true },
WindsurfModel { canonical_name: "arena-smart", enum_value: 0, model_uid: Some("arena-smart"), credit_multiplier: 1.0, provider: "windsurf", deprecated: true },
];
#[rustfmt::skip]
const ALIASES: &[(&str, &str)] = &[
("claude-3-5-haiku-20241022", "claude-4.5-haiku"),
("claude-3-5-haiku-latest", "claude-4.5-haiku"),
("claude-3-5-sonnet-20240620", "claude-3.5-sonnet"),
("claude-3-5-sonnet-20241022", "claude-3.5-sonnet"),
("claude-3-5-sonnet-latest", "claude-3.5-sonnet"),
("claude-3-7-sonnet-20250219", "claude-3.7-sonnet"),
("claude-3-7-sonnet-latest", "claude-3.7-sonnet"),
("claude-4.6", "claude-sonnet-4.6"),
("claude-4.6-1m", "claude-sonnet-4.6-1m"),
("claude-4.6-thinking", "claude-sonnet-4.6-thinking"),
("claude-4.6-thinking-1m", "claude-sonnet-4.6-thinking-1m"),
("claude-haiku-3-5", "claude-4.5-haiku"),
("claude-haiku-3-5-latest", "claude-4.5-haiku"),
("claude-haiku-4-5", "claude-4.5-haiku"),
("claude-haiku-4-5-20251001", "claude-4.5-haiku"),
("claude-haiku-4-5-latest", "claude-4.5-haiku"),
("claude-haiku-4.5", "claude-4.5-haiku"),
("claude-haiku-4.5-latest", "claude-4.5-haiku"),
("claude-opus-4-0", "claude-4-opus"),
("claude-opus-4-1", "claude-4.1-opus"),
("claude-opus-4-1-20250805", "claude-4.1-opus"),
("claude-opus-4-20250514", "claude-4-opus"),
("claude-opus-4-5", "claude-4.5-opus"),
("claude-opus-4-5-20251101", "claude-4.5-opus"),
("claude-opus-4-5-latest", "claude-4.5-opus"),
("claude-opus-4-6", "claude-opus-4.6"),
("claude-opus-4-6-thinking", "claude-opus-4.6-thinking"),
("claude-opus-4-7", "claude-opus-4-7-medium"),
("claude-opus-4-7-latest", "claude-opus-4-7-medium"),
("claude-opus-4-7-thinking", "claude-opus-4-7-medium-thinking"),
("claude-opus-4.5", "claude-4.5-opus"),
("claude-opus-4.5-thinking", "claude-4.5-opus-thinking"),
("claude-opus-4.7", "claude-opus-4-7-medium"),
("claude-opus-4.7-high", "claude-opus-4-7-high"),
("claude-opus-4.7-high-thinking", "claude-opus-4-7-high-thinking"),
("claude-opus-4.7-low", "claude-opus-4-7-low"),
("claude-opus-4.7-max", "claude-opus-4-7-max"),
("claude-opus-4.7-medium", "claude-opus-4-7-medium"),
("claude-opus-4.7-medium-thinking", "claude-opus-4-7-medium-thinking"),
("claude-opus-4.7-thinking", "claude-opus-4-7-medium-thinking"),
("claude-opus-4.7-xhigh", "claude-opus-4-7-xhigh"),
("claude-opus-4.7-xhigh-thinking", "claude-opus-4-7-xhigh-thinking"),
("claude-sonnet-4-0", "claude-4-sonnet"),
("claude-sonnet-4-20250514", "claude-4-sonnet"),
("claude-sonnet-4-5", "claude-4.5-sonnet"),
("claude-sonnet-4-5-20250929", "claude-4.5-sonnet"),
("claude-sonnet-4-5-latest", "claude-4.5-sonnet"),
("claude-sonnet-4-6", "claude-sonnet-4.6"),
("claude-sonnet-4-6-1m", "claude-sonnet-4.6-1m"),
("claude-sonnet-4-6-thinking", "claude-sonnet-4.6-thinking"),
("claude-sonnet-4-6-thinking-1m", "claude-sonnet-4.6-thinking-1m"),
("claude-sonnet-4.5", "claude-4.5-sonnet"),
("claude-sonnet-4.5-thinking", "claude-4.5-sonnet-thinking"),
("gpt-4.1-2025-04-14", "gpt-4.1"),
("gpt-4.1-mini-2025-04-14", "gpt-4.1-mini"),
("gpt-4.1-nano-2025-04-14", "gpt-4.1-nano"),
("gpt-4o-2024-05-13", "gpt-4o"),
("gpt-4o-2024-08-06", "gpt-4o"),
("gpt-4o-2024-11-20", "gpt-4o"),
("gpt-4o-mini-2024-07-18", "gpt-4o-mini"),
("gpt-5-2-codex-medium", "gpt-5.2-codex-medium"),
("gpt-5-2-medium", "gpt-5.2"),
("gpt-5-2025-08-07", "gpt-5"),
("gpt-5-3-codex-high", "gpt-5.3-codex-high"),
("gpt-5-3-codex-high-priority", "gpt-5.3-codex-high-fast"),
("gpt-5-3-codex-low", "gpt-5.3-codex-low"),
("gpt-5-3-codex-low-priority", "gpt-5.3-codex-low-fast"),
("gpt-5-3-codex-medium", "gpt-5.3-codex"),
("gpt-5-3-codex-medium-priority", "gpt-5.3-codex-medium-fast"),
("gpt-5-3-codex-xhigh", "gpt-5.3-codex-xhigh"),
("gpt-5-3-codex-xhigh-priority", "gpt-5.3-codex-xhigh-fast"),
("gpt-5-4-high", "gpt-5.4-high"),
("gpt-5-4-low", "gpt-5.4-low"),
("gpt-5-4-medium", "gpt-5.4-medium"),
("gpt-5-4-mini-high", "gpt-5.4-mini-high"),
("gpt-5-4-mini-low", "gpt-5.4-mini-low"),
("gpt-5-4-mini-medium", "gpt-5.4-mini-medium"),
("gpt-5-4-mini-xhigh", "gpt-5.4-mini-xhigh"),
("gpt-5-4-none", "gpt-5.4-none"),
("gpt-5-4-xhigh", "gpt-5.4-xhigh"),
("gpt-5-5", "gpt-5.5-medium"),
("gpt-5-5-high", "gpt-5.5-high"),
("gpt-5-5-high-priority", "gpt-5.5-high-fast"),
("gpt-5-5-low", "gpt-5.5-low"),
("gpt-5-5-low-priority", "gpt-5.5-low-fast"),
("gpt-5-5-medium", "gpt-5.5-medium"),
("gpt-5-5-medium-priority", "gpt-5.5-medium-fast"),
("gpt-5-5-none", "gpt-5.5-none"),
("gpt-5-5-none-priority", "gpt-5.5-none-fast"),
("gpt-5-5-xhigh", "gpt-5.5-xhigh"),
("gpt-5-5-xhigh-priority", "gpt-5.5-xhigh-fast"),
("gpt-5-pro-2025-10-06", "gpt-5-high"),
("gpt-5.2-codex", "gpt-5.2-codex-medium"),
("gpt-5.2-medium", "gpt-5.2"),
("gpt-5.3-codex-medium", "gpt-5.3-codex"),
("gpt-5.4", "gpt-5.4-medium"),
("gpt-5.5", "gpt-5.5-medium"),
("haiku-4.5", "claude-4.5-haiku"),
("kimi-k2-5", "kimi-k2.5"),
("minimax-m2-5", "minimax-m2.5"),
("model_claude_4_5_sonnet", "claude-4.5-sonnet"),
("model_claude_4_5_sonnet_thinking", "claude-4.5-sonnet-thinking"),
("o4.7", "claude-opus-4-7-medium"),
("opus-4", "claude-4-opus"),
("opus-4-7", "claude-opus-4-7-medium"),
("opus-4.1", "claude-4.1-opus"),
("opus-4.6", "claude-opus-4.6"),
("opus-4.6-thinking", "claude-opus-4.6-thinking"),
("opus-4.7", "claude-opus-4-7-medium"),
("opus-4.7-thinking", "claude-opus-4-7-medium-thinking"),
("sonnet-3.5", "claude-3.5-sonnet"),
("sonnet-3.7", "claude-3.7-sonnet"),
("sonnet-4", "claude-4-sonnet"),
("sonnet-4.5", "claude-4.5-sonnet"),
("sonnet-4.5-thinking", "claude-4.5-sonnet-thinking"),
("sonnet-4.6", "claude-sonnet-4.6"),
("sonnet-4.6-1m", "claude-sonnet-4.6-1m"),
("sonnet-4.6-thinking", "claude-sonnet-4.6-thinking"),
("swe-1-6", "swe-1.6"),
("swe-1-6-fast", "swe-1.6-fast"),
("ws-haiku", "claude-4.5-haiku"),
("ws-opus", "claude-opus-4.6"),
("ws-opus-thinking", "claude-opus-4.6-thinking"),
("ws-sonnet", "claude-sonnet-4.6"),
("ws-sonnet-thinking", "claude-sonnet-4.6-thinking"),
];
pub fn windsurf_models() -> &'static [WindsurfModel] {
MODELS
}
pub fn resolve_windsurf_model(name: &str) -> Option<WindsurfModel> {
let normalized = name.trim().to_ascii_lowercase();
if normalized.is_empty() {
return None;
}
let canonical = ALIASES
.iter()
.find_map(|(alias, canonical)| (*alias == normalized).then_some(*canonical))
.unwrap_or(normalized.as_str());
MODELS
.iter()
.find(|model| model_matches(model, canonical))
.cloned()
}
fn model_matches(model: &WindsurfModel, value: &str) -> bool {
model.canonical_name.eq_ignore_ascii_case(value)
|| model
.model_uid
.is_some_and(|uid| uid.eq_ignore_ascii_case(value))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolves_gpt55_cloud_alias_to_windsurf_model_uid() {
let model = resolve_windsurf_model("gpt-5-5-low").expect("model should resolve");
assert_eq!(model.canonical_name, "gpt-5.5-low");
assert_eq!(model.model_uid.as_deref(), Some("gpt-5-5-low"));
assert_eq!(model.enum_value, 0);
assert_eq!(model.credit_multiplier, 1.0);
}
#[test]
fn resolves_claude_opus_47_bare_alias_to_medium() {
let model = resolve_windsurf_model("claude-opus-4.7").expect("model should resolve");
assert_eq!(model.canonical_name, "claude-opus-4-7-medium");
assert_eq!(model.model_uid.as_deref(), Some("claude-opus-4-7-medium"));
assert_eq!(model.enum_value, 0);
assert_eq!(model.credit_multiplier, 8.0);
}
#[test]
fn resolves_priority_alias_to_fast_variant() {
let model = resolve_windsurf_model("gpt-5-5-low-priority").expect("model should resolve");
assert_eq!(model.canonical_name, "gpt-5.5-low-fast");
assert_eq!(model.model_uid.as_deref(), Some("gpt-5-5-low-priority"));
assert_eq!(model.credit_multiplier, 2.0);
}
#[test]
fn resolves_full_gpt55_effort_ladder_and_priority_aliases() {
let none = resolve_windsurf_model("gpt-5-5-none").expect("none should resolve");
assert_eq!(none.canonical_name, "gpt-5.5-none");
assert_eq!(none.model_uid.as_deref(), Some("gpt-5-5-none"));
assert_eq!(none.credit_multiplier, 1.0);
let high = resolve_windsurf_model("gpt-5.5-high").expect("high should resolve");
assert_eq!(high.canonical_name, "gpt-5.5-high");
assert_eq!(high.model_uid.as_deref(), Some("gpt-5-5-high"));
assert_eq!(high.credit_multiplier, 4.0);
let xhigh_fast = resolve_windsurf_model("gpt-5-5-xhigh-priority")
.expect("xhigh priority should resolve");
assert_eq!(xhigh_fast.canonical_name, "gpt-5.5-xhigh-fast");
assert_eq!(
xhigh_fast.model_uid.as_deref(),
Some("gpt-5-5-xhigh-priority")
);
assert_eq!(xhigh_fast.credit_multiplier, 16.0);
}
#[test]
fn resolves_windsurfapi_catalog_aliases_beyond_gpt55() {
let gpt52_medium = resolve_windsurf_model("gpt-5.2-medium").expect("gpt-5.2 medium alias");
assert_eq!(gpt52_medium.canonical_name, "gpt-5.2");
assert_eq!(
gpt52_medium.model_uid.as_deref(),
Some("MODEL_GPT_5_2_MEDIUM")
);
let haiku = resolve_windsurf_model("claude-haiku-4-5-20251001").expect("dated haiku alias");
assert_eq!(haiku.canonical_name, "claude-4.5-haiku");
assert_eq!(haiku.model_uid.as_deref(), Some("MODEL_PRIVATE_11"));
let uid = resolve_windsurf_model("MODEL_GPT_5_2_LOW").expect("model uid alias");
assert_eq!(uid.canonical_name, "gpt-5.2-low");
assert_eq!(uid.enum_value, 400);
let cursor = resolve_windsurf_model("ws-opus").expect("cursor alias");
assert_eq!(cursor.canonical_name, "claude-opus-4.6");
}
#[test]
fn windsurf_catalog_covers_current_windsurfapi_model_set() {
assert_eq!(MODELS.len(), 139);
assert!(ALIASES.len() >= 100);
}
}

View File

@@ -0,0 +1,248 @@
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WireType {
Varint = 0,
Fixed64 = 1,
Len = 2,
Fixed32 = 5,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FieldValue {
Varint(u64),
Bytes(Vec<u8>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Field {
pub number: u32,
pub wire_type: WireType,
pub value: FieldValue,
}
impl Field {
pub fn bytes(&self) -> &[u8] {
match &self.value {
FieldValue::Bytes(bytes) => bytes.as_slice(),
FieldValue::Varint(_) => &[],
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProtoError {
message: String,
}
impl ProtoError {
fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
impl fmt::Display for ProtoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for ProtoError {}
pub fn encode_varint(value: u64) -> Vec<u8> {
let mut value = value;
let mut out = Vec::new();
loop {
let mut byte = (value & 0x7f) as u8;
value >>= 7;
if value != 0 {
byte |= 0x80;
}
out.push(byte);
if value == 0 {
break;
}
}
out
}
fn decode_varint(buf: &[u8], offset: usize) -> Result<(u64, usize), ProtoError> {
let mut value = 0u64;
let mut shift = 0u32;
let mut pos = offset;
while pos < buf.len() {
let byte = buf[pos];
pos += 1;
value |= u64::from(byte & 0x7f) << shift;
if byte & 0x80 == 0 {
return Ok((value, pos - offset));
}
shift += 7;
if shift >= 64 {
return Err(ProtoError::new("varint overflow"));
}
}
Err(ProtoError::new("truncated varint"))
}
fn tag(field: u32, wire_type: WireType) -> Vec<u8> {
encode_varint((u64::from(field) << 3) | wire_type as u64)
}
pub fn write_varint_field(field: u32, value: u64) -> Vec<u8> {
let mut out = tag(field, WireType::Varint);
out.extend(encode_varint(value));
out
}
pub fn write_string_field(field: u32, value: &str) -> Vec<u8> {
let bytes = value.as_bytes();
let mut out = tag(field, WireType::Len);
out.extend(encode_varint(bytes.len() as u64));
out.extend(bytes);
out
}
pub fn write_message_field(field: u32, value: &[u8]) -> Vec<u8> {
if value.is_empty() {
return Vec::new();
}
let mut out = tag(field, WireType::Len);
out.extend(encode_varint(value.len() as u64));
out.extend(value);
out
}
pub fn write_bool_field(field: u32, value: bool) -> Vec<u8> {
if value {
write_varint_field(field, 1)
} else {
Vec::new()
}
}
pub fn parse_fields(buf: &[u8]) -> Result<Vec<Field>, ProtoError> {
let mut fields = Vec::new();
let mut pos = 0usize;
while pos < buf.len() {
let (tag, tag_len) = decode_varint(buf, pos)?;
pos += tag_len;
let number = (tag >> 3) as u32;
let wire_type = match tag & 0x07 {
0 => WireType::Varint,
1 => WireType::Fixed64,
2 => WireType::Len,
5 => WireType::Fixed32,
other => {
return Err(ProtoError::new(format!(
"unknown wire type {other} at offset {pos}"
)))
}
};
let value = match wire_type {
WireType::Varint => {
let (value, value_len) = decode_varint(buf, pos)?;
pos += value_len;
FieldValue::Varint(value)
}
WireType::Len => {
let (len, len_len) = decode_varint(buf, pos)?;
pos += len_len;
let len = usize::try_from(len)
.map_err(|_| ProtoError::new("length-delimited field too large"))?;
if pos + len > buf.len() {
return Err(ProtoError::new(format!(
"truncated len-delimited field {number} at offset {pos}"
)));
}
let bytes = buf[pos..pos + len].to_vec();
pos += len;
FieldValue::Bytes(bytes)
}
WireType::Fixed64 => {
if pos + 8 > buf.len() {
return Err(ProtoError::new(format!("truncated fixed64 field {number}")));
}
let bytes = buf[pos..pos + 8].to_vec();
pos += 8;
FieldValue::Bytes(bytes)
}
WireType::Fixed32 => {
if pos + 4 > buf.len() {
return Err(ProtoError::new(format!("truncated fixed32 field {number}")));
}
let bytes = buf[pos..pos + 4].to_vec();
pos += 4;
FieldValue::Bytes(bytes)
}
};
fields.push(Field {
number,
wire_type,
value,
});
}
Ok(fields)
}
pub fn get_field(fields: &[Field], number: u32, wire_type: Option<WireType>) -> Option<&Field> {
fields
.iter()
.find(|field| field.number == number && wire_type.is_none_or(|ty| field.wire_type == ty))
}
pub fn get_all_fields(fields: &[Field], number: u32) -> Vec<&Field> {
fields
.iter()
.filter(|field| field.number == number)
.collect()
}
pub fn get_varint(fields: &[Field], number: u32) -> Option<u64> {
match get_field(fields, number, Some(WireType::Varint))?.value {
FieldValue::Varint(value) => Some(value),
FieldValue::Bytes(_) => None,
}
}
pub fn get_string(fields: &[Field], number: u32) -> Option<String> {
let field = get_field(fields, number, Some(WireType::Len))?;
String::from_utf8(field.bytes().to_vec()).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encodes_varint_and_string_fields_like_windsurfapi() {
assert_eq!(encode_varint(300), vec![0xac, 0x02]);
assert_eq!(
write_string_field(3, "abc"),
vec![0x1a, 0x03, b'a', b'b', b'c']
);
assert_eq!(write_bool_field(2, false), Vec::<u8>::new());
assert_eq!(write_bool_field(2, true), vec![0x10, 0x01]);
}
#[test]
fn parses_repeated_len_delimited_fields() {
let mut bytes = Vec::new();
bytes.extend(write_string_field(1, "alpha"));
bytes.extend(write_string_field(1, "beta"));
bytes.extend(write_varint_field(2, 42));
let fields = parse_fields(&bytes).expect("fields should parse");
assert_eq!(get_all_fields(&fields, 1).len(), 2);
assert_eq!(get_string(&fields, 1).as_deref(), Some("alpha"));
assert_eq!(get_varint(&fields, 2), Some(42));
}
#[test]
fn rejects_truncated_len_delimited_field() {
let err = parse_fields(&[0x0a, 0x05, b'a']).expect_err("must reject truncated field");
assert!(err.to_string().contains("truncated"));
}
}

View File

@@ -153,20 +153,16 @@ pub fn resolve_provider_model_name_with_model_directives(
return None;
}
if key_allowed_models
.iter()
.any(|value| value == requested_model_name)
for candidate_name in
requested_model_name_candidates(requested_model_name, enable_model_directives)
{
return Some((selected_provider_model_name, None));
}
if enable_model_directives {
if let Some(base_model) =
aether_ai_formats::model_directive_base_model(requested_model_name)
if key_allowed_models
.iter()
.any(|value| value == candidate_name.as_ref())
{
if key_allowed_models.iter().any(|value| value == &base_model) {
return Some((selected_provider_model_name, Some(base_model)));
}
let matched = (candidate_name.as_ref() != requested_model_name)
.then(|| candidate_name.into_owned());
return Some((selected_provider_model_name, matched));
}
}
@@ -428,10 +424,55 @@ fn requested_model_name_candidates(
enable_model_directives: bool,
) -> impl Iterator<Item = Cow<'_, str>> {
let requested_model_name = requested_model_name.trim();
let base_model = enable_model_directives
.then(|| aether_ai_formats::model_directive_base_model(requested_model_name))
.flatten();
std::iter::once(Cow::Borrowed(requested_model_name)).chain(base_model.map(Cow::Owned))
let mut candidates = Vec::new();
push_model_name_candidate(&mut candidates, Cow::Borrowed(requested_model_name));
for alias in requested_model_name_aliases(requested_model_name) {
push_model_name_candidate(&mut candidates, Cow::Owned(alias));
}
if enable_model_directives {
if let Some(base_model) =
aether_ai_formats::model_directive_base_model(requested_model_name)
{
for alias in requested_model_name_aliases(&base_model) {
push_model_name_candidate(&mut candidates, Cow::Owned(alias));
}
push_model_name_candidate(&mut candidates, Cow::Owned(base_model));
}
}
candidates.into_iter()
}
fn push_model_name_candidate<'a>(candidates: &mut Vec<Cow<'a, str>>, candidate: Cow<'a, str>) {
if candidate.trim().is_empty() {
return;
}
if candidates
.iter()
.any(|existing| existing.as_ref() == candidate.as_ref())
{
return;
}
candidates.push(candidate);
}
fn requested_model_name_aliases(requested_model_name: &str) -> Vec<String> {
let requested_model_name = requested_model_name.trim();
let Some(alias) = windsurf_gpt55_model_alias(requested_model_name) else {
return Vec::new();
};
vec![alias]
}
fn windsurf_gpt55_model_alias(model_name: &str) -> Option<String> {
let suffix = model_name
.strip_prefix("gpt-5-5")
.map(|suffix| format!("gpt-5.5{suffix}"))
.or_else(|| {
model_name
.strip_prefix("gpt-5.5")
.map(|suffix| format!("gpt-5-5{suffix}"))
})?;
(suffix != model_name).then_some(suffix)
}
#[cfg(test)]
@@ -544,6 +585,39 @@ mod tests {
assert_eq!(resolved.1.as_deref(), Some("gpt-5.4"));
}
#[test]
fn windsurf_dashed_gpt55_alias_matches_dotted_model_name() {
let row = sample_row("gpt-5.5-low", "gpt-5.5-low");
assert!(row_supports_requested_model(
&row,
"gpt-5-5-low",
"openai:chat"
));
assert_eq!(
resolve_requested_global_model_name_with_model_directives(
&[row],
"gpt-5-5-low",
"openai:chat",
false,
)
.as_deref(),
Some("gpt-5.5-low")
);
}
#[test]
fn windsurf_dashed_gpt55_alias_satisfies_key_allowed_models() {
let mut row = sample_row("gpt-5.5-low", "windsurf-upstream-uid");
row.key_allowed_models = Some(vec!["gpt-5.5-low".to_string()]);
let resolved = resolve_provider_model_name(&row, "gpt-5-5-low", "openai:chat")
.expect("dashed alias should satisfy dotted allowed model");
assert_eq!(resolved.0, "windsurf-upstream-uid");
assert_eq!(resolved.1.as_deref(), Some("gpt-5.5-low"));
}
#[test]
fn endpoint_scoped_default_mapping_limits_exact_global_model_match() {
let mut row = sample_row("deepseek-v4-pro", "deepseek-v4-pro");