diff --git a/CHANGELOG.md b/CHANGELOG.md index 12c1d2cfe..7bc54d941 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Development since v3.16.4 reworks the Codex native-Responses path — restoring ### Changed +- **Codex Chat Prompt-Cache Routing**: Added provider-aware `prompt_cache_key` injection on the Codex Responses→Chat Completions bridge. Kimi Coding and OpenAI official endpoints are enabled automatically, Kimi's preset opts in explicitly, and unknown OpenAI-compatible gateways remain off to avoid strict-schema 400s. The key is taken from an explicit client value or a real client-provided session ID—never a generated per-request UUID—with an advanced Auto/Enabled/Disabled override in all four UI locales. - **Provider Connectivity Configuration Simplified**: Removed the obsolete per-provider `testConfig` override (timeout, retry count, and degraded-latency threshold) from provider forms, frontend/backend provider metadata, and reachability-check merging. The lightweight `base_url` probe now always uses the single global connectivity-check configuration, while automatic failover remains driven exclusively by its separate proxy timeout and circuit-breaker settings. Also renamed the remaining settings UI and API modules from model-test terminology to connectivity-check terminology, removed the retired first-use confirmation flag, and deleted stale model/prompt/error copy across all four locales. - **Decoupled Codex Model Mapping From the Local-Routing Toggle**: Aligns the Codex provider form with Claude Code — the model-mapping catalog is now independent of route takeover, since native Responses providers (MiMo, Doubao, MiniMax) need it for proxy-less direct connect while Chat providers use the proxy regardless of any per-provider flag. The "Needs Local Routing" toggle is removed: it had no backend field and only gated catalog/reasoning persistence, which is equivalent to whether the mapping is filled. Model mapping is now always shown for non-official providers and persisted whenever the list is non-empty (the backend already keys off `modelCatalog.models`), while reasoning visibility/persistence is gated on the Chat format instead of the toggle. The Chat upstream-format option is marked "routing required" with a refreshed advanced-section hint across all four locales (zh/en/ja/zh-TW), dead `localRouting*` keys are dropped, and the model-mapping block moves above the custom User-Agent block. Also fixes `useCodexConfigState` dropping `supportsParallelToolCalls`/`inputModalities`/`baseInstructions` when loading a saved provider — which silently lost parallel tools, image input, and the official base instructions on edit — by preserving them on load (camelCase and snake_case) and comparing them in the row sync. - **Auto-Sync Claude Common Config From Live settings.json on Switch**: When switching away from a Claude provider that has `common_config_enabled`, the service now re-extracts the shareable portion of its live `settings.json` and replaces the stored common-config snippet, instead of only writing the snippet one way. This captures config the user added directly in the running app (`enabledPlugins`, `hooks`, `env`, `theme`, shared prefs) so it isn't silently lost on the next switch, and it propagates deletions so a removed key isn't re-injected later. The sync in `services/provider/mod.rs` is scoped strictly to Claude providers with `common_config_enabled`, is skipped when the snippet was explicitly cleared, and all failures are non-fatal (warn-only) and never block the switch. Four integration tests cover capture / delete-sync / opt-out / cleared. diff --git a/src-tauri/src/provider.rs b/src-tauri/src/provider.rs index 7056cb820..4ce3c37d8 100644 --- a/src-tauri/src/provider.rs +++ b/src-tauri/src/provider.rs @@ -457,6 +457,10 @@ pub struct ProviderMeta { /// identity when available; generated session IDs are not sent upstream. #[serde(rename = "promptCacheKey", skip_serializing_if = "Option::is_none")] pub prompt_cache_key: Option, + /// Session-based prompt-cache routing for Codex Responses -> Chat conversions. + /// "auto" enables known-compatible upstreams; "enabled" / "disabled" are overrides. + #[serde(rename = "promptCacheRouting", skip_serializing_if = "Option::is_none")] + pub prompt_cache_routing: Option, /// Codex OAuth FAST mode: inject `service_tier = "priority"` for ChatGPT Codex requests. #[serde(rename = "codexFastMode", skip_serializing_if = "Option::is_none")] pub codex_fast_mode: Option, diff --git a/src-tauri/src/proxy/forwarder.rs b/src-tauri/src/proxy/forwarder.rs index 5d62fed39..d61d928cf 100644 --- a/src-tauri/src/proxy/forwarder.rs +++ b/src-tauri/src/proxy/forwarder.rs @@ -1384,6 +1384,10 @@ impl RequestForwarder { // 转换请求体(如果需要) let mut request_body = if codex_responses_to_chat { let mut mapped_body = mapped_body; + let explicit_prompt_cache_key = mapped_body + .get("prompt_cache_key") + .and_then(|value| value.as_str()) + .map(ToString::to_string); let restored = self .codex_chat_history .enrich_request(&mut mapped_body) @@ -1396,10 +1400,18 @@ impl RequestForwarder { super::providers::apply_codex_chat_upstream_model(provider, &mut mapped_body); let reasoning_config = super::providers::resolve_codex_chat_reasoning_config(provider, &mapped_body); - super::providers::transform_codex_chat::responses_to_chat_completions_with_reasoning( + let mut chat_body = super::providers::transform_codex_chat::responses_to_chat_completions_with_reasoning( mapped_body, reasoning_config.as_ref(), - )? + )?; + super::providers::inject_codex_chat_prompt_cache_key( + provider, + &mut chat_body, + explicit_prompt_cache_key.as_deref(), + self.session_client_provided + .then_some(self.session_id.as_str()), + ); + chat_body } else if codex_responses_to_anthropic { let mut mapped_body = mapped_body; super::providers::apply_codex_upstream_model(provider, &mut mapped_body); diff --git a/src-tauri/src/proxy/providers/codex.rs b/src-tauri/src/proxy/providers/codex.rs index cf714bbb2..281d2968d 100644 --- a/src-tauri/src/proxy/providers/codex.rs +++ b/src-tauri/src/proxy/providers/codex.rs @@ -84,6 +84,81 @@ pub fn should_convert_codex_responses_to_chat(provider: &Provider, endpoint: &st ) && codex_provider_uses_chat_completions(provider) } +/// Whether a converted Codex Responses request may send `prompt_cache_key` to +/// its Chat Completions upstream. Unknown OpenAI-compatible gateways default to +/// false because many reject unsupported request fields with HTTP 400. +pub fn should_send_codex_chat_prompt_cache_key(provider: &Provider) -> bool { + match provider + .meta + .as_ref() + .and_then(|meta| meta.prompt_cache_routing.as_deref()) + .unwrap_or("auto") + { + "enabled" => return true, + "disabled" => return false, + _ => {} + } + + let base_url = provider + .settings_config + .get("base_url") + .or_else(|| provider.settings_config.get("baseURL")) + .and_then(|value| value.as_str()) + .map(ToString::to_string) + .or_else(|| { + provider + .settings_config + .get("config") + .and_then(|value| value.as_str()) + .and_then(extract_codex_base_url_from_toml) + }); + + let Some(base_url) = base_url else { + return false; + }; + let Ok(url) = url::Url::parse(&base_url) else { + return false; + }; + + match url.host_str() { + Some("api.openai.com") => true, + Some("api.kimi.com") => { + let path = url.path().trim_end_matches('/'); + path == "/coding" || path.starts_with("/coding/") + } + _ => false, + } +} + +/// Add a stable cache-routing key after Responses -> Chat conversion. An +/// explicit client key wins; otherwise only a real client-provided session ID +/// is eligible. Generated per-request UUIDs must never be used here. +pub fn inject_codex_chat_prompt_cache_key( + provider: &Provider, + chat_body: &mut JsonValue, + explicit_key: Option<&str>, + client_session_id: Option<&str>, +) -> bool { + if !should_send_codex_chat_prompt_cache_key(provider) { + return false; + } + + let key = explicit_key + .map(str::trim) + .filter(|key| !key.is_empty()) + .or_else(|| { + client_session_id + .map(str::trim) + .filter(|session_id| !session_id.is_empty()) + }); + let Some(key) = key else { + return false; + }; + + chat_body["prompt_cache_key"] = JsonValue::String(key.to_string()); + true +} + /// Whether this Codex provider's real upstream speaks the native Anthropic /// Messages protocol (`/v1/messages`). The local Codex client always talks to CC /// Switch through the Responses API, so CC Switch bridges Responses ⇄ Anthropic. @@ -706,6 +781,100 @@ mod tests { } } + #[test] + fn prompt_cache_routing_auto_enables_known_upstreams_only() { + let kimi = create_provider(json!({ + "config": r#" +model_provider = "custom" +[model_providers.custom] +base_url = "https://api.kimi.com/coding/v1" +wire_api = "responses" +"# + })); + let openai = create_provider(json!({ + "base_url": "https://api.openai.com/v1" + })); + let unknown = create_provider(json!({ + "base_url": "https://strict.example.com/v1" + })); + + assert!(should_send_codex_chat_prompt_cache_key(&kimi)); + assert!(should_send_codex_chat_prompt_cache_key(&openai)); + assert!(!should_send_codex_chat_prompt_cache_key(&unknown)); + } + + #[test] + fn prompt_cache_routing_user_override_wins_over_auto_detection() { + let mut kimi = create_provider(json!({ + "base_url": "https://api.kimi.com/coding/v1" + })); + kimi.meta = Some(crate::provider::ProviderMeta { + prompt_cache_routing: Some("disabled".to_string()), + ..Default::default() + }); + assert!(!should_send_codex_chat_prompt_cache_key(&kimi)); + + let mut unknown = create_provider(json!({ + "base_url": "https://strict.example.com/v1" + })); + unknown.meta = Some(crate::provider::ProviderMeta { + prompt_cache_routing: Some("enabled".to_string()), + ..Default::default() + }); + assert!(should_send_codex_chat_prompt_cache_key(&unknown)); + } + + #[test] + fn prompt_cache_key_prefers_explicit_key_then_real_session() { + let provider = create_provider(json!({ + "base_url": "https://api.kimi.com/coding/v1" + })); + let mut explicit_body = json!({ "model": "kimi-for-coding" }); + assert!(inject_codex_chat_prompt_cache_key( + &provider, + &mut explicit_body, + Some("request-key"), + Some("session-key"), + )); + assert_eq!(explicit_body["prompt_cache_key"], "request-key"); + + let mut session_body = json!({ "model": "kimi-for-coding" }); + assert!(inject_codex_chat_prompt_cache_key( + &provider, + &mut session_body, + None, + Some("session-key"), + )); + assert_eq!(session_body["prompt_cache_key"], "session-key"); + } + + #[test] + fn prompt_cache_key_is_not_injected_without_real_session_or_support() { + let kimi = create_provider(json!({ + "base_url": "https://api.kimi.com/coding/v1" + })); + let mut no_session_body = json!({ "model": "kimi-for-coding" }); + assert!(!inject_codex_chat_prompt_cache_key( + &kimi, + &mut no_session_body, + None, + None, + )); + assert!(no_session_body.get("prompt_cache_key").is_none()); + + let unknown = create_provider(json!({ + "base_url": "https://strict.example.com/v1" + })); + let mut unsupported_body = json!({ "model": "other" }); + assert!(!inject_codex_chat_prompt_cache_key( + &unknown, + &mut unsupported_body, + Some("request-key"), + Some("session-key"), + )); + assert!(unsupported_body.get("prompt_cache_key").is_none()); + } + #[test] fn test_extract_base_url_direct() { let adapter = CodexAdapter::new(); diff --git a/src-tauri/src/proxy/providers/mod.rs b/src-tauri/src/proxy/providers/mod.rs index 2684da481..cbd2eac2b 100644 --- a/src-tauri/src/proxy/providers/mod.rs +++ b/src-tauri/src/proxy/providers/mod.rs @@ -52,8 +52,9 @@ pub use claude::{ pub use codex::CodexAdapter; pub use codex::{ apply_codex_chat_upstream_model, apply_codex_upstream_model, codex_provider_upstream_model, - resolve_codex_catalog_tool_profile, resolve_codex_chat_reasoning_config, - should_convert_codex_responses_to_anthropic, should_convert_codex_responses_to_chat, + inject_codex_chat_prompt_cache_key, resolve_codex_catalog_tool_profile, + resolve_codex_chat_reasoning_config, should_convert_codex_responses_to_anthropic, + should_convert_codex_responses_to_chat, }; pub use gemini::GeminiAdapter; diff --git a/src/components/providers/forms/CodexFormFields.tsx b/src/components/providers/forms/CodexFormFields.tsx index 8cea0d88a..b6c612b02 100644 --- a/src/components/providers/forms/CodexFormFields.tsx +++ b/src/components/providers/forms/CodexFormFields.tsx @@ -40,6 +40,7 @@ import type { CodexApiFormat, CodexCatalogModel, CodexChatReasoning, + PromptCacheRoutingMode, ProviderCategory, } from "@/types"; @@ -89,6 +90,8 @@ interface CodexFormFieldsProps { onMaxOutputTokensChange: (value: string) => void; codexChatReasoning?: CodexChatReasoning; onCodexChatReasoningChange?: (value: CodexChatReasoning) => void; + promptCacheRouting: PromptCacheRoutingMode; + onPromptCacheRoutingChange: (value: PromptCacheRoutingMode) => void; // Model Catalog catalogModels?: CodexCatalogModel[]; @@ -182,6 +185,8 @@ export function CodexFormFields({ onMaxOutputTokensChange, codexChatReasoning = {}, onCodexChatReasoningChange, + promptCacheRouting, + onPromptCacheRoutingChange, catalogModels = [], onCatalogModelsChange, speedTestEndpoints, @@ -229,6 +234,7 @@ export function CodexFormFields({ isAnthropicFormat || supportsThinking || supportsEffort || + promptCacheRouting !== "auto" || !!maxOutputTokens; const [advancedExpanded, setAdvancedExpanded] = useState(hasAnyAdvancedValue); @@ -716,6 +722,49 @@ export function CodexFormFields({ shouldShowSpeedTest && "border-t border-border-default pt-3", )} > +
+ + {t("codexConfig.promptCacheRoutingLabel", { + defaultValue: "提示词缓存路由", + })} + + +

+ {t("codexConfig.promptCacheRoutingHint", { + defaultValue: + "自动模式仅对已确认兼容的上游发送 prompt_cache_key;开启可用于其他兼容网关,关闭可避免严格网关因未知字段返回 400。只使用客户端提供的稳定会话 ID。", + })} +

+
+
{t("codexConfig.reasoningGroupTitle", { diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx index 3cbceb9ab..75130ecbe 100644 --- a/src/components/providers/forms/ProviderForm.tsx +++ b/src/components/providers/forms/ProviderForm.tsx @@ -21,6 +21,7 @@ import type { CodexApiFormat, CodexCatalogModel, CodexChatReasoning, + PromptCacheRoutingMode, ClaudeApiKeyField, } from "@/types"; import { @@ -357,6 +358,7 @@ function ProviderFormFull({ ), }); setCodexChatReasoning(initialData?.meta?.codexChatReasoning ?? {}); + setPromptCacheRouting(initialData?.meta?.promptCacheRouting ?? "auto"); setCustomUserAgent(initialData?.meta?.customUserAgent ?? ""); setLocalProxyHeadersOverride( formatRequestOverrideObject( @@ -529,6 +531,10 @@ function ProviderFormFull({ useState( () => initialData?.meta?.codexChatReasoning ?? {}, ); + const [promptCacheRouting, setPromptCacheRouting] = + useState( + () => initialData?.meta?.promptCacheRouting ?? "auto", + ); const [customUserAgent, setCustomUserAgent] = useState( () => initialData?.meta?.customUserAgent ?? "", ); @@ -632,6 +638,7 @@ function ProviderFormFull({ const template = getCodexCustomTemplate(); resetCodexConfig(template.auth, template.config); setCodexChatReasoning({}); + setPromptCacheRouting("auto"); } }, [appId, initialData, selectedPresetId, resetCodexConfig]); @@ -1485,6 +1492,13 @@ function ProviderFormFull({ localCodexApiFormat === "openai_chat" ? normalizeCodexChatReasoningForSave(codexChatReasoning) : undefined, + promptCacheRouting: + appId === "codex" && + category !== "official" && + localCodexApiFormat === "openai_chat" && + promptCacheRouting !== "auto" + ? promptCacheRouting + : undefined, customUserAgent: (appId === "claude" || appId === "codex") && category !== "official" ? customUserAgent.trim() || undefined @@ -1651,6 +1665,7 @@ function ProviderFormFull({ const template = getCodexCustomTemplate(); resetCodexConfig(template.auth, template.config); setCodexChatReasoning({}); + setPromptCacheRouting("auto"); setLocalCodexApiFormat( codexApiFormatFromWireApi(extractCodexWireApi(template.config)) ?? "openai_responses", @@ -1692,6 +1707,7 @@ function ProviderFormFull({ resetCodexConfig(auth, config, preset.modelCatalog ?? []); setCodexChatReasoning(preset.codexChatReasoning ?? {}); + setPromptCacheRouting(preset.promptCacheRouting ?? "auto"); setLocalCodexApiFormat( preset.apiFormat ?? codexApiFormatFromWireApi(extractCodexWireApi(config)) ?? @@ -2182,6 +2198,8 @@ function ProviderFormFull({ onMaxOutputTokensChange={setLocalCodexMaxOutputTokens} codexChatReasoning={codexChatReasoning} onCodexChatReasoningChange={setCodexChatReasoning} + promptCacheRouting={promptCacheRouting} + onPromptCacheRoutingChange={setPromptCacheRouting} catalogModels={codexCatalogModels} onCatalogModelsChange={setCodexCatalogModels} speedTestEndpoints={speedTestEndpoints} diff --git a/src/config/codexProviderPresets.ts b/src/config/codexProviderPresets.ts index bb82c56a9..f116353ce 100644 --- a/src/config/codexProviderPresets.ts +++ b/src/config/codexProviderPresets.ts @@ -6,6 +6,7 @@ import type { CodexApiFormat, CodexCatalogModel, CodexChatReasoning, + PromptCacheRoutingMode, } from "../types"; import type { PresetTheme } from "./claudeProviderPresets"; @@ -36,6 +37,8 @@ export interface CodexProviderPreset { modelCatalog?: CodexCatalogModel[]; // Codex Responses -> Chat Completions reasoning capability defaults codexChatReasoning?: CodexChatReasoning; + // Session-based prompt-cache routing override for Chat Completions upstreams + promptCacheRouting?: PromptCacheRoutingMode; } /** @@ -604,6 +607,7 @@ requires_openai_auth = true`, ), endpointCandidates: ["https://api.kimi.com/coding/v1"], apiFormat: "openai_chat", + promptCacheRouting: "enabled", modelCatalog: modelCatalog([ { model: "kimi-for-coding", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 5da83962c..316291d40 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1284,6 +1284,11 @@ "maxOutputTokensLabel": "Max output tokens", "maxOutputTokensPlaceholder": "Leave empty to use the default 8192", "maxOutputTokensHint": "Codex does not forward model_max_output_tokens in the request body, so the default 8192 ceiling can truncate long or thinking-heavy responses (stop_reason=max_tokens). This value overrides the request's max_tokens on the Anthropic path. Do not exceed the real output ceiling of the model/gateway or it may 400. Leave empty to use the default 8192.", + "promptCacheRoutingLabel": "Prompt cache routing", + "promptCacheRoutingAuto": "Auto (recommended)", + "promptCacheRoutingEnabled": "Enabled", + "promptCacheRoutingDisabled": "Disabled", + "promptCacheRoutingHint": "Auto sends prompt_cache_key only to known-compatible upstreams. Enable it for other compatible gateways, or disable it if a strict gateway rejects unknown fields with HTTP 400. Only a stable client-provided session ID is used.", "upstreamModelName": "Upstream Model Name", "upstreamModelNameHint": "For Chat format, enter the real upstream model here; routing converts Codex Responses requests to Chat Completions while keeping this model.", "modelNamePlaceholder": "e.g., gpt-5-codex", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 0d606b7c6..f639b7bf9 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -1284,6 +1284,11 @@ "maxOutputTokensLabel": "最大出力トークン", "maxOutputTokensPlaceholder": "空欄の場合はデフォルトの 8192 を使用", "maxOutputTokensHint": "Codex は model_max_output_tokens をリクエストボディに含めないため、デフォルト上限 8192 では長い回答や深い思考時に切り詰められることがあります(stop_reason=max_tokens)。この値は Anthropic 経路で max_tokens を上書きします。モデル/ゲートウェイの実際の出力上限を超えると 400 になる場合があります。空欄でデフォルトの 8192 を使用します。", + "promptCacheRoutingLabel": "プロンプトキャッシュルーティング", + "promptCacheRoutingAuto": "自動(推奨)", + "promptCacheRoutingEnabled": "有効", + "promptCacheRoutingDisabled": "無効", + "promptCacheRoutingHint": "自動モードでは、互換性を確認済みの上流にのみ prompt_cache_key を送信します。他の互換ゲートウェイでは有効にし、未知のフィールドを 400 で拒否する厳格なゲートウェイでは無効にしてください。クライアントが提供した安定したセッション ID のみを使用します。", "upstreamModelName": "上流モデル名", "upstreamModelNameHint": "Chat 形式ではここに実際の上流モデルを入力します。ルーティングで Codex の Responses リクエストを Chat Completions に変換し、このモデルを維持します。", "modelNamePlaceholder": "例: gpt-5-codex", diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index cecf1f6a2..2d0a19d4d 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -1261,6 +1261,11 @@ "maxOutputTokensLabel": "最大輸出 tokens", "maxOutputTokensPlaceholder": "留空則使用預設 8192", "maxOutputTokensHint": "Codex 不會把 model_max_output_tokens 寫進請求體,預設上限 8192 容易在長回答或深度思考時被截斷(stop_reason=max_tokens)。此處設定會作為 Anthropic 的 max_tokens 覆蓋請求值。請勿超過該模型/閘道的真實輸出上限,否則可能 400。留空使用預設 8192。", + "promptCacheRoutingLabel": "提示詞快取路由", + "promptCacheRoutingAuto": "自動(建議)", + "promptCacheRoutingEnabled": "開啟", + "promptCacheRoutingDisabled": "關閉", + "promptCacheRoutingHint": "自動模式僅對已確認相容的上游傳送 prompt_cache_key;開啟可用於其他相容閘道,關閉可避免嚴格閘道因未知欄位回傳 400。只使用客戶端提供的穩定工作階段 ID。", "defaultModelLabel": "預設模型", "defaultModelPlaceholder": "例如: gpt-5.6", "defaultModelHint": "Codex 預設請求的模型,隨時可改,無需等待軟體更新供應商範本。留空且設定了模型映射時,預設使用映射第一行。", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index f2c842867..427b7dc7a 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -1284,6 +1284,11 @@ "maxOutputTokensLabel": "最大输出 tokens", "maxOutputTokensPlaceholder": "留空则使用默认 8192", "maxOutputTokensHint": "Codex 不会把 model_max_output_tokens 写进请求体,默认上限 8192 容易在长回答或深度思考时被截断(stop_reason=max_tokens)。此处设置会作为 Anthropic 的 max_tokens 覆盖请求值。请勿超过该模型/网关的真实输出上限,否则可能 400。留空使用默认 8192。", + "promptCacheRoutingLabel": "提示词缓存路由", + "promptCacheRoutingAuto": "自动(推荐)", + "promptCacheRoutingEnabled": "开启", + "promptCacheRoutingDisabled": "关闭", + "promptCacheRoutingHint": "自动模式仅对已确认兼容的上游发送 prompt_cache_key;开启可用于其他兼容网关,关闭可避免严格网关因未知字段返回 400。只使用客户端提供的稳定会话 ID。", "upstreamModelName": "上游模型名称", "upstreamModelNameHint": "Chat 格式下这里填写真实上游模型;路由会把 Codex 的 Responses 请求转换为 Chat Completions 并保持该模型。", "modelNamePlaceholder": "例如: gpt-5-codex", diff --git a/src/types.ts b/src/types.ts index 49c72fbad..0f9eccb50 100644 --- a/src/types.ts +++ b/src/types.ts @@ -161,6 +161,8 @@ export interface CodexChatReasoning { outputFormat?: CodexChatReasoningOutputFormat; } +export type PromptCacheRoutingMode = "auto" | "enabled" | "disabled"; + export interface LocalProxyRequestOverrides { headers?: Record; body?: Record; @@ -206,6 +208,9 @@ export interface ProviderMeta { isFullUrl?: boolean; // Prompt cache key for OpenAI Responses-compatible endpoints (improves cache hit rate) promptCacheKey?: string; + // Session-based prompt-cache routing for Codex Responses -> Chat conversions. + // auto enables only for known-compatible upstreams; enabled/disabled are user overrides. + promptCacheRouting?: PromptCacheRoutingMode; // Codex OAuth FAST mode: injects service_tier="priority" on ChatGPT Codex requests codexFastMode?: boolean; // Codex Responses -> Chat Completions reasoning capability metadata diff --git a/tests/config/codexChatProviderPresets.test.ts b/tests/config/codexChatProviderPresets.test.ts index a4499e551..e29988a43 100644 --- a/tests/config/codexChatProviderPresets.test.ts +++ b/tests/config/codexChatProviderPresets.test.ts @@ -129,6 +129,14 @@ const expectedChatPresets = new Map< ]); describe("Codex Chat provider presets", () => { + it("enables session-based prompt cache routing for Kimi Coding", () => { + const preset = codexProviderPresets.find( + (item) => item.name === "Kimi For Coding", + ); + + expect(preset?.promptCacheRouting).toBe("enabled"); + }); + it("marks migrated Chat Completions presets for local routing", () => { for (const [name, expected] of expectedChatPresets) { const preset = codexProviderPresets.find((item) => item.name === name);