Merge remote-tracking branch 'origin/main' into fix/gemini-cli-v1internal

# Conflicts:
#	apps/aether-gateway/src/ai_serving/transport.rs
#	apps/aether-gateway/src/handlers/admin/provider/oauth/dispatch/batch/parse.rs
#	apps/aether-gateway/src/handlers/shared/catalog.rs
#	crates/aether-admin/src/provider/quota.rs
#	crates/aether-model-fetch/src/strategy.rs
#	crates/aether-provider-pool/src/lib.rs
#	crates/aether-provider-pool/src/service.rs
#	crates/aether-provider-transport/src/provider_types.rs
#	frontend/src/features/providers/components/ProviderDetailDrawer.vue
#	frontend/src/utils/__tests__/providerKeyQuota.spec.ts
#	frontend/src/utils/providerKeyQuota.ts
#	frontend/src/views/admin/PoolManagement.vue
This commit is contained in:
Mas0nShi
2026-05-22 17:13:57 +08:00
220 changed files with 30324 additions and 2776 deletions
+5 -4
View File
@@ -175,6 +175,7 @@ pub use crate::formats::{
build_local_openai_chat_request_body_with_model_directives,
build_local_openai_responses_request_body,
build_local_openai_responses_request_body_with_model_directives,
is_claude_messages_shaped_body_on_openai_chat_endpoint,
},
},
};
@@ -211,10 +212,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,
@@ -275,6 +275,10 @@ mod tests {
"gemini:generate_content"
));
assert!(!api_format_alias_matches("openai:cli", "openai:responses"));
assert_eq!(
normalize_api_format_alias("openai:compact"),
"openai:compact"
);
}
#[test]
@@ -1180,6 +1180,23 @@ impl OpenAIResponsesProviderState {
}
}
}
event_type if openai_stream_payload_is_terminal_error(&value) => {
self.finished = true;
let mut payload = value.clone();
if event_type != "response.failed"
&& event_type != "response.incomplete"
&& event_type != "error"
{
payload = openai_stream_terminal_error_body(&value).unwrap_or(payload);
if let Some(object) = payload.as_object_mut() {
object.insert(
"type".to_string(),
Value::String("response.failed".to_string()),
);
}
}
out.push(self.unknown_frame(report_context, payload));
}
"response.completed" => {
let Some(response) = value.get("response").and_then(Value::as_object) else {
return Ok(out);
@@ -1291,6 +1308,8 @@ pub struct OpenAIChatClientEmitter {
model: Option<String>,
started: bool,
finished: bool,
next_tool_call_index: usize,
tool_call_index_by_canonical: BTreeMap<usize, usize>,
}
#[derive(Clone, Default)]
@@ -1299,6 +1318,7 @@ struct OpenAIResponsesClientToolState {
name: String,
arguments: String,
output_index: Option<usize>,
web_search: bool,
}
#[derive(Clone, Default)]
@@ -1310,6 +1330,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>,
@@ -1357,6 +1394,17 @@ impl OpenAIChatClientEmitter {
)
}
fn chat_tool_call_index(&mut self, canonical_index: usize) -> usize {
if let Some(index) = self.tool_call_index_by_canonical.get(&canonical_index) {
return *index;
}
let index = self.next_tool_call_index;
self.next_tool_call_index += 1;
self.tool_call_index_by_canonical
.insert(canonical_index, index);
index
}
pub fn emit(&mut self, frame: CanonicalStreamFrame) -> Result<Vec<u8>, AiSurfaceFinalizeError> {
self.update_identity(&frame);
match frame.event {
@@ -1465,6 +1513,7 @@ impl OpenAIChatClientEmitter {
name,
} => {
let mut out = self.ensure_started()?;
let chat_index = self.chat_tool_call_index(index);
out.extend(encode_json_sse(
None,
&build_openai_chat_chunk(
@@ -1474,7 +1523,7 @@ impl OpenAIChatClientEmitter {
self.model.as_deref().unwrap_or("unknown"),
String::new(),
Some(vec![json!({
"index": index,
"index": chat_index,
"id": call_id,
"type": "function",
"function": {
@@ -1489,6 +1538,7 @@ impl OpenAIChatClientEmitter {
}
CanonicalStreamEvent::ToolCallArgumentsDelta { index, arguments } => {
let mut out = self.ensure_started()?;
let chat_index = self.chat_tool_call_index(index);
out.extend(encode_json_sse(
None,
&json!({
@@ -1501,7 +1551,7 @@ impl OpenAIChatClientEmitter {
"index": 0,
"delta": {
"tool_calls": [{
"index": index,
"index": chat_index,
"function": {
"arguments": arguments,
}
@@ -1544,6 +1594,13 @@ impl OpenAIChatClientEmitter {
)?);
Ok(out)
}
CanonicalStreamEvent::UnknownEvent(payload)
if openai_stream_terminal_error_body(&payload).is_some() =>
{
self.finished = true;
let error_body = openai_stream_terminal_error_body(&payload).unwrap_or(payload);
encode_json_sse(None, &error_body)
}
CanonicalStreamEvent::UnknownEvent(_) => Ok(Vec::new()),
CanonicalStreamEvent::Finish {
finish_reason,
@@ -1985,6 +2042,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 +2220,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 +2411,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 +2451,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 {
@@ -2441,6 +2547,29 @@ impl OpenAIResponsesClientEmitter {
}
Ok(out)
}
CanonicalStreamEvent::UnknownEvent(payload)
if openai_stream_terminal_error_body(&payload).is_some() =>
{
self.finished = true;
let raw_event = payload.get("type").and_then(Value::as_str);
let event = raw_event
.filter(|event| {
matches!(*event, "response.failed" | "response.incomplete" | "error")
})
.unwrap_or("response.failed")
.to_string();
let mut payload = if raw_event == Some(event.as_str()) {
payload
} else {
openai_stream_terminal_error_body(&payload).unwrap_or(payload)
};
if payload.get("type").is_none() {
if let Some(object) = payload.as_object_mut() {
object.insert("type".to_string(), Value::String(event.clone()));
}
}
self.encode_response_event(event.as_str(), payload)
}
CanonicalStreamEvent::UnknownEvent(_) => Ok(Vec::new()),
CanonicalStreamEvent::Finish { usage, .. } => {
if self.finished {
@@ -2588,6 +2717,27 @@ mod tests {
parts
}
fn openai_chat_tool_call_indices(sse: &str) -> Vec<u64> {
let mut indices = Vec::new();
for payload in sse.lines().filter_map(|line| line.strip_prefix("data: ")) {
let Ok(value) = serde_json::from_str::<Value>(payload) else {
continue;
};
let Some(tool_calls) = value
.pointer("/choices/0/delta/tool_calls")
.and_then(Value::as_array)
else {
continue;
};
for tool_call in tool_calls {
if let Some(index) = tool_call.get("index").and_then(Value::as_u64) {
indices.push(index);
}
}
}
indices
}
#[test]
fn openai_chat_provider_state_emits_unknown_events_for_unrecognized_deltas() {
let mut state = OpenAIChatProviderState::default();
@@ -2646,6 +2796,40 @@ mod tests {
)));
}
#[test]
fn openai_responses_provider_state_treats_failed_event_as_terminal() {
let mut state = OpenAIResponsesProviderState::default();
let report_context = json!({});
let frames = state
.push_line(
&report_context,
data_line(json!({
"type": "response.failed",
"response": {
"id": "resp_failed_123",
"model": "gpt-5.4",
"status": "failed",
"error": {
"message": "policy failure",
"type": "invalid_request_error",
"code": "cyber_policy"
}
}
})),
)
.expect("failed response event should parse");
assert!(frames.iter().any(|frame| matches!(
frame.event,
CanonicalStreamEvent::UnknownEvent(ref payload)
if payload.get("type").and_then(Value::as_str) == Some("response.failed")
)));
assert!(state
.finish(&report_context)
.expect("terminal failure should not synthesize completion")
.is_empty());
}
#[test]
fn openai_usage_derives_missing_input_tokens_from_total() {
let usage = canonical_usage_from_openai_usage(Some(&json!({
@@ -2812,6 +2996,41 @@ mod tests {
assert_eq!(response_sequence_numbers(&sse), (1..=9).collect::<Vec<_>>());
}
#[test]
fn openai_responses_client_emitter_forwards_failed_unknown_event() {
let mut emitter = OpenAIResponsesClientEmitter::default();
let bytes = emitter
.emit(CanonicalStreamFrame {
id: "resp_failed_123".to_string(),
model: "gpt-5.4".to_string(),
event: CanonicalStreamEvent::UnknownEvent(json!({
"type": "response.failed",
"response": {
"id": "resp_failed_123",
"model": "gpt-5.4",
"status": "failed",
"error": {
"message": "policy failure",
"type": "invalid_request_error",
"code": "cyber_policy"
}
}
})),
})
.expect("failed response event should encode");
let mut all = bytes;
all.extend(
emitter
.finish()
.expect("failed stream should not synthesize completion"),
);
let sse = String::from_utf8(all).expect("sse should be utf8");
assert!(sse.contains("event: response.failed\n"));
assert!(sse.contains("\"message\":\"policy failure\""));
assert!(!sse.contains("event: response.completed\n"));
}
#[test]
fn openai_responses_client_emitter_keeps_text_item_id_stable_after_text_started() {
let mut emitter = OpenAIResponsesClientEmitter::default();
@@ -3167,6 +3386,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();
@@ -3250,6 +3519,50 @@ mod tests {
assert!(sse.contains("[Image]"));
}
#[test]
fn openai_chat_client_emitter_normalizes_sparse_tool_call_indices() {
let mut emitter = OpenAIChatClientEmitter::default();
let mut bytes = Vec::new();
for event in [
CanonicalStreamEvent::ToolCallStart {
index: 1,
call_id: "call_first".to_string(),
name: "first_tool".to_string(),
},
CanonicalStreamEvent::ToolCallArgumentsDelta {
index: 1,
arguments: "{\"first\":".to_string(),
},
CanonicalStreamEvent::ToolCallStart {
index: 3,
call_id: "call_second".to_string(),
name: "second_tool".to_string(),
},
CanonicalStreamEvent::ToolCallArgumentsDelta {
index: 3,
arguments: "{\"second\":true}".to_string(),
},
CanonicalStreamEvent::ToolCallArgumentsDelta {
index: 1,
arguments: "true}".to_string(),
},
] {
bytes.extend(
emitter
.emit(CanonicalStreamFrame {
id: "chatcmpl_sparse".to_string(),
model: "claude-opus-4-6".to_string(),
event,
})
.expect("tool event should encode"),
);
}
let sse = String::from_utf8(bytes).expect("sse should be utf8");
assert_eq!(openai_chat_tool_call_indices(&sse), vec![0, 0, 1, 1, 0]);
}
#[test]
fn openai_chat_client_emitter_emits_usage_only_final_chunk() {
let mut emitter = OpenAIChatClientEmitter::default();
@@ -165,14 +165,25 @@ fn inject_codex_default_variation_prompt(body_object: &mut serde_json::Map<Strin
);
}
fn build_stable_codex_prompt_cache_key(user_api_key_id: &str) -> Option<String> {
let normalized = user_api_key_id.trim();
fn build_stable_codex_prompt_cache_key_from_seed(kind: &str, seed: &str) -> Option<String> {
let normalized = seed.trim();
if normalized.is_empty() {
return None;
}
let normalized_kind = kind
.trim()
.to_ascii_lowercase()
.chars()
.filter(|ch| ch.is_ascii_alphanumeric() || *ch == '_' || *ch == '-')
.collect::<String>();
let normalized_kind = if normalized_kind.is_empty() {
"seed".to_string()
} else {
normalized_kind
};
let namespace = format!(
"aether:codex:prompt-cache:{CODEX_PROMPT_CACHE_NAMESPACE_VERSION}:user:{normalized}"
"aether:codex:prompt-cache:{CODEX_PROMPT_CACHE_NAMESPACE_VERSION}:{normalized_kind}:{normalized}"
);
let mut hasher = Sha1::new();
hasher.update(UUID_NAMESPACE_OID_BYTES);
@@ -186,6 +197,266 @@ fn build_stable_codex_prompt_cache_key(user_api_key_id: &str) -> Option<String>
Some(Uuid::from_bytes(bytes).to_string())
}
fn build_stable_codex_prompt_cache_key(user_api_key_id: &str) -> Option<String> {
build_stable_codex_prompt_cache_key_from_seed("user", user_api_key_id)
}
fn extract_codex_prompt_cache_session_seed(provider_request_body: &Value) -> Option<String> {
fn non_empty_str(value: Option<&Value>) -> Option<&str> {
value
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
}
fn session_seed_from_metadata(metadata: &Value) -> Option<String> {
let object = metadata.as_object()?;
non_empty_str(object.get("session_id"))
.or_else(|| non_empty_str(object.get("sessionId")))
.or_else(|| non_empty_str(object.get("conversation_id")))
.or_else(|| non_empty_str(object.get("conversationId")))
.map(|value| format!("metadata:{value}"))
.or_else(|| {
let user_id = non_empty_str(object.get("user_id"))?;
serde_json::from_str::<Value>(user_id)
.ok()
.and_then(|decoded| {
non_empty_str(decoded.get("session_id"))
.or_else(|| non_empty_str(decoded.get("sessionId")))
.or_else(|| non_empty_str(decoded.get("conversation_id")))
.or_else(|| non_empty_str(decoded.get("conversationId")))
.map(|value| format!("metadata.user_id:{value}"))
})
})
}
let object = provider_request_body.as_object()?;
non_empty_str(object.get("session_id"))
.or_else(|| non_empty_str(object.get("sessionId")))
.or_else(|| non_empty_str(object.get("conversation_id")))
.or_else(|| non_empty_str(object.get("conversationId")))
.map(|value| format!("body:{value}"))
.or_else(|| object.get("metadata").and_then(session_seed_from_metadata))
}
fn sha256_hex(input: &[u8]) -> String {
let digest = Sha256::digest(input);
let mut output = String::with_capacity(digest.len() * 2);
for byte in digest {
let _ = write!(&mut output, "{byte:02x}");
}
output
}
fn stable_json_digest(value: &Value) -> Option<String> {
serde_json::to_vec(value)
.ok()
.map(|serialized| sha256_hex(&serialized))
}
fn compact_prompt_cache_text(value: &str) -> Option<Value> {
const MAX_PROMPT_CACHE_TEXT_CHARS: usize = 4096;
let normalized = value.trim();
if normalized.is_empty() {
return None;
}
let mut text = normalized
.chars()
.take(MAX_PROMPT_CACHE_TEXT_CHARS)
.collect::<String>();
if normalized.chars().count() > MAX_PROMPT_CACHE_TEXT_CHARS {
text.push_str("...");
}
Some(Value::String(text))
}
fn compact_prompt_cache_anchor(value: &Value) -> Value {
match value {
Value::String(text) => compact_prompt_cache_text(text).unwrap_or(Value::Null),
Value::Array(items) => Value::Array(
items
.iter()
.take(16)
.map(compact_prompt_cache_anchor)
.filter(|value| !value.is_null())
.collect(),
),
Value::Object(object) => {
let mut compacted = serde_json::Map::new();
for key in [
"type",
"role",
"id",
"name",
"description",
"text",
"input_text",
"output_text",
"content",
"call_id",
"arguments",
"output",
"parameters",
"strict",
"function",
"effort",
"summary",
] {
let Some(value) = object.get(key) else {
continue;
};
let value = compact_prompt_cache_anchor(value);
if !value.is_null() {
compacted.insert(key.to_string(), value);
}
}
Value::Object(compacted)
}
Value::Null | Value::Bool(_) | Value::Number(_) => value.clone(),
}
}
fn compact_prompt_cache_json_anchor(value: &Value) -> Value {
match value {
Value::String(text) => compact_prompt_cache_text(text).unwrap_or(Value::Null),
Value::Array(items) => Value::Array(
items
.iter()
.take(16)
.map(compact_prompt_cache_json_anchor)
.filter(|value| !value.is_null())
.collect(),
),
Value::Object(object) => {
let mut compacted = serde_json::Map::new();
let mut keys = object.keys().collect::<Vec<_>>();
keys.sort();
for key in keys {
if key == "cache_control" {
continue;
}
let Some(value) = object.get(key) else {
continue;
};
let value = compact_prompt_cache_json_anchor(value);
if !value.is_null() {
compacted.insert(key.clone(), value);
}
}
Value::Object(compacted)
}
Value::Null | Value::Bool(_) | Value::Number(_) => value.clone(),
}
}
fn collect_codex_prompt_cache_control_anchors(value: &Value, anchors: &mut Vec<Value>) {
const MAX_PROMPT_CACHE_CONTROL_ANCHORS: usize = 16;
if anchors.len() >= MAX_PROMPT_CACHE_CONTROL_ANCHORS {
return;
}
match value {
Value::Object(object) => {
if object.contains_key("cache_control") {
let mut anchor = object.clone();
anchor.remove("cache_control");
let anchor = compact_prompt_cache_anchor(&Value::Object(anchor));
if !anchor.is_null() {
anchors.push(anchor);
}
}
for child in object.values() {
if anchors.len() >= MAX_PROMPT_CACHE_CONTROL_ANCHORS {
break;
}
collect_codex_prompt_cache_control_anchors(child, anchors);
}
}
Value::Array(items) => {
for child in items {
if anchors.len() >= MAX_PROMPT_CACHE_CONTROL_ANCHORS {
break;
}
collect_codex_prompt_cache_control_anchors(child, anchors);
}
}
_ => {}
}
}
fn extract_codex_prompt_cache_control_seed(provider_request_body: &Value) -> Option<String> {
let mut anchors = Vec::new();
collect_codex_prompt_cache_control_anchors(provider_request_body, &mut anchors);
if anchors.is_empty() {
return None;
}
let seed = json!({
"model": provider_request_body.get("model"),
"anchors": anchors,
});
stable_json_digest(&seed).map(|digest| format!("cache_control:{digest}"))
}
fn first_responses_input_anchor(input: &Value) -> Option<Value> {
let items = input.as_array()?;
let first_user_message = items.iter().find(|item| {
item.get("type")
.and_then(Value::as_str)
.is_some_and(|value| value == "message")
&& item
.get("role")
.and_then(Value::as_str)
.is_some_and(|value| value == "user")
});
let first_item = first_user_message.or_else(|| items.first())?;
let anchor = compact_prompt_cache_anchor(first_item);
(!anchor.is_null()).then_some(anchor)
}
fn extract_codex_stable_request_prompt_cache_seed(
provider_request_body: &Value,
user_api_key_id: Option<&str>,
) -> Option<String> {
let object = provider_request_body.as_object()?;
let mut seed = serde_json::Map::new();
for key in [
"model",
"instructions",
"reasoning",
"tools",
"tool_choice",
"parallel_tool_calls",
] {
if let Some(value) = object.get(key).filter(|value| !value.is_null()) {
let value = if key == "tools" {
compact_prompt_cache_json_anchor(value)
} else {
compact_prompt_cache_anchor(value)
};
seed.insert(key.to_string(), value);
}
}
if let Some(input_anchor) = object.get("input").and_then(first_responses_input_anchor) {
seed.insert("first_input".to_string(), input_anchor);
}
if let Some(user_api_key_id) = user_api_key_id
.map(str::trim)
.filter(|value| !value.is_empty())
{
seed.insert(
"api_key_id".to_string(),
Value::String(user_api_key_id.to_string()),
);
}
if seed.len() < 2 {
return None;
}
stable_json_digest(&Value::Object(seed)).map(|digest| format!("stable_request:{digest}"))
}
fn build_short_codex_header_id(seed: &str) -> Option<String> {
let normalized = seed.trim();
if normalized.is_empty() {
@@ -261,31 +532,47 @@ fn maybe_insert_default_codex_header(
provider_request_headers.insert(header_name.to_string(), header_value.to_string());
}
fn maybe_inject_codex_prompt_cache_key(
provider_request_body: &mut Value,
fn codex_prompt_cache_key_to_insert(
provider_request_body: &Value,
provider_type: &str,
provider_api_format: &str,
user_api_key_id: Option<&str>,
) {
) -> Option<String> {
if !is_codex_openai_responses_request(provider_type, provider_api_format) {
return;
return None;
}
let Some(body_object) = provider_request_body.as_object_mut() else {
return;
};
let existing = body_object
let existing = provider_request_body
.get("prompt_cache_key")
.and_then(Value::as_str)
.map(str::trim)
.unwrap_or_default();
if !existing.is_empty() {
return;
return None;
}
let Some(prompt_cache_key) = user_api_key_id.and_then(build_stable_codex_prompt_cache_key)
else {
extract_codex_prompt_cache_session_seed(provider_request_body)
.and_then(|seed| build_stable_codex_prompt_cache_key_from_seed("session", &seed))
.or_else(|| {
extract_codex_prompt_cache_control_seed(provider_request_body)
.and_then(|seed| build_stable_codex_prompt_cache_key_from_seed("anchor", &seed))
})
.or_else(|| {
extract_codex_stable_request_prompt_cache_seed(provider_request_body, user_api_key_id)
.and_then(|seed| build_stable_codex_prompt_cache_key_from_seed("request", &seed))
})
.or_else(|| user_api_key_id.and_then(build_stable_codex_prompt_cache_key))
}
fn insert_codex_prompt_cache_key(
provider_request_body: &mut Value,
prompt_cache_key: Option<String>,
) {
let Some(prompt_cache_key) = prompt_cache_key else {
return;
};
let Some(body_object) = provider_request_body.as_object_mut() else {
return;
};
@@ -431,6 +718,13 @@ pub fn apply_codex_openai_responses_special_body_edits(
return;
}
let prompt_cache_key = codex_prompt_cache_key_to_insert(
provider_request_body,
provider_type,
provider_api_format,
user_api_key_id,
);
let Some(body_object) = provider_request_body.as_object_mut() else {
return;
};
@@ -479,12 +773,7 @@ pub fn apply_codex_openai_responses_special_body_edits(
inject_codex_default_variation_prompt(body_object);
}
maybe_inject_codex_prompt_cache_key(
provider_request_body,
provider_type,
provider_api_format,
user_api_key_id,
);
insert_codex_prompt_cache_key(provider_request_body, prompt_cache_key);
}
pub fn apply_codex_openai_responses_chat_body_edits(
@@ -509,6 +798,9 @@ pub fn apply_codex_openai_responses_chat_body_edits(
return;
};
ensure_codex_chat_reasoning_defaults(body_object, provider_api_format, body_rules);
if let Some(prompt_cache_key) = body_object.remove("prompt_cache_key") {
body_object.insert("prompt_cache_key".to_string(), prompt_cache_key);
}
}
pub fn apply_codex_openai_responses_special_headers(
@@ -748,6 +1040,208 @@ mod tests {
);
}
#[test]
fn codex_responses_body_edits_derive_prompt_cache_key_from_session_metadata() {
let mut body_a = json!({
"input": [{"role": "user", "content": "hello"}],
"model": "gpt-5.4",
"metadata": {
"user_id": "{\"session_id\":\"session-a\",\"device_id\":\"device-a\"}"
}
});
let mut body_b = json!({
"input": [{"role": "user", "content": "hello again"}],
"model": "gpt-5.4",
"metadata": {
"user_id": "{\"session_id\":\"session-a\",\"device_id\":\"device-b\"}"
}
});
let mut body_c = json!({
"input": [{"role": "user", "content": "hello"}],
"model": "gpt-5.4",
"metadata": {"session_id": "session-b"}
});
apply_codex_openai_responses_special_body_edits(
&mut body_a,
"codex",
"openai:responses",
None,
Some("key-123"),
);
apply_codex_openai_responses_special_body_edits(
&mut body_b,
"codex",
"openai:responses",
None,
Some("different-key"),
);
apply_codex_openai_responses_special_body_edits(
&mut body_c,
"codex",
"openai:responses",
None,
Some("key-123"),
);
assert_eq!(body_a["prompt_cache_key"], body_b["prompt_cache_key"]);
assert_ne!(body_a["prompt_cache_key"], body_c["prompt_cache_key"]);
assert!(body_a.get("metadata").is_none());
assert!(body_b.get("metadata").is_none());
assert!(body_c.get("metadata").is_none());
}
#[test]
fn codex_responses_body_edits_derive_prompt_cache_key_from_cache_control_anchor() {
let mut body_a = json!({
"input": [{
"type": "message",
"role": "user",
"content": [{
"type": "input_text",
"text": "stable project brief",
"cache_control": {"type": "ephemeral"}
}]
}, {
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "new turn A"}]
}],
"model": "gpt-5.4"
});
let mut body_b = json!({
"input": [{
"type": "message",
"role": "user",
"content": [{
"type": "input_text",
"text": "stable project brief",
"cache_control": {"type": "ephemeral"}
}]
}, {
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "new turn B"}]
}],
"model": "gpt-5.4"
});
let mut body_c = json!({
"input": [{
"type": "message",
"role": "user",
"content": [{
"type": "input_text",
"text": "different project brief",
"cache_control": {"type": "ephemeral"}
}]
}],
"model": "gpt-5.4"
});
apply_codex_openai_responses_special_body_edits(
&mut body_a,
"codex",
"openai:responses",
None,
Some("key-a"),
);
apply_codex_openai_responses_special_body_edits(
&mut body_b,
"codex",
"openai:responses",
None,
Some("key-b"),
);
apply_codex_openai_responses_special_body_edits(
&mut body_c,
"codex",
"openai:responses",
None,
Some("key-a"),
);
assert_eq!(body_a["prompt_cache_key"], body_b["prompt_cache_key"]);
assert_ne!(body_a["prompt_cache_key"], body_c["prompt_cache_key"]);
}
#[test]
fn codex_responses_body_edits_derive_prompt_cache_key_from_stable_request_anchor() {
let mut body_a = json!({
"input": [{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "open workspace"}]
}, {
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "new turn A"}]
}],
"model": "gpt-5.4",
"instructions": "Be concise.",
"tools": [{
"type": "function",
"name": "shell",
"parameters": {"type": "object", "properties": {}}
}],
"reasoning": {"effort": "medium"}
});
let mut body_b = json!({
"input": [{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "open workspace"}]
}, {
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "new turn B"}]
}],
"model": "gpt-5.4",
"instructions": "Be concise.",
"tools": [{
"type": "function",
"name": "shell",
"parameters": {"type": "object", "properties": {}}
}],
"reasoning": {"effort": "medium"}
});
let mut body_c = json!({
"input": [{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "open another workspace"}]
}],
"model": "gpt-5.4",
"instructions": "Be concise.",
"tools": [{"type": "function", "name": "shell"}],
"reasoning": {"effort": "medium"}
});
apply_codex_openai_responses_special_body_edits(
&mut body_a,
"codex",
"openai:responses",
None,
Some("key-a"),
);
apply_codex_openai_responses_special_body_edits(
&mut body_b,
"codex",
"openai:responses",
None,
Some("key-a"),
);
apply_codex_openai_responses_special_body_edits(
&mut body_c,
"codex",
"openai:responses",
None,
Some("key-a"),
);
assert_eq!(body_a["prompt_cache_key"], body_b["prompt_cache_key"]);
assert_ne!(body_a["prompt_cache_key"], body_c["prompt_cache_key"]);
}
#[test]
fn compact_body_edits_strip_include_store_and_stream() {
let mut provider_request_body = json!({
@@ -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")
);
}
}
@@ -2,6 +2,14 @@ use base64::Engine as _;
use crate::formats::id::api_format_uses_body_stream_field;
/// JSON key under which `upstream_is_stream` is written into the AI execution
/// report context and propagated into usage metadata. Shared by the producer
/// (`aether-ai-serving::report_context`) and every downstream consumer so that
/// renames cannot silently desync them — a string-literal mismatch here would
/// degrade to default values (e.g. assuming streaming) without any compile-time
/// signal.
pub const UPSTREAM_IS_STREAM_KEY: &str = "upstream_is_stream";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum UpstreamStreamPolicy {
Auto,
@@ -3,6 +3,7 @@ use std::collections::BTreeMap;
use serde_json::Value;
use crate::contracts::core_success_background_report_kind;
use crate::formats::shared::request::UPSTREAM_IS_STREAM_KEY;
#[derive(Debug, Clone, PartialEq)]
pub struct LocalSyncReportParts {
@@ -66,7 +67,7 @@ fn should_capture_client_sync_success_body(payload: &LocalSyncReportParts) -> bo
.report_context
.as_ref()
.and_then(Value::as_object)
.and_then(|context| context.get("upstream_is_stream"))
.and_then(|context| context.get(UPSTREAM_IS_STREAM_KEY))
.and_then(Value::as_bool)
.unwrap_or(false)
}
@@ -17,7 +17,10 @@ use crate::formats::openai::responses::codex::{
apply_codex_openai_responses_chat_body_edits, apply_codex_openai_responses_special_body_edits,
apply_openai_responses_compact_special_body_edits,
};
use crate::formats::shared::standard_normalize::build_local_openai_chat_request_body_with_model_directives;
use crate::formats::shared::standard_normalize::{
build_local_openai_chat_request_body_with_model_directives,
is_claude_messages_shaped_body_on_openai_chat_endpoint,
};
#[allow(clippy::too_many_arguments)]
pub fn build_standard_request_body(
@@ -91,9 +94,14 @@ pub fn build_standard_request_body_with_model_directives_and_request_headers(
.with_mapped_model(mapped_model)
.with_request_path(request_path)
.with_upstream_stream(upstream_is_stream);
let mut provider_request_body = convert_request(
let source_api_format = compatible_source_format_for_standard_request(
body_json,
client_api_format,
provider_api_format,
);
let mut provider_request_body = convert_request(
source_api_format.as_ref(),
provider_api_format,
body_json,
&format_context,
)
@@ -157,6 +165,24 @@ pub fn build_standard_request_body_with_model_directives_and_request_headers(
Some(provider_request_body)
}
fn compatible_source_format_for_standard_request<'a>(
body_json: &Value,
client_api_format: &'a str,
provider_api_format: &str,
) -> Cow<'a, str> {
if matches!(
aether_ai_formats::normalize_api_format_alias(client_api_format).as_str(),
"openai:chat"
) && matches!(
aether_ai_formats::normalize_api_format_alias(provider_api_format).as_str(),
"claude:messages"
) && is_claude_messages_shaped_body_on_openai_chat_endpoint(body_json)
{
return Cow::Borrowed("claude:messages");
}
Cow::Borrowed(client_api_format)
}
pub fn build_standard_request_body_from_canonical(
canonical_request: &Value,
mapped_model: &str,
@@ -444,6 +470,87 @@ mod tests {
}
}
#[test]
fn standard_openai_chat_to_claude_accepts_claude_native_body_from_chat_endpoint() {
let request = json!({
"model": "deepseek-v4-flash",
"messages": [
{"role": "user", "content": "lookup"},
{
"role": "assistant",
"content": [
{"type": "text", "text": "checking"},
{
"type": "tool_use",
"id": "call_1",
"name": "lookup",
"input": {"q": "db"}
}
]
},
{
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": "call_1",
"content": {"rows": 1},
"is_error": false
}]
}
],
"tools": [{
"name": "lookup",
"description": "Lookup data",
"input_schema": {"type": "object", "properties": {"q": {"type": "string"}}}
}],
"tool_choice": {"type": "auto"},
"max_tokens": 128,
"stream": true
});
let converted = build_standard_request_body(
&request,
"openai:chat",
"claude-sonnet-4-5",
"custom",
"claude:messages",
"/v1/chat/completions",
true,
None,
None,
)
.expect("claude-native chat endpoint body should build as claude messages");
assert_eq!(converted["model"], "claude-sonnet-4-5");
assert_eq!(converted["max_tokens"], 128);
assert_eq!(converted["tools"][0]["name"], "lookup");
assert_eq!(
converted["tools"][0]["input_schema"]["properties"]["q"]["type"],
"string"
);
assert_eq!(converted["messages"][1]["content"][1]["type"], "tool_use");
assert_eq!(converted["messages"][1]["content"][1]["id"], "call_1");
assert_eq!(
converted["messages"][2]["content"][0]["type"],
"tool_result"
);
assert_eq!(
converted["messages"][2]["content"][0]["tool_use_id"],
"call_1"
);
assert_eq!(
serde_json::from_str::<Value>(
converted["messages"][2]["content"][0]["content"]
.as_str()
.expect("object tool result content should be serialized for Claude")
)
.expect("serialized tool result content should remain JSON"),
json!({"rows": 1})
);
assert_eq!(converted["tool_choice"]["type"], "auto");
assert_eq!(converted["stream"], true);
}
fn codex_default_body_rules() -> Value {
json!([
{"action":"drop","path":"max_output_tokens"},
@@ -1,13 +1,67 @@
use std::borrow::Cow;
use aether_ai_formats::formats::conversion::request::{
convert_openai_chat_request_to_claude_request, convert_openai_chat_request_to_gemini_request,
convert_openai_chat_request_to_openai_responses_request,
normalize_openai_responses_request_to_openai_chat_request,
};
use aether_ai_formats::{request_conversion_kind, RequestConversionKind};
use aether_ai_formats::{request_conversion_kind, FormatContext, RequestConversionKind};
use serde_json::{json, Value};
use crate::formats::shared::model_directives::apply_model_directive_overrides_from_request;
fn is_responses_shaped_body_on_chat_endpoint(body_json: &Value) -> bool {
body_json
.as_object()
.is_some_and(|object| !object.contains_key("messages") && object.contains_key("input"))
}
pub fn is_claude_messages_shaped_body_on_openai_chat_endpoint(body_json: &Value) -> bool {
let Some(request) = body_json.as_object() else {
return false;
};
if !request.contains_key("messages") {
return false;
}
request
.get("tools")
.and_then(Value::as_array)
.is_some_and(|tools| tools.iter().any(is_claude_native_tool_definition))
|| request
.get("messages")
.and_then(Value::as_array)
.is_some_and(|messages| messages.iter().any(message_has_claude_tool_block))
}
fn is_claude_native_tool_definition(tool: &Value) -> bool {
tool.as_object().is_some_and(|tool_object| {
tool_object.contains_key("input_schema") && !tool_object.contains_key("function")
})
}
fn message_has_claude_tool_block(message: &Value) -> bool {
message
.as_object()
.and_then(|object| object.get("content"))
.and_then(Value::as_array)
.is_some_and(|parts| parts.iter().any(is_claude_tool_content_block))
}
fn is_claude_tool_content_block(part: &Value) -> bool {
part.as_object()
.and_then(|object| object.get("type"))
.and_then(Value::as_str)
.is_some_and(|block_type| matches!(block_type, "tool_use" | "tool_result"))
}
fn chat_compatible_body_for_openai_chat_endpoint(body_json: &Value) -> Option<Cow<'_, Value>> {
if is_responses_shaped_body_on_chat_endpoint(body_json) {
return normalize_openai_responses_request_to_openai_chat_request(body_json)
.map(Cow::Owned);
}
Some(Cow::Borrowed(body_json))
}
pub fn build_local_openai_chat_request_body(
body_json: &Value,
mapped_model: &str,
@@ -27,7 +81,8 @@ pub fn build_local_openai_chat_request_body_with_model_directives(
upstream_is_stream: bool,
enable_model_directives: bool,
) -> Option<Value> {
let request_body_object = body_json.as_object()?;
let chat_body = chat_compatible_body_for_openai_chat_endpoint(body_json)?;
let request_body_object = chat_body.as_object()?;
let mut provider_request_body = serde_json::Map::from_iter(
request_body_object
.iter()
@@ -94,24 +149,48 @@ pub fn build_cross_format_openai_chat_request_body_with_model_directives(
) -> Option<Value> {
let conversion_kind = request_conversion_kind("openai:chat", provider_api_format)?;
let provider_request_body = match conversion_kind {
RequestConversionKind::ToClaudeStandard => convert_openai_chat_request_to_claude_request(
body_json,
mapped_model,
upstream_is_stream,
)?,
RequestConversionKind::ToGeminiStandard => convert_openai_chat_request_to_gemini_request(
body_json,
mapped_model,
upstream_is_stream,
)?,
RequestConversionKind::ToOpenAiResponses => {
convert_openai_chat_request_to_openai_responses_request(
body_json,
RequestConversionKind::ToClaudeStandard => {
if is_claude_messages_shaped_body_on_openai_chat_endpoint(body_json) {
convert_claude_compatible_chat_endpoint_request(
body_json,
mapped_model,
provider_api_format,
upstream_is_stream,
)?
} else {
let chat_body = chat_compatible_body_for_openai_chat_endpoint(body_json)?;
convert_openai_chat_request_to_claude_request(
chat_body.as_ref(),
mapped_model,
upstream_is_stream,
)?
}
}
RequestConversionKind::ToGeminiStandard => {
let chat_body = chat_compatible_body_for_openai_chat_endpoint(body_json)?;
convert_openai_chat_request_to_gemini_request(
chat_body.as_ref(),
mapped_model,
upstream_is_stream,
false,
)?
}
RequestConversionKind::ToOpenAiResponses => {
if is_responses_shaped_body_on_chat_endpoint(body_json) {
build_local_openai_responses_request_body_with_model_directives(
body_json,
mapped_model,
upstream_is_stream,
enable_model_directives,
)?
} else {
convert_openai_chat_request_to_openai_responses_request(
body_json,
mapped_model,
upstream_is_stream,
false,
)?
}
}
_ => return None,
};
let mut provider_request_body = with_model_directive_overrides(
@@ -134,6 +213,23 @@ pub fn build_cross_format_openai_chat_request_body_with_model_directives(
Some(provider_request_body)
}
fn convert_claude_compatible_chat_endpoint_request(
body_json: &Value,
mapped_model: &str,
provider_api_format: &str,
upstream_is_stream: bool,
) -> Option<Value> {
aether_ai_formats::convert_request(
"claude:messages",
provider_api_format,
body_json,
&FormatContext::default()
.with_mapped_model(mapped_model)
.with_upstream_stream(upstream_is_stream),
)
.ok()
}
pub fn build_local_openai_responses_request_body(
body_json: &Value,
mapped_model: &str,
@@ -342,6 +438,111 @@ mod tests {
assert_eq!(provider_request_body["model"], "gpt-5-upstream");
}
#[test]
fn local_openai_chat_request_body_accepts_responses_shape_from_chat_endpoint() {
let body_json = json!({
"model": "gpt-5",
"stream": true,
"input": [{"role": "user", "content": "hello"}],
"tools": [{
"type": "function",
"name": "Shell",
"parameters": {"type": "object"},
"strict": false
}],
"reasoning": {"effort": "high"}
});
let provider_request_body =
build_local_openai_chat_request_body(&body_json, "gpt-5-upstream", true)
.expect("responses-shaped chat body should build as chat");
assert_eq!(provider_request_body["model"], "gpt-5-upstream");
assert_eq!(provider_request_body["messages"][0]["role"], "user");
assert_eq!(provider_request_body["messages"][0]["content"], "hello");
assert_eq!(
provider_request_body["tools"][0]["function"]["name"],
"Shell"
);
assert_eq!(provider_request_body["reasoning_effort"], "high");
assert_eq!(provider_request_body["stream"], true);
assert_eq!(
provider_request_body["stream_options"]["include_usage"],
true
);
}
#[test]
fn cross_format_openai_chat_request_body_preserves_responses_shape_for_responses_target() {
let body_json = json!({
"model": "gpt-5",
"stream": true,
"input": [{"role": "user", "content": "hello"}],
"include": ["reasoning.encrypted_content"],
"stream_options": {"include_usage": true},
"tools": [{
"type": "function",
"name": "Shell",
"parameters": {"type": "object"},
"strict": false
}, {
"type": "function",
"parameters": {"type": "object"}
}]
});
let provider_request_body =
build_cross_format_openai_chat_request_body_with_model_directives(
&body_json,
"gpt-5-upstream",
"openai:responses",
false,
false,
)
.expect("responses-shaped chat body should build as responses");
assert_eq!(provider_request_body["model"], "gpt-5-upstream");
assert_eq!(provider_request_body["input"][0]["role"], "user");
assert_eq!(provider_request_body["input"][0]["content"], "hello");
assert_eq!(provider_request_body["tools"][0]["name"], "Shell");
assert_eq!(provider_request_body["tools"][0]["strict"], false);
assert_eq!(provider_request_body["tools"][1]["type"], "function");
assert_eq!(
provider_request_body["include"][0],
"reasoning.encrypted_content"
);
assert_eq!(
provider_request_body["stream_options"]["include_usage"],
true
);
assert_eq!(provider_request_body["stream"], false);
assert!(provider_request_body.get("messages").is_none());
}
#[test]
fn openai_chat_request_body_prefers_messages_when_messages_and_input_are_both_present() {
let body_json = json!({
"model": "gpt-5",
"messages": [{"role": "user", "content": "from messages"}],
"input": [{"role": "user", "content": "from input"}]
});
let provider_request_body =
build_cross_format_openai_chat_request_body_with_model_directives(
&body_json,
"gpt-5-upstream",
"openai:responses",
false,
false,
)
.expect("normal chat body should still use messages");
assert_eq!(
provider_request_body["input"][0]["content"][0]["text"],
"from messages"
);
}
#[test]
fn builds_streaming_local_openai_chat_request_body_with_include_usage() {
let body_json = json!({
@@ -572,4 +773,78 @@ mod tests {
);
assert_eq!(provider_request_body["stream_options"]["extra"], "keep-me");
}
#[test]
fn cross_format_openai_chat_request_body_accepts_claude_native_messages_for_claude_target() {
let body_json = json!({
"model": "deepseek-v4-flash",
"messages": [
{"role": "user", "content": "lookup"},
{
"role": "assistant",
"content": [
{"type": "text", "text": "checking"},
{
"type": "tool_use",
"id": "call_1",
"name": "lookup",
"input": {"q": "db"}
}
]
},
{
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": "call_1",
"content": {"rows": 1}
}]
}
],
"tools": [{
"name": "lookup",
"description": "Lookup data",
"input_schema": {"type": "object", "properties": {"q": {"type": "string"}}}
}],
"tool_choice": {"type": "auto"},
"max_tokens": 128,
"stream": true
});
let provider_request_body =
build_cross_format_openai_chat_request_body_with_model_directives(
&body_json,
"claude-sonnet-4-5",
"claude:messages",
true,
false,
)
.expect("claude-native chat endpoint body should build as claude messages");
assert_eq!(provider_request_body["model"], "claude-sonnet-4-5");
assert_eq!(provider_request_body["tools"][0]["name"], "lookup");
assert_eq!(
provider_request_body["tools"][0]["input_schema"]["properties"]["q"]["type"],
"string"
);
assert_eq!(
provider_request_body["messages"][1]["content"][1]["type"],
"tool_use"
);
assert_eq!(
provider_request_body["messages"][2]["content"][0]["type"],
"tool_result"
);
assert_eq!(
serde_json::from_str::<Value>(
provider_request_body["messages"][2]["content"][0]["content"]
.as_str()
.expect("object tool result content should be serialized for Claude")
)
.expect("serialized tool result content should remain JSON"),
json!({"rows": 1})
);
assert_eq!(provider_request_body["tool_choice"]["type"], "auto");
assert_eq!(provider_request_body["stream"], true);
}
}
@@ -108,6 +108,107 @@ pub fn canonical_usage_from_openai_usage(value: Option<&Value>) -> Option<Canoni
})
}
pub fn openai_stream_payload_is_terminal_error(payload: &Value) -> bool {
let event_type = payload
.get("type")
.and_then(Value::as_str)
.unwrap_or_default();
if payload.get("error").is_some() {
return true;
}
if matches!(
event_type,
"error" | "response.failed" | "response.incomplete"
) {
return true;
}
payload
.get("response")
.and_then(Value::as_object)
.and_then(|response| response.get("status"))
.and_then(Value::as_str)
.is_some_and(|status| matches!(status, "failed" | "incomplete"))
}
pub fn openai_stream_terminal_error_body(payload: &Value) -> Option<Value> {
if !openai_stream_payload_is_terminal_error(payload) {
return None;
}
let event_type = payload
.get("type")
.and_then(Value::as_str)
.unwrap_or_default();
let response = payload.get("response").and_then(Value::as_object);
let status = response
.and_then(|response| response.get("status"))
.and_then(Value::as_str);
let raw_error = response
.and_then(|response| response.get("error"))
.or_else(|| payload.get("error"));
let mut error = raw_error
.and_then(Value::as_object)
.cloned()
.unwrap_or_default();
let message = error
.get("message")
.and_then(Value::as_str)
.map(ToOwned::to_owned)
.or_else(|| raw_error.and_then(Value::as_str).map(ToOwned::to_owned))
.or_else(|| {
payload
.get("message")
.and_then(Value::as_str)
.map(ToOwned::to_owned)
})
.or_else(|| {
response
.and_then(|response| response.get("incomplete_details"))
.and_then(|details| details.get("reason"))
.and_then(Value::as_str)
.map(|reason| format!("Response incomplete: {reason}"))
})
.or_else(|| status.map(|status| format!("Response ended with status {status}")))
.unwrap_or_else(|| "Upstream stream ended with an error".to_string());
error
.entry("message".to_string())
.or_insert_with(|| Value::String(message));
error.entry("type".to_string()).or_insert_with(|| {
if event_type == "response.incomplete" || status == Some("incomplete") {
Value::String("incomplete".to_string())
} else {
Value::String("server_error".to_string())
}
});
if !error.contains_key("code") {
if let Some(reason) = response
.and_then(|response| response.get("incomplete_details"))
.and_then(|details| details.get("reason"))
.and_then(Value::as_str)
{
error.insert("code".to_string(), Value::String(reason.to_string()));
}
}
Some(json!({ "error": Value::Object(error) }))
}
pub fn openai_stream_terminal_error_message(payload: &Value) -> Option<String> {
openai_stream_terminal_error_body(payload)
.and_then(|body| body.get("error").cloned())
.and_then(|error| {
error
.get("message")
.and_then(Value::as_str)
.map(ToOwned::to_owned)
})
}
pub fn canonical_usage_from_claude_usage(value: Option<&Value>) -> Option<CanonicalUsage> {
let usage = value?.as_object()?;
let input_tokens = usage
@@ -14,7 +14,8 @@ use crate::formats::shared::error_body::{
};
use crate::formats::shared::sse::encode_json_sse;
use crate::formats::shared::stream_core::common::{
decode_json_data_line, CanonicalStreamEvent, CanonicalStreamFrame, CanonicalUsage,
decode_json_data_line, openai_stream_terminal_error_body, openai_stream_terminal_error_message,
CanonicalStreamEvent, CanonicalStreamFrame, CanonicalUsage,
};
use crate::formats::shared::AiSurfaceFinalizeError;
@@ -197,6 +198,14 @@ impl StreamingStandardTerminalObserver {
summary.model = Some(model);
}
match event {
CanonicalStreamEvent::UnknownEvent(payload)
if openai_stream_terminal_error_body(&payload).is_some() =>
{
summary.unknown_event_count = summary.unknown_event_count.saturating_add(1);
summary.observed_finish = true;
summary.finish_reason = Some("error".to_string());
summary.parser_error = openai_stream_terminal_error_message(&payload);
}
CanonicalStreamEvent::UnknownEvent(_) => {
summary.unknown_event_count = summary.unknown_event_count.saturating_add(1);
}
@@ -410,7 +419,8 @@ fn parse_provider_error(
}
fn parse_openai_error(payload: &Value) -> Option<(String, Option<String>, LocalCoreSyncErrorKind)> {
let error = payload.get("error")?.as_object()?;
let error_body = openai_stream_terminal_error_body(payload)?;
let error = error_body.get("error")?.as_object()?;
let message = error.get("message").and_then(Value::as_str)?.to_string();
let code = error
.get("code")
@@ -973,6 +983,41 @@ mod tests {
assert!(!summary.observed_finish);
}
#[test]
fn terminal_observer_marks_openai_responses_failed_event_as_terminal_error() {
let mut report_context = report_context("openai:chat", "openai:responses");
report_context["provider_stream_event_api_format"] = json!("openai:responses");
let mut observer = StreamingStandardTerminalObserver::default();
observer
.push_line(
&report_context,
data_line(json!({
"type": "response.failed",
"response": {
"id": "resp_failed_123",
"model": "gpt-5.4",
"status": "failed",
"error": {
"message": "policy failure",
"type": "invalid_request_error",
"code": "cyber_policy"
}
}
})),
)
.expect("failed event should be observed");
let summary = observer
.latest_summary()
.cloned()
.expect("summary should exist");
assert!(summary.observed_finish);
assert_eq!(summary.finish_reason.as_deref(), Some("error"));
assert_eq!(summary.parser_error.as_deref(), Some("policy failure"));
assert_eq!(summary.unknown_event_count, 1);
}
#[test]
fn terminal_observer_tracks_openai_image_stream_usage() {
let mut report_context = report_context("openai:image", "openai:chat");
+1 -1
View File
@@ -29,7 +29,7 @@ pub use formats::shared::model_directives::{
};
pub use formats::shared::request::{
endpoint_config_forces_upstream_stream_policy, enforce_request_body_stream_field,
resolve_upstream_is_stream_from_endpoint_config,
resolve_upstream_is_stream_from_endpoint_config, UPSTREAM_IS_STREAM_KEY,
};
pub use protocol::canonical::{
canonical_request_unknown_block_count, canonical_response_unknown_block_count,
@@ -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")
@@ -3763,9 +3820,11 @@ pub(crate) fn canonical_block_to_claude(
);
out.insert(
"content".to_string(),
output
.clone()
.unwrap_or_else(|| Value::String(content_text.clone().unwrap_or_default())),
canonical_tool_result_content_to_claude(
output.as_ref(),
content_text.as_deref(),
role,
),
);
out.insert("is_error".to_string(), Value::Bool(*is_error));
out.extend(namespace_extension_object(extensions, "claude", &out));
@@ -3775,6 +3834,41 @@ pub(crate) fn canonical_block_to_claude(
}
}
fn canonical_tool_result_content_to_claude(
output: Option<&Value>,
content_text: Option<&str>,
role: &CanonicalRole,
) -> Value {
if matches!(role, CanonicalRole::Assistant) {
return output
.cloned()
.unwrap_or_else(|| Value::String(content_text.unwrap_or_default().to_string()));
}
match output {
Some(Value::String(text)) => Value::String(text.clone()),
Some(Value::Array(parts)) if claude_tool_result_content_blocks_are_wire_safe(parts) => {
Value::Array(parts.clone())
}
Some(value) => serde_json::to_string(value)
.map(Value::String)
.unwrap_or_else(|_| Value::String(content_text.unwrap_or_default().to_string())),
None => Value::String(content_text.unwrap_or_default().to_string()),
}
}
fn claude_tool_result_content_blocks_are_wire_safe(parts: &[Value]) -> bool {
!parts.is_empty()
&& parts.iter().all(|part| {
part.as_object()
.and_then(|object| object.get("type"))
.and_then(Value::as_str)
.is_some_and(|block_type| {
matches!(block_type, "text" | "image" | "document" | "file")
})
})
}
pub(crate) fn claude_source_value(
media_type: Option<&str>,
data: Option<&str>,
@@ -8,7 +8,7 @@ use crate::provider_compat::kiro_stream::KiroToClaudeCliStreamState;
use super::surfaces::{
provider_adaptation_allows_sync_finalize_envelope, provider_adaptation_descriptor_for_envelope,
provider_adaptation_should_unwrap_stream_envelope, ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME,
GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME, KIRO_ENVELOPE_NAME,
GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME, KIRO_ENVELOPE_NAME, WINDSURF_ENVELOPE_NAME,
};
pub fn provider_private_response_allows_sync_finalize(report_context: &Value) -> bool {
@@ -92,6 +92,7 @@ pub fn normalize_provider_private_response_value(
data
}
}
Some(WINDSURF_ENVELOPE_NAME) => normalize_windsurf_sync_response_value(data)?,
_ => return None,
};
postprocess_private_response_value(&mut unwrapped, report_context);
@@ -101,14 +102,28 @@ pub fn normalize_provider_private_response_value(
pub fn transform_provider_private_stream_line(
report_context: &Value,
line: Vec<u8>,
) -> Result<Vec<u8>, serde_json::Error> {
transform_provider_private_stream_line_with_event_state(report_context, line, &mut None)
}
fn transform_provider_private_stream_line_with_event_state(
report_context: &Value,
line: Vec<u8>,
current_event_type: &mut Option<String>,
) -> Result<Vec<u8>, serde_json::Error> {
let Ok(text) = std::str::from_utf8(&line) else {
return Ok(line);
};
let trimmed = text.trim_matches('\r').trim();
if trimmed.is_empty() || trimmed.starts_with(':') || trimmed.starts_with("event:") {
if trimmed.is_empty() || trimmed.starts_with(':') {
return Ok(Vec::new());
}
if let Some(event_name) = trimmed.strip_prefix("event:") {
let event_name = event_name.trim().to_string();
let is_error = event_name.eq_ignore_ascii_case("error");
*current_event_type = (!event_name.is_empty()).then_some(event_name);
return if is_error { Ok(line) } else { Ok(Vec::new()) };
}
let Some(data_line) = trimmed.strip_prefix("data:") else {
return Ok(line);
};
@@ -121,6 +136,13 @@ pub fn transform_provider_private_stream_line(
Ok(value) => value,
Err(_) => return Ok(line),
};
let event_is_error = current_event_type
.as_deref()
.is_some_and(|event| event.eq_ignore_ascii_case("error"));
*current_event_type = None;
if event_is_error {
return Ok(line);
}
let envelope_name = report_context
.get("envelope_name")
@@ -133,6 +155,9 @@ pub fn transform_provider_private_stream_line(
if !provider_adaptation_should_unwrap_stream_envelope(envelope_name, provider_api_format) {
return Ok(line);
}
if envelope_name == WINDSURF_ENVELOPE_NAME && looks_like_windsurf_error(&body) {
return Ok(line);
}
let unwrapped = match envelope_name {
GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME => body.get("response").cloned().unwrap_or(body),
ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME => {
@@ -147,6 +172,7 @@ pub fn transform_provider_private_stream_line(
inject_antigravity_stream_tool_ids(&mut response);
response
}
WINDSURF_ENVELOPE_NAME => normalize_windsurf_stream_event_value(&body).unwrap_or(body),
_ => body,
};
@@ -156,6 +182,96 @@ pub fn transform_provider_private_stream_line(
Ok(out)
}
const CONNECT_FRAME_HEADER_BYTES: usize = 5;
const MAX_CONNECT_JSON_FRAME_BYTES: usize = 16 * 1024 * 1024;
fn report_context_is_windsurf_envelope(report_context: &Value) -> bool {
report_context
.get("envelope_name")
.and_then(Value::as_str)
.is_some_and(|value| value.eq_ignore_ascii_case(WINDSURF_ENVELOPE_NAME))
}
fn buffer_looks_like_connect_frame(buffer: &[u8]) -> bool {
let Some(flags) = buffer.first().copied() else {
return false;
};
if flags & !0x03 != 0 {
return false;
}
if buffer.len() < CONNECT_FRAME_HEADER_BYTES {
return true;
}
let len = u32::from_be_bytes([buffer[1], buffer[2], buffer[3], buffer[4]]) as usize;
len <= MAX_CONNECT_JSON_FRAME_BYTES
}
fn drain_windsurf_connect_json_frames(
buffer: &mut Vec<u8>,
) -> Result<Vec<u8>, AiSurfaceFinalizeError> {
let mut output = Vec::new();
while buffer.len() >= CONNECT_FRAME_HEADER_BYTES {
let flags = buffer[0];
if flags & !0x03 != 0 {
return Err(AiSurfaceFinalizeError::new(format!(
"invalid Connect frame flags: {flags}"
)));
}
let len = u32::from_be_bytes([buffer[1], buffer[2], buffer[3], buffer[4]]) as usize;
if len > MAX_CONNECT_JSON_FRAME_BYTES {
return Err(AiSurfaceFinalizeError::new(format!(
"Connect frame size {len} exceeds {MAX_CONNECT_JSON_FRAME_BYTES}"
)));
}
if buffer.len() < CONNECT_FRAME_HEADER_BYTES + len {
break;
}
let payload = buffer[CONNECT_FRAME_HEADER_BYTES..CONNECT_FRAME_HEADER_BYTES + len].to_vec();
buffer.drain(..CONNECT_FRAME_HEADER_BYTES + len);
if flags & 0x01 != 0 {
return Err(AiSurfaceFinalizeError::new(
"compressed Connect JSON frames are not supported for Windsurf chat",
));
}
if payload.is_empty() {
continue;
}
let body: Value = serde_json::from_slice(&payload)?;
if flags & 0x02 != 0 {
if let Some(error) = body.get("error") {
output.extend_from_slice(b"event: error\n");
output.extend_from_slice(b"data: ");
output.extend(serde_json::to_vec(error)?);
output.extend_from_slice(b"\n\n");
}
continue;
}
if looks_like_windsurf_error(&body) {
output.extend_from_slice(b"event: error\n");
output.extend_from_slice(b"data: ");
output.extend(serde_json::to_vec(&body)?);
output.extend_from_slice(b"\n\n");
continue;
}
let unwrapped = normalize_windsurf_stream_event_value(&body).unwrap_or(body);
let mut line = b"data: ".to_vec();
line.extend(serde_json::to_vec(&unwrapped)?);
line.extend_from_slice(b"\n\n");
output.extend(line);
}
if !buffer.is_empty()
&& buffer.len() < CONNECT_FRAME_HEADER_BYTES
&& !buffer_looks_like_connect_frame(buffer)
{
return Err(AiSurfaceFinalizeError::new(
"invalid partial Connect JSON frame",
));
}
Ok(output)
}
enum ProviderPrivateStreamNormalizeMode {
EnvelopeUnwrap,
KiroToClaudeCli(Box<KiroToClaudeCliStreamState>),
@@ -164,6 +280,7 @@ enum ProviderPrivateStreamNormalizeMode {
pub struct ProviderPrivateStreamNormalizer<'a> {
report_context: &'a Value,
buffered: Vec<u8>,
current_event_type: Option<String>,
mode: ProviderPrivateStreamNormalizeMode,
}
@@ -203,10 +320,24 @@ pub fn maybe_build_provider_private_stream_normalizer<'a>(
Some(ProviderPrivateStreamNormalizer {
report_context,
buffered: Vec::new(),
current_event_type: None,
mode,
})
}
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 {
@@ -215,12 +346,21 @@ impl ProviderPrivateStreamNormalizer<'_> {
}
ProviderPrivateStreamNormalizeMode::EnvelopeUnwrap => {
self.buffered.extend_from_slice(chunk);
if report_context_is_windsurf_envelope(self.report_context)
&& buffer_looks_like_connect_frame(&self.buffered)
{
return drain_windsurf_connect_json_frames(&mut self.buffered);
}
let mut output = Vec::new();
while let Some(line_end) = self.buffered.iter().position(|byte| *byte == b'\n') {
let line = self.buffered.drain(..=line_end).collect::<Vec<_>>();
output.extend(
transform_provider_private_stream_line(self.report_context, line)
.map_err(AiSurfaceFinalizeError::from)?,
transform_provider_private_stream_line_with_event_state(
self.report_context,
line,
&mut self.current_event_type,
)
.map_err(AiSurfaceFinalizeError::from)?,
);
}
Ok(output)
@@ -237,18 +377,235 @@ impl ProviderPrivateStreamNormalizer<'_> {
if self.buffered.is_empty() {
return Ok(Vec::new());
}
if report_context_is_windsurf_envelope(self.report_context)
&& buffer_looks_like_connect_frame(&self.buffered)
{
return drain_windsurf_connect_json_frames(&mut self.buffered);
}
let line = std::mem::take(&mut self.buffered);
transform_provider_private_stream_line(self.report_context, line)
.map_err(AiSurfaceFinalizeError::from)
transform_provider_private_stream_line_with_event_state(
self.report_context,
line,
&mut self.current_event_type,
)
.map_err(AiSurfaceFinalizeError::from)
}
}
}
}
pub fn stream_body_contains_error_event(body: &[u8]) -> bool {
let Ok(text) = std::str::from_utf8(body) else {
fn normalize_windsurf_sync_response_value(data: Value) -> Option<Value> {
if looks_like_openai_chat_response(&data) {
return Some(data);
}
if looks_like_windsurf_error(&data) {
return None;
}
if let Some(response) = data
.get("response")
.or_else(|| data.get("message"))
.or_else(|| data.get("chatMessage"))
.cloned()
{
if looks_like_openai_chat_response(&response) {
return Some(response);
}
if let Some(text) = extract_windsurf_text(&response) {
return Some(build_openai_chat_response_from_text(&data, text));
}
}
extract_windsurf_text(&data).map(|text| build_openai_chat_response_from_text(&data, text))
}
fn normalize_windsurf_stream_event_value(data: &Value) -> Option<Value> {
if looks_like_openai_chat_stream_event(data) {
return Some(data.clone());
}
if looks_like_windsurf_error(data) {
return None;
}
let response = data
.get("response")
.or_else(|| data.get("message"))
.or_else(|| data.get("chatMessage"))
.unwrap_or(data);
if looks_like_openai_chat_stream_event(response) {
return Some(response.clone());
}
extract_windsurf_text(response).map(|text| {
serde_json::json!({
"id": windsurf_response_id(data),
"object": "chat.completion.chunk",
"choices": [{
"index": 0,
"delta": {"content": text},
"finish_reason": null
}]
})
})
}
fn looks_like_openai_chat_response(value: &Value) -> bool {
value
.get("choices")
.and_then(Value::as_array)
.is_some_and(|choices| !choices.is_empty())
}
fn looks_like_openai_chat_stream_event(value: &Value) -> bool {
value
.get("choices")
.and_then(Value::as_array)
.and_then(|choices| choices.first())
.and_then(Value::as_object)
.is_some_and(|choice| choice.contains_key("delta"))
}
fn looks_like_windsurf_error(value: &Value) -> bool {
let Some(object) = value.as_object() else {
return false;
};
if object.contains_key("error") {
return true;
}
if object
.get("type")
.and_then(Value::as_str)
.is_some_and(|value| value.eq_ignore_ascii_case("error"))
{
return true;
}
if object.contains_key("code") || object.contains_key("status") {
return object
.get("message")
.and_then(Value::as_str)
.is_some_and(|value| !value.trim().is_empty());
}
object
.get("message")
.and_then(Value::as_str)
.is_some_and(|value| !value.trim().is_empty())
&& !object.contains_key("response")
&& !object.contains_key("chatMessage")
&& !object.contains_key("choices")
&& !object.contains_key("text")
&& !object.contains_key("content")
&& !object.contains_key("assistantMessage")
&& !object.contains_key("assistant_message")
}
fn extract_windsurf_text(value: &Value) -> Option<String> {
if let Some(text) = value
.as_str()
.map(str::trim)
.filter(|value| !value.is_empty())
{
return Some(text.to_string());
}
let object = value.as_object()?;
for key in [
"text",
"content",
"message",
"answer",
"completion",
"assistantMessage",
"assistant_message",
] {
if let Some(text) = object
.get(key)
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
{
return Some(text.to_string());
}
}
None
}
fn windsurf_response_id(value: &Value) -> String {
value
.get("id")
.or_else(|| value.get("responseId"))
.or_else(|| value.get("messageId"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("windsurf-cascade")
.to_string()
}
fn build_openai_chat_response_from_text(source: &Value, text: String) -> Value {
serde_json::json!({
"id": windsurf_response_id(source),
"object": "chat.completion",
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": text},
"finish_reason": "stop"
}]
})
}
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 None;
};
let mut current_event_type: Option<String> = None;
for raw_line in text.lines() {
let line = raw_line.trim_matches('\r').trim();
@@ -282,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 {
@@ -427,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]
@@ -497,6 +878,104 @@ mod tests {
assert!(output_text.contains("\"id\":\"call_get_weather_0\""));
}
#[test]
fn normalizes_windsurf_sync_text_response_to_openai_chat() {
let report_context = json!({
"has_envelope": true,
"envelope_name": "windsurf:GetChatMessage",
"provider_api_format": "openai:chat",
});
let normalized = normalize_provider_private_response_value(
json!({
"responseId": "ws-1",
"response": {"text": "hello from cascade"}
}),
&report_context,
)
.expect("windsurf response should normalize");
assert_eq!(normalized["id"], json!("ws-1"));
assert_eq!(
normalized["choices"][0]["message"]["content"],
json!("hello from cascade")
);
}
#[test]
fn unwraps_windsurf_stream_text_event() {
let report_context = json!({
"has_envelope": true,
"envelope_name": "windsurf:GetChatMessage",
"provider_api_format": "openai:chat",
});
let output = transform_provider_private_stream_line(
&report_context,
br#"data: {"responseId":"ws-2","response":{"text":"chunk"}}"#.to_vec(),
)
.expect("windsurf stream line should transform");
let text = String::from_utf8(output).expect("utf8");
assert!(text.contains(r#""object":"chat.completion.chunk""#));
assert!(text.contains(r#""content":"chunk""#));
}
#[test]
fn unwraps_windsurf_connect_json_stream_frames() {
let report_context = json!({
"has_envelope": true,
"envelope_name": "windsurf:GetChatMessage",
"provider_api_format": "openai:chat",
});
let mut normalizer = maybe_build_provider_private_stream_normalizer(Some(&report_context))
.expect("normalizer should exist");
let mut framed = connect_json_frame(
0,
br#"{"responseId":"ws-3","response":{"text":"frame chunk"}}"#,
);
framed.extend(connect_json_frame(2, b"{}"));
let mut output = normalizer
.push_chunk(&framed)
.expect("connect frame should normalize");
output.extend(normalizer.finish().expect("finish should succeed"));
let text = String::from_utf8(output).expect("utf8");
assert!(text.contains(r#""object":"chat.completion.chunk""#));
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);
out.extend_from_slice(&(payload.len() as u32).to_be_bytes());
out.extend_from_slice(payload);
out
}
#[test]
fn private_stream_normalizer_unwraps_antigravity_stream() {
let report_context = json!({
@@ -526,4 +1005,39 @@ data: {"message":"bad"}
"#;
assert!(stream_body_contains_error_event(body));
}
#[test]
fn windsurf_sync_error_message_is_not_normalized_as_success() {
let report_context = json!({
"has_envelope": true,
"envelope_name": "windsurf:GetChatMessage",
"provider_api_format": "openai:chat",
});
let normalized = normalize_provider_private_response_value(
json!({"message": "rate limited"}),
&report_context,
);
assert!(normalized.is_none());
}
#[test]
fn windsurf_stream_error_event_is_preserved() {
let report_context = json!({
"has_envelope": true,
"envelope_name": "windsurf:GetChatMessage",
"provider_api_format": "openai:chat",
});
let mut normalizer = maybe_build_provider_private_stream_normalizer(Some(&report_context))
.expect("normalizer should exist");
let output = normalizer
.push_chunk(b"event: error\ndata: {\"message\":\"rate limited\"}\n\n")
.expect("normalizer should preserve error event");
let output_text = String::from_utf8(output).expect("utf8");
assert!(output_text.contains("event: error"));
assert!(output_text.contains("\"message\":\"rate limited\""));
assert!(!output_text.contains("chat.completion.chunk"));
}
}
@@ -1,8 +1,10 @@
pub const ANTIGRAVITY_PROVIDER_TYPE: &str = "antigravity";
pub const KIRO_PROVIDER_TYPE: &str = "kiro";
pub const WINDSURF_PROVIDER_TYPE: &str = "windsurf";
pub const KIRO_ENVELOPE_NAME: &str = "kiro:generateAssistantResponse";
pub const ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME: &str = "antigravity:v1internal";
pub const GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME: &str = "gemini_cli:v1internal";
pub const WINDSURF_ENVELOPE_NAME: &str = "windsurf:GetChatMessage";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProviderAdaptationSurface {
@@ -10,6 +12,7 @@ pub enum ProviderAdaptationSurface {
AntigravityGeminiCli,
GeminiCliV1Internal,
KiroClaudeCli,
WindsurfCascade,
}
#[derive(Debug, Clone, Copy)]
@@ -70,6 +73,17 @@ const PROVIDER_ADAPTATION_SURFACES: &[ProviderAdaptationDescriptor] = &[
requires_eventstream_accept: true,
unwraps_response_envelope: false,
},
ProviderAdaptationDescriptor {
surface: ProviderAdaptationSurface::WindsurfCascade,
provider_type: Some(WINDSURF_PROVIDER_TYPE),
envelope_name: WINDSURF_ENVELOPE_NAME,
anchor_api_format: "openai:chat",
supports_request_bridge: true,
supports_sync_finalize_bridge: true,
supports_stream_bridge: true,
requires_eventstream_accept: false,
unwraps_response_envelope: true,
},
];
pub fn provider_adaptation_descriptor_for_envelope(
@@ -141,7 +155,7 @@ mod tests {
provider_adaptation_allows_sync_finalize_envelope, provider_adaptation_anchor_api_format,
provider_adaptation_requires_eventstream_accept,
provider_adaptation_should_unwrap_stream_envelope, ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME,
GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME, KIRO_ENVELOPE_NAME,
GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME, KIRO_ENVELOPE_NAME, WINDSURF_ENVELOPE_NAME,
};
#[test]
@@ -164,6 +178,10 @@ mod tests {
provider_adaptation_anchor_api_format(KIRO_ENVELOPE_NAME, "claude:messages"),
Some("claude:messages")
);
assert_eq!(
provider_adaptation_anchor_api_format(WINDSURF_ENVELOPE_NAME, "openai:chat"),
Some("openai:chat")
);
}
#[test]
@@ -184,5 +202,9 @@ mod tests {
Some(ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME),
"gemini:generate_content"
));
assert!(provider_adaptation_should_unwrap_stream_envelope(
WINDSURF_ENVELOPE_NAME,
"openai:chat"
));
}
}