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:
Jason
2026-07-11 21:42:43 +08:00
parent 0e563b50c5
commit a078b4b207
14 changed files with 295 additions and 4 deletions
@@ -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",
)}
>
<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">
<FormLabel>
{t("codexConfig.reasoningGroupTitle", {
@@ -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<CodexChatReasoning>(
() => initialData?.meta?.codexChatReasoning ?? {},
);
const [promptCacheRouting, setPromptCacheRouting] =
useState<PromptCacheRoutingMode>(
() => initialData?.meta?.promptCacheRouting ?? "auto",
);
const [customUserAgent, setCustomUserAgent] = useState<string>(
() => 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}