mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
feat(proxy): session-based prompt_cache_key routing for Codex Chat bridge
Codex always sends prompt_cache_key in its Responses requests, but the Responses -> Chat Completions conversion dropped it, breaking session cache affinity on upstreams that route by key (e.g. Kimi Coding). - Re-inject prompt_cache_key after conversion in the forwarder: an explicit client key wins, otherwise a client-provided session ID; generated per-request UUIDs are never sent upstream. - Provider-aware gating: "auto" enables only known-compatible upstreams (api.openai.com, api.kimi.com/coding) because strict gateways reject unknown fields with HTTP 400 (e.g. Fireworks); an advanced Auto/Enabled/Disabled override is available on the Codex form in all four locales. - Kimi For Coding preset opts in explicitly.
This commit is contained in:
@@ -24,6 +24,7 @@ Development since v3.16.4 reworks the Codex native-Responses path — restoring
|
|||||||
|
|
||||||
### Changed
|
### 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.
|
- **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.
|
- **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.
|
- **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.
|
||||||
|
|||||||
@@ -457,6 +457,10 @@ pub struct ProviderMeta {
|
|||||||
/// identity when available; generated session IDs are not sent upstream.
|
/// identity when available; generated session IDs are not sent upstream.
|
||||||
#[serde(rename = "promptCacheKey", skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "promptCacheKey", skip_serializing_if = "Option::is_none")]
|
||||||
pub prompt_cache_key: Option<String>,
|
pub prompt_cache_key: Option<String>,
|
||||||
|
/// 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<String>,
|
||||||
/// Codex OAuth FAST mode: inject `service_tier = "priority"` for ChatGPT Codex requests.
|
/// Codex OAuth FAST mode: inject `service_tier = "priority"` for ChatGPT Codex requests.
|
||||||
#[serde(rename = "codexFastMode", skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "codexFastMode", skip_serializing_if = "Option::is_none")]
|
||||||
pub codex_fast_mode: Option<bool>,
|
pub codex_fast_mode: Option<bool>,
|
||||||
|
|||||||
@@ -1384,6 +1384,10 @@ impl RequestForwarder {
|
|||||||
// 转换请求体(如果需要)
|
// 转换请求体(如果需要)
|
||||||
let mut request_body = if codex_responses_to_chat {
|
let mut request_body = if codex_responses_to_chat {
|
||||||
let mut mapped_body = mapped_body;
|
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
|
let restored = self
|
||||||
.codex_chat_history
|
.codex_chat_history
|
||||||
.enrich_request(&mut mapped_body)
|
.enrich_request(&mut mapped_body)
|
||||||
@@ -1396,10 +1400,18 @@ impl RequestForwarder {
|
|||||||
super::providers::apply_codex_chat_upstream_model(provider, &mut mapped_body);
|
super::providers::apply_codex_chat_upstream_model(provider, &mut mapped_body);
|
||||||
let reasoning_config =
|
let reasoning_config =
|
||||||
super::providers::resolve_codex_chat_reasoning_config(provider, &mapped_body);
|
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,
|
mapped_body,
|
||||||
reasoning_config.as_ref(),
|
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 {
|
} else if codex_responses_to_anthropic {
|
||||||
let mut mapped_body = mapped_body;
|
let mut mapped_body = mapped_body;
|
||||||
super::providers::apply_codex_upstream_model(provider, &mut mapped_body);
|
super::providers::apply_codex_upstream_model(provider, &mut mapped_body);
|
||||||
|
|||||||
@@ -84,6 +84,81 @@ pub fn should_convert_codex_responses_to_chat(provider: &Provider, endpoint: &st
|
|||||||
) && codex_provider_uses_chat_completions(provider)
|
) && 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
|
/// Whether this Codex provider's real upstream speaks the native Anthropic
|
||||||
/// Messages protocol (`/v1/messages`). The local Codex client always talks to CC
|
/// Messages protocol (`/v1/messages`). The local Codex client always talks to CC
|
||||||
/// Switch through the Responses API, so CC Switch bridges Responses ⇄ Anthropic.
|
/// 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]
|
#[test]
|
||||||
fn test_extract_base_url_direct() {
|
fn test_extract_base_url_direct() {
|
||||||
let adapter = CodexAdapter::new();
|
let adapter = CodexAdapter::new();
|
||||||
|
|||||||
@@ -52,8 +52,9 @@ pub use claude::{
|
|||||||
pub use codex::CodexAdapter;
|
pub use codex::CodexAdapter;
|
||||||
pub use codex::{
|
pub use codex::{
|
||||||
apply_codex_chat_upstream_model, apply_codex_upstream_model, codex_provider_upstream_model,
|
apply_codex_chat_upstream_model, apply_codex_upstream_model, codex_provider_upstream_model,
|
||||||
resolve_codex_catalog_tool_profile, resolve_codex_chat_reasoning_config,
|
inject_codex_chat_prompt_cache_key, resolve_codex_catalog_tool_profile,
|
||||||
should_convert_codex_responses_to_anthropic, should_convert_codex_responses_to_chat,
|
resolve_codex_chat_reasoning_config, should_convert_codex_responses_to_anthropic,
|
||||||
|
should_convert_codex_responses_to_chat,
|
||||||
};
|
};
|
||||||
pub use gemini::GeminiAdapter;
|
pub use gemini::GeminiAdapter;
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import type {
|
|||||||
CodexApiFormat,
|
CodexApiFormat,
|
||||||
CodexCatalogModel,
|
CodexCatalogModel,
|
||||||
CodexChatReasoning,
|
CodexChatReasoning,
|
||||||
|
PromptCacheRoutingMode,
|
||||||
ProviderCategory,
|
ProviderCategory,
|
||||||
} from "@/types";
|
} from "@/types";
|
||||||
|
|
||||||
@@ -89,6 +90,8 @@ interface CodexFormFieldsProps {
|
|||||||
onMaxOutputTokensChange: (value: string) => void;
|
onMaxOutputTokensChange: (value: string) => void;
|
||||||
codexChatReasoning?: CodexChatReasoning;
|
codexChatReasoning?: CodexChatReasoning;
|
||||||
onCodexChatReasoningChange?: (value: CodexChatReasoning) => void;
|
onCodexChatReasoningChange?: (value: CodexChatReasoning) => void;
|
||||||
|
promptCacheRouting: PromptCacheRoutingMode;
|
||||||
|
onPromptCacheRoutingChange: (value: PromptCacheRoutingMode) => void;
|
||||||
|
|
||||||
// Model Catalog
|
// Model Catalog
|
||||||
catalogModels?: CodexCatalogModel[];
|
catalogModels?: CodexCatalogModel[];
|
||||||
@@ -182,6 +185,8 @@ export function CodexFormFields({
|
|||||||
onMaxOutputTokensChange,
|
onMaxOutputTokensChange,
|
||||||
codexChatReasoning = {},
|
codexChatReasoning = {},
|
||||||
onCodexChatReasoningChange,
|
onCodexChatReasoningChange,
|
||||||
|
promptCacheRouting,
|
||||||
|
onPromptCacheRoutingChange,
|
||||||
catalogModels = [],
|
catalogModels = [],
|
||||||
onCatalogModelsChange,
|
onCatalogModelsChange,
|
||||||
speedTestEndpoints,
|
speedTestEndpoints,
|
||||||
@@ -229,6 +234,7 @@ export function CodexFormFields({
|
|||||||
isAnthropicFormat ||
|
isAnthropicFormat ||
|
||||||
supportsThinking ||
|
supportsThinking ||
|
||||||
supportsEffort ||
|
supportsEffort ||
|
||||||
|
promptCacheRouting !== "auto" ||
|
||||||
!!maxOutputTokens;
|
!!maxOutputTokens;
|
||||||
const [advancedExpanded, setAdvancedExpanded] = useState(hasAnyAdvancedValue);
|
const [advancedExpanded, setAdvancedExpanded] = useState(hasAnyAdvancedValue);
|
||||||
|
|
||||||
@@ -716,6 +722,49 @@ export function CodexFormFields({
|
|||||||
shouldShowSpeedTest && "border-t border-border-default pt-3",
|
shouldShowSpeedTest && "border-t border-border-default pt-3",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<FormLabel>
|
||||||
|
{t("codexConfig.promptCacheRoutingLabel", {
|
||||||
|
defaultValue: "提示词缓存路由",
|
||||||
|
})}
|
||||||
|
</FormLabel>
|
||||||
|
<Select
|
||||||
|
value={promptCacheRouting}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
onPromptCacheRoutingChange(
|
||||||
|
value as PromptCacheRoutingMode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="auto">
|
||||||
|
{t("codexConfig.promptCacheRoutingAuto", {
|
||||||
|
defaultValue: "自动(推荐)",
|
||||||
|
})}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="enabled">
|
||||||
|
{t("codexConfig.promptCacheRoutingEnabled", {
|
||||||
|
defaultValue: "开启",
|
||||||
|
})}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="disabled">
|
||||||
|
{t("codexConfig.promptCacheRoutingDisabled", {
|
||||||
|
defaultValue: "关闭",
|
||||||
|
})}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs leading-relaxed text-muted-foreground">
|
||||||
|
{t("codexConfig.promptCacheRoutingHint", {
|
||||||
|
defaultValue:
|
||||||
|
"自动模式仅对已确认兼容的上游发送 prompt_cache_key;开启可用于其他兼容网关,关闭可避免严格网关因未知字段返回 400。只使用客户端提供的稳定会话 ID。",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t("codexConfig.reasoningGroupTitle", {
|
{t("codexConfig.reasoningGroupTitle", {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import type {
|
|||||||
CodexApiFormat,
|
CodexApiFormat,
|
||||||
CodexCatalogModel,
|
CodexCatalogModel,
|
||||||
CodexChatReasoning,
|
CodexChatReasoning,
|
||||||
|
PromptCacheRoutingMode,
|
||||||
ClaudeApiKeyField,
|
ClaudeApiKeyField,
|
||||||
} from "@/types";
|
} from "@/types";
|
||||||
import {
|
import {
|
||||||
@@ -357,6 +358,7 @@ function ProviderFormFull({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
setCodexChatReasoning(initialData?.meta?.codexChatReasoning ?? {});
|
setCodexChatReasoning(initialData?.meta?.codexChatReasoning ?? {});
|
||||||
|
setPromptCacheRouting(initialData?.meta?.promptCacheRouting ?? "auto");
|
||||||
setCustomUserAgent(initialData?.meta?.customUserAgent ?? "");
|
setCustomUserAgent(initialData?.meta?.customUserAgent ?? "");
|
||||||
setLocalProxyHeadersOverride(
|
setLocalProxyHeadersOverride(
|
||||||
formatRequestOverrideObject(
|
formatRequestOverrideObject(
|
||||||
@@ -529,6 +531,10 @@ function ProviderFormFull({
|
|||||||
useState<CodexChatReasoning>(
|
useState<CodexChatReasoning>(
|
||||||
() => initialData?.meta?.codexChatReasoning ?? {},
|
() => initialData?.meta?.codexChatReasoning ?? {},
|
||||||
);
|
);
|
||||||
|
const [promptCacheRouting, setPromptCacheRouting] =
|
||||||
|
useState<PromptCacheRoutingMode>(
|
||||||
|
() => initialData?.meta?.promptCacheRouting ?? "auto",
|
||||||
|
);
|
||||||
const [customUserAgent, setCustomUserAgent] = useState<string>(
|
const [customUserAgent, setCustomUserAgent] = useState<string>(
|
||||||
() => initialData?.meta?.customUserAgent ?? "",
|
() => initialData?.meta?.customUserAgent ?? "",
|
||||||
);
|
);
|
||||||
@@ -632,6 +638,7 @@ function ProviderFormFull({
|
|||||||
const template = getCodexCustomTemplate();
|
const template = getCodexCustomTemplate();
|
||||||
resetCodexConfig(template.auth, template.config);
|
resetCodexConfig(template.auth, template.config);
|
||||||
setCodexChatReasoning({});
|
setCodexChatReasoning({});
|
||||||
|
setPromptCacheRouting("auto");
|
||||||
}
|
}
|
||||||
}, [appId, initialData, selectedPresetId, resetCodexConfig]);
|
}, [appId, initialData, selectedPresetId, resetCodexConfig]);
|
||||||
|
|
||||||
@@ -1485,6 +1492,13 @@ function ProviderFormFull({
|
|||||||
localCodexApiFormat === "openai_chat"
|
localCodexApiFormat === "openai_chat"
|
||||||
? normalizeCodexChatReasoningForSave(codexChatReasoning)
|
? normalizeCodexChatReasoningForSave(codexChatReasoning)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
promptCacheRouting:
|
||||||
|
appId === "codex" &&
|
||||||
|
category !== "official" &&
|
||||||
|
localCodexApiFormat === "openai_chat" &&
|
||||||
|
promptCacheRouting !== "auto"
|
||||||
|
? promptCacheRouting
|
||||||
|
: undefined,
|
||||||
customUserAgent:
|
customUserAgent:
|
||||||
(appId === "claude" || appId === "codex") && category !== "official"
|
(appId === "claude" || appId === "codex") && category !== "official"
|
||||||
? customUserAgent.trim() || undefined
|
? customUserAgent.trim() || undefined
|
||||||
@@ -1651,6 +1665,7 @@ function ProviderFormFull({
|
|||||||
const template = getCodexCustomTemplate();
|
const template = getCodexCustomTemplate();
|
||||||
resetCodexConfig(template.auth, template.config);
|
resetCodexConfig(template.auth, template.config);
|
||||||
setCodexChatReasoning({});
|
setCodexChatReasoning({});
|
||||||
|
setPromptCacheRouting("auto");
|
||||||
setLocalCodexApiFormat(
|
setLocalCodexApiFormat(
|
||||||
codexApiFormatFromWireApi(extractCodexWireApi(template.config)) ??
|
codexApiFormatFromWireApi(extractCodexWireApi(template.config)) ??
|
||||||
"openai_responses",
|
"openai_responses",
|
||||||
@@ -1692,6 +1707,7 @@ function ProviderFormFull({
|
|||||||
|
|
||||||
resetCodexConfig(auth, config, preset.modelCatalog ?? []);
|
resetCodexConfig(auth, config, preset.modelCatalog ?? []);
|
||||||
setCodexChatReasoning(preset.codexChatReasoning ?? {});
|
setCodexChatReasoning(preset.codexChatReasoning ?? {});
|
||||||
|
setPromptCacheRouting(preset.promptCacheRouting ?? "auto");
|
||||||
setLocalCodexApiFormat(
|
setLocalCodexApiFormat(
|
||||||
preset.apiFormat ??
|
preset.apiFormat ??
|
||||||
codexApiFormatFromWireApi(extractCodexWireApi(config)) ??
|
codexApiFormatFromWireApi(extractCodexWireApi(config)) ??
|
||||||
@@ -2182,6 +2198,8 @@ function ProviderFormFull({
|
|||||||
onMaxOutputTokensChange={setLocalCodexMaxOutputTokens}
|
onMaxOutputTokensChange={setLocalCodexMaxOutputTokens}
|
||||||
codexChatReasoning={codexChatReasoning}
|
codexChatReasoning={codexChatReasoning}
|
||||||
onCodexChatReasoningChange={setCodexChatReasoning}
|
onCodexChatReasoningChange={setCodexChatReasoning}
|
||||||
|
promptCacheRouting={promptCacheRouting}
|
||||||
|
onPromptCacheRoutingChange={setPromptCacheRouting}
|
||||||
catalogModels={codexCatalogModels}
|
catalogModels={codexCatalogModels}
|
||||||
onCatalogModelsChange={setCodexCatalogModels}
|
onCatalogModelsChange={setCodexCatalogModels}
|
||||||
speedTestEndpoints={speedTestEndpoints}
|
speedTestEndpoints={speedTestEndpoints}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type {
|
|||||||
CodexApiFormat,
|
CodexApiFormat,
|
||||||
CodexCatalogModel,
|
CodexCatalogModel,
|
||||||
CodexChatReasoning,
|
CodexChatReasoning,
|
||||||
|
PromptCacheRoutingMode,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import type { PresetTheme } from "./claudeProviderPresets";
|
import type { PresetTheme } from "./claudeProviderPresets";
|
||||||
|
|
||||||
@@ -36,6 +37,8 @@ export interface CodexProviderPreset {
|
|||||||
modelCatalog?: CodexCatalogModel[];
|
modelCatalog?: CodexCatalogModel[];
|
||||||
// Codex Responses -> Chat Completions reasoning capability defaults
|
// Codex Responses -> Chat Completions reasoning capability defaults
|
||||||
codexChatReasoning?: CodexChatReasoning;
|
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"],
|
endpointCandidates: ["https://api.kimi.com/coding/v1"],
|
||||||
apiFormat: "openai_chat",
|
apiFormat: "openai_chat",
|
||||||
|
promptCacheRouting: "enabled",
|
||||||
modelCatalog: modelCatalog([
|
modelCatalog: modelCatalog([
|
||||||
{
|
{
|
||||||
model: "kimi-for-coding",
|
model: "kimi-for-coding",
|
||||||
|
|||||||
@@ -1284,6 +1284,11 @@
|
|||||||
"maxOutputTokensLabel": "Max output tokens",
|
"maxOutputTokensLabel": "Max output tokens",
|
||||||
"maxOutputTokensPlaceholder": "Leave empty to use the default 8192",
|
"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.",
|
"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",
|
"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.",
|
"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",
|
"modelNamePlaceholder": "e.g., gpt-5-codex",
|
||||||
|
|||||||
@@ -1284,6 +1284,11 @@
|
|||||||
"maxOutputTokensLabel": "最大出力トークン",
|
"maxOutputTokensLabel": "最大出力トークン",
|
||||||
"maxOutputTokensPlaceholder": "空欄の場合はデフォルトの 8192 を使用",
|
"maxOutputTokensPlaceholder": "空欄の場合はデフォルトの 8192 を使用",
|
||||||
"maxOutputTokensHint": "Codex は model_max_output_tokens をリクエストボディに含めないため、デフォルト上限 8192 では長い回答や深い思考時に切り詰められることがあります(stop_reason=max_tokens)。この値は Anthropic 経路で max_tokens を上書きします。モデル/ゲートウェイの実際の出力上限を超えると 400 になる場合があります。空欄でデフォルトの 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": "上流モデル名",
|
"upstreamModelName": "上流モデル名",
|
||||||
"upstreamModelNameHint": "Chat 形式ではここに実際の上流モデルを入力します。ルーティングで Codex の Responses リクエストを Chat Completions に変換し、このモデルを維持します。",
|
"upstreamModelNameHint": "Chat 形式ではここに実際の上流モデルを入力します。ルーティングで Codex の Responses リクエストを Chat Completions に変換し、このモデルを維持します。",
|
||||||
"modelNamePlaceholder": "例: gpt-5-codex",
|
"modelNamePlaceholder": "例: gpt-5-codex",
|
||||||
|
|||||||
@@ -1261,6 +1261,11 @@
|
|||||||
"maxOutputTokensLabel": "最大輸出 tokens",
|
"maxOutputTokensLabel": "最大輸出 tokens",
|
||||||
"maxOutputTokensPlaceholder": "留空則使用預設 8192",
|
"maxOutputTokensPlaceholder": "留空則使用預設 8192",
|
||||||
"maxOutputTokensHint": "Codex 不會把 model_max_output_tokens 寫進請求體,預設上限 8192 容易在長回答或深度思考時被截斷(stop_reason=max_tokens)。此處設定會作為 Anthropic 的 max_tokens 覆蓋請求值。請勿超過該模型/閘道的真實輸出上限,否則可能 400。留空使用預設 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": "預設模型",
|
"defaultModelLabel": "預設模型",
|
||||||
"defaultModelPlaceholder": "例如: gpt-5.6",
|
"defaultModelPlaceholder": "例如: gpt-5.6",
|
||||||
"defaultModelHint": "Codex 預設請求的模型,隨時可改,無需等待軟體更新供應商範本。留空且設定了模型映射時,預設使用映射第一行。",
|
"defaultModelHint": "Codex 預設請求的模型,隨時可改,無需等待軟體更新供應商範本。留空且設定了模型映射時,預設使用映射第一行。",
|
||||||
|
|||||||
@@ -1284,6 +1284,11 @@
|
|||||||
"maxOutputTokensLabel": "最大输出 tokens",
|
"maxOutputTokensLabel": "最大输出 tokens",
|
||||||
"maxOutputTokensPlaceholder": "留空则使用默认 8192",
|
"maxOutputTokensPlaceholder": "留空则使用默认 8192",
|
||||||
"maxOutputTokensHint": "Codex 不会把 model_max_output_tokens 写进请求体,默认上限 8192 容易在长回答或深度思考时被截断(stop_reason=max_tokens)。此处设置会作为 Anthropic 的 max_tokens 覆盖请求值。请勿超过该模型/网关的真实输出上限,否则可能 400。留空使用默认 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": "上游模型名称",
|
"upstreamModelName": "上游模型名称",
|
||||||
"upstreamModelNameHint": "Chat 格式下这里填写真实上游模型;路由会把 Codex 的 Responses 请求转换为 Chat Completions 并保持该模型。",
|
"upstreamModelNameHint": "Chat 格式下这里填写真实上游模型;路由会把 Codex 的 Responses 请求转换为 Chat Completions 并保持该模型。",
|
||||||
"modelNamePlaceholder": "例如: gpt-5-codex",
|
"modelNamePlaceholder": "例如: gpt-5-codex",
|
||||||
|
|||||||
@@ -161,6 +161,8 @@ export interface CodexChatReasoning {
|
|||||||
outputFormat?: CodexChatReasoningOutputFormat;
|
outputFormat?: CodexChatReasoningOutputFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PromptCacheRoutingMode = "auto" | "enabled" | "disabled";
|
||||||
|
|
||||||
export interface LocalProxyRequestOverrides {
|
export interface LocalProxyRequestOverrides {
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
body?: Record<string, unknown>;
|
body?: Record<string, unknown>;
|
||||||
@@ -206,6 +208,9 @@ export interface ProviderMeta {
|
|||||||
isFullUrl?: boolean;
|
isFullUrl?: boolean;
|
||||||
// Prompt cache key for OpenAI Responses-compatible endpoints (improves cache hit rate)
|
// Prompt cache key for OpenAI Responses-compatible endpoints (improves cache hit rate)
|
||||||
promptCacheKey?: string;
|
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
|
// Codex OAuth FAST mode: injects service_tier="priority" on ChatGPT Codex requests
|
||||||
codexFastMode?: boolean;
|
codexFastMode?: boolean;
|
||||||
// Codex Responses -> Chat Completions reasoning capability metadata
|
// Codex Responses -> Chat Completions reasoning capability metadata
|
||||||
|
|||||||
@@ -129,6 +129,14 @@ const expectedChatPresets = new Map<
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
describe("Codex Chat provider presets", () => {
|
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", () => {
|
it("marks migrated Chat Completions presets for local routing", () => {
|
||||||
for (const [name, expected] of expectedChatPresets) {
|
for (const [name, expected] of expectedChatPresets) {
|
||||||
const preset = codexProviderPresets.find((item) => item.name === name);
|
const preset = codexProviderPresets.find((item) => item.name === name);
|
||||||
|
|||||||
Reference in New Issue
Block a user