mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-28 00:35:32 +08:00
fix(proxy): improve cache hit rate for Codex/Responses requests
prompt_cache_key was falling back to provider.id when the client did not supply a session, which collapsed every conversation onto a single key and defeated upstream prefix caching. Only emit the key when a real client-provided session/thread identity is available; otherwise let the upstream use its default matching behaviour. Additional fixes that affect cache stability: - Canonicalise (sort) JSON keys in outgoing request bodies and in tool_call arguments / tool_result content so semantically identical requests produce identical byte sequences for upstream prefix caches. - Exempt JSON Schema property maps (properties, patternProperties, definitions, \$defs) from the underscore-prefix filter so user-defined schema keys like _id and _meta survive. - Add a [CacheTrace] debug log with stable hashes for instructions, tools, input and include to help diagnose cache misses. - Thread session_id into the usage logger for request correlation.
This commit is contained in:
@@ -154,37 +154,37 @@ pub fn transform_claude_request_for_api_format(
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string())
|
||||
})
|
||||
} else if is_codex_oauth {
|
||||
} else {
|
||||
session_id
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(ToString::to_string)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let explicit_cache_key = provider
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|m| m.prompt_cache_key.as_deref());
|
||||
let cache_key = if is_codex_oauth {
|
||||
explicit_cache_key
|
||||
.or(session_cache_key.as_deref())
|
||||
.unwrap_or(&provider.id)
|
||||
let (cache_key, cache_key_source) = if let Some(key) = explicit_cache_key {
|
||||
(Some(key), "explicit")
|
||||
} else if let Some(key) = session_cache_key.as_deref() {
|
||||
(Some(key), "session")
|
||||
} else {
|
||||
session_cache_key
|
||||
.as_deref()
|
||||
.or(explicit_cache_key)
|
||||
.unwrap_or(&provider.id)
|
||||
(None, "none")
|
||||
};
|
||||
match api_format {
|
||||
"openai_responses" => {
|
||||
log::debug!(
|
||||
"[Cache] OpenAI Responses prompt_cache_key source={cache_key_source}, provider={}, codex_oauth={is_codex_oauth}, has_key={}",
|
||||
provider.id,
|
||||
cache_key.is_some()
|
||||
);
|
||||
// Codex OAuth (ChatGPT Plus/Pro 反代) 需要在请求体里强制 store: false
|
||||
// + include: ["reasoning.encrypted_content"],由 transform 层统一处理。
|
||||
let codex_fast_mode = provider.codex_fast_mode_enabled();
|
||||
super::transform_responses::anthropic_to_responses(
|
||||
body,
|
||||
Some(cache_key),
|
||||
cache_key,
|
||||
is_codex_oauth,
|
||||
codex_fast_mode,
|
||||
)
|
||||
@@ -1445,7 +1445,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transform_claude_request_for_codex_oauth_without_session_falls_back_to_provider_id() {
|
||||
fn test_transform_claude_request_for_codex_oauth_without_session_omits_cache_key() {
|
||||
let provider = create_provider_with_meta(
|
||||
json!({
|
||||
"env": {
|
||||
@@ -1473,7 +1473,69 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(transformed["prompt_cache_key"], provider.id);
|
||||
assert!(transformed.get("prompt_cache_key").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transform_claude_request_for_responses_uses_session_cache_key() {
|
||||
let provider = create_provider_with_meta(
|
||||
json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.openai.example.com"
|
||||
}
|
||||
}),
|
||||
ProviderMeta {
|
||||
api_format: Some("openai_responses".to_string()),
|
||||
..ProviderMeta::default()
|
||||
},
|
||||
);
|
||||
let body = json!({
|
||||
"model": "gpt-5.4",
|
||||
"messages": [{ "role": "user", "content": "hello" }],
|
||||
"max_tokens": 128
|
||||
});
|
||||
|
||||
let transformed = transform_claude_request_for_api_format(
|
||||
body,
|
||||
&provider,
|
||||
"openai_responses",
|
||||
Some("claude-session-123"),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(transformed["prompt_cache_key"], "claude-session-123");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transform_claude_request_for_responses_without_session_omits_cache_key() {
|
||||
let provider = create_provider_with_meta(
|
||||
json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.openai.example.com"
|
||||
}
|
||||
}),
|
||||
ProviderMeta {
|
||||
api_format: Some("openai_responses".to_string()),
|
||||
..ProviderMeta::default()
|
||||
},
|
||||
);
|
||||
let body = json!({
|
||||
"model": "gpt-5.4",
|
||||
"messages": [{ "role": "user", "content": "hello" }],
|
||||
"max_tokens": 128
|
||||
});
|
||||
|
||||
let transformed = transform_claude_request_for_api_format(
|
||||
body,
|
||||
&provider,
|
||||
"openai_responses",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(transformed.get("prompt_cache_key").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! 实现 Anthropic ↔ OpenAI 格式转换,用于 OpenRouter 支持
|
||||
//! 参考: anthropic-proxy-rs
|
||||
|
||||
use crate::proxy::error::ProxyError;
|
||||
use crate::proxy::{error::ProxyError, json_canonical::canonical_json_string};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
const ANTHROPIC_BILLING_HEADER_PREFIX: &str = "x-anthropic-billing-header:";
|
||||
@@ -371,7 +371,7 @@ fn convert_message_to_openai(
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": name,
|
||||
"arguments": serde_json::to_string(&input).unwrap_or_default()
|
||||
"arguments": canonical_json_string(&input)
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -384,7 +384,7 @@ fn convert_message_to_openai(
|
||||
let content_val = block.get("content");
|
||||
let content_str = match content_val {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
Some(v) => serde_json::to_string(v).unwrap_or_default(),
|
||||
Some(v) => canonical_json_string(v),
|
||||
None => String::new(),
|
||||
};
|
||||
result.push(json!({
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
//! - system prompt 使用 `instructions` 字段而非 system role message
|
||||
//! - usage 字段命名与 Anthropic 一致 (input_tokens/output_tokens)
|
||||
|
||||
use crate::proxy::error::ProxyError;
|
||||
use crate::proxy::{error::ProxyError, json_canonical::canonical_json_string};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
pub(crate) fn sanitize_anthropic_tool_use_input(name: &str, input: Value) -> Value {
|
||||
@@ -441,7 +441,7 @@ fn convert_messages_to_input(messages: &[Value]) -> Result<Vec<Value>, ProxyErro
|
||||
"type": "function_call",
|
||||
"call_id": id,
|
||||
"name": name,
|
||||
"arguments": serde_json::to_string(&arguments).unwrap_or_default()
|
||||
"arguments": canonical_json_string(&arguments)
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@ fn convert_messages_to_input(messages: &[Value]) -> Result<Vec<Value>, ProxyErro
|
||||
.unwrap_or("");
|
||||
let output = match block.get("content") {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
Some(v) => serde_json::to_string(v).unwrap_or_default(),
|
||||
Some(v) => canonical_json_string(v),
|
||||
None => String::new(),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user