mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-30 10:25:05 +08:00
feat(codex): support native Anthropic Messages protocol as upstream (#5071)
* feat(codex): support native Anthropic Messages protocol as upstream Allow gateways that only expose the native Anthropic Messages protocol (/v1/messages) to be used by Codex: the local proxy performs bidirectional request/response/streaming conversion between Responses and Anthropic. Backend: - Add two conversion modules: transform_codex_anthropic / streaming_codex_anthropic - codex.rs: add routing detection and auth: ANTHROPIC_AUTH_TOKEN→Bearer (default), ANTHROPIC_API_KEY→x-api-key, mutually exclusive - handlers.rs: add handle_codex_anthropic_to_responses_transform - forwarder.rs: support optional Claude Code client fingerprint impersonation (User-Agent / anthropic-beta / x-app / system prompt first-line injection) and /responses→/v1/messages rewriting - codex_config: the anthropic format reuses the NativeResponses profile to strip custom tools - ProviderMeta: add impersonateClaudeCode Frontend: - CodexApiFormat: add "anthropic"; the form adds auth field selection and an impersonation toggle - Add en/ja/zh/zh-TW copy Robustness: - Downgrade when tool history / forced tool_choice conflicts with extended thinking, avoiding upstream 400s - Emit cache_creation_input_tokens in usage and use saturating_add to guard against overflow - Append a unique suffix to non-streaming/streaming output-item ids to avoid multi-segment text/thinking overwriting each other * fix(codex): harden Anthropic bridge against empty text blocks and truncated streams - Drop empty/whitespace-only text content blocks when rebuilding Anthropic messages from Responses history; Anthropic 400s on empty text blocks (e.g. an empty assistant text emitted alongside a tool_use), which broke follow-up and tool-result requests. Also drop messages left without content. - Do not report a truncated Anthropic stream as completed: when the SSE connection ends before message_stop with no stop_reason, emit an incomplete response if partial output exists, or a failed (stream_truncated) response otherwise, mirroring the chat converter's EOF handling. * fix(codex): resolve Anthropic-bridge review findings Blocking: 1. Defer stripping the [1m] long-context marker until after catalog matching and model write-back, re-stripping it on the final Codex→Anthropic body and setting a flag to emit the context-1m-2025-08-07 beta header, so the marker is no longer lost or overridden by the default model. 2. Gate Anthropic thinking on the trailing turn only (via trailing_turn_allows_thinking) instead of scanning full history, so a Codex session that resends history each round no longer permanently loses thinking after the first tool call. 3. Inject 5m ephemeral cache_control on the Codex→Anthropic body by reusing cache_injector (handling the system string→array conversion), so system/tools/history are cached instead of re-sent at full price every round. 4. Add a shared base_url_is_full_endpoint helper (normalizing whitespace/query/fragment/trailing slash) used by both the Anthropic and Chat paths, so a base URL already ending in /v1/messages is treated as a full endpoint instead of double-appending to /v1/messages/v1/messages. 5. Align the catalog tool-profile predicate with the routing predicate so resolve_codex_catalog_tool_profile returns the Anthropic profile whenever the request converts to Anthropic, preventing freeform tools like apply_patch from being silently filtered by a ProxyChat catalog. 6. Explicitly disable native web_search for the Anthropic profile (including the no-catalog branch) via set_codex_native_web_search_field, so Codex no longer treats it as available while the transform silently drops it. 7. Only forward tool_choice when tools survive filtering, dropping it otherwise, to avoid a non-retryable 400 from upstream when tool_choice is sent with no tools. 8. Lower the fallback default to max_tokens=8192 (only when max_output_tokens is omitted) and clamp the thinking budget to max_tokens/2, disabling thinking below the 1024 floor, to avoid hard 400s on low-output-ceiling models/gateways. Minor: 9. Centralize the Codex/OpenAI fingerprint-header denylist in is_codex_client_fingerprint_header so impersonating Claude Code uniformly drops originator/session_id/conversation_id/chatgpt-account-id/x-client-request-id/openai-* and the x-stainless-*/x-codex-* prefixes. 10. Retain content_block_start.input as start_input and fall back to it at block close when no input_json_delta arrived, so a gateway that carries the full tool input on the start event no longer yields empty tool arguments. 11. Extract a shared codex_responses_sse module as the single Responses SSE envelope builder that both the chat and anthropic streaming emitters delegate to, with byte-for-byte-unchanged wire output, so future event-format fixes touch one place instead of two. * fix(codex): add per-provider max_output_tokens override for Codex→Anthropic path Codex does not forward model_max_output_tokens in the request body, causing the proxy to fall back to a conservative 8192 default. This truncates long or thinking-heavy responses (stop_reason=max_tokens). - Add maxOutputTokens field to ProviderMeta (Rust + TypeScript) - Inject the value into the Anthropic request body before transform, taking precedence over request-supplied and default values - Add numeric input in Codex form fields (Anthropic format only) - Add i18n entries for label, placeholder, and hint (en/zh/zh-TW/ja) - Include roundtrip and omission unit tests for the new field * fix(codex): harden Anthropic protocol bridge * fix(codex): address Anthropic bridge review * fix(codex): preserve flattened Anthropic inputs * fix(codex): harden Anthropic recovery paths * fix(ci): satisfy Rust clippy --------- Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
@@ -22,6 +22,7 @@ import {
|
||||
extractCodexBaseUrl,
|
||||
extractCodexExperimentalBearerToken,
|
||||
extractCodexWireApi,
|
||||
isCodexAnthropicWireApi,
|
||||
isCodexChatWireApi,
|
||||
} from "@/utils/providerConfigUtils";
|
||||
import { useProviderHealth } from "@/lib/query/failover";
|
||||
@@ -220,11 +221,16 @@ export function ProviderCard({
|
||||
provider.meta?.providerType === PROVIDER_TYPES.CODEX_OAUTH;
|
||||
const codexNeedsRouting = useMemo(() => {
|
||||
if (appId !== "codex" || provider.category === "official") return false;
|
||||
if (provider.meta?.apiFormat === "openai_chat") return true;
|
||||
if (
|
||||
provider.meta?.apiFormat === "openai_chat" ||
|
||||
provider.meta?.apiFormat === "anthropic"
|
||||
)
|
||||
return true;
|
||||
const config = (provider.settingsConfig as Record<string, any>)?.config;
|
||||
return (
|
||||
typeof config === "string" &&
|
||||
isCodexChatWireApi(extractCodexWireApi(config))
|
||||
(isCodexChatWireApi(extractCodexWireApi(config)) ||
|
||||
isCodexAnthropicWireApi(extractCodexWireApi(config)))
|
||||
);
|
||||
}, [
|
||||
appId,
|
||||
|
||||
@@ -36,6 +36,7 @@ import { CustomUserAgentField } from "./CustomUserAgentField";
|
||||
import { LocalProxyRequestOverridesField } from "./LocalProxyRequestOverridesField";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type {
|
||||
ClaudeApiKeyField,
|
||||
CodexApiFormat,
|
||||
CodexCatalogModel,
|
||||
CodexChatReasoning,
|
||||
@@ -73,6 +74,15 @@ interface CodexFormFieldsProps {
|
||||
// Note: wire_api is always "responses" for Codex; apiFormat controls proxy-layer conversion
|
||||
apiFormat: CodexApiFormat;
|
||||
onApiFormatChange: (format: CodexApiFormat) => void;
|
||||
// Auth field for the Anthropic Messages upstream (only used when apiFormat === "anthropic")
|
||||
anthropicAuthField: ClaudeApiKeyField;
|
||||
onAnthropicAuthFieldChange: (value: ClaudeApiKeyField) => void;
|
||||
// Anthropic path: whether to emulate the Claude Code client
|
||||
impersonateClaudeCode: boolean;
|
||||
onImpersonateClaudeCodeChange: (value: boolean) => void;
|
||||
// Anthropic path: output ceiling override (empty string = use default). Digits only.
|
||||
maxOutputTokens: string;
|
||||
onMaxOutputTokensChange: (value: string) => void;
|
||||
codexChatReasoning?: CodexChatReasoning;
|
||||
onCodexChatReasoningChange?: (value: CodexChatReasoning) => void;
|
||||
|
||||
@@ -158,6 +168,12 @@ export function CodexFormFields({
|
||||
onAutoSelectChange,
|
||||
apiFormat,
|
||||
onApiFormatChange,
|
||||
anthropicAuthField,
|
||||
onAnthropicAuthFieldChange,
|
||||
impersonateClaudeCode,
|
||||
onImpersonateClaudeCodeChange,
|
||||
maxOutputTokens,
|
||||
onMaxOutputTokensChange,
|
||||
codexChatReasoning = {},
|
||||
onCodexChatReasoningChange,
|
||||
catalogModels = [],
|
||||
@@ -177,6 +193,7 @@ export function CodexFormFields({
|
||||
// 思考能力随 Chat 格式显示(仅 Chat Completions 转换路径用得上);模型映射常驻
|
||||
//(填了才生成 catalog)。两者都已与「路由接管」概念解耦。
|
||||
const isChatFormat = apiFormat === "openai_chat";
|
||||
const isAnthropicFormat = apiFormat === "anthropic";
|
||||
const canEditCatalog = Boolean(onCatalogModelsChange);
|
||||
const canEditReasoning = Boolean(onCodexChatReasoningChange);
|
||||
const supportsThinking =
|
||||
@@ -194,8 +211,10 @@ export function CodexFormFields({
|
||||
hasRequestOverrides ||
|
||||
catalogModels.length > 0 ||
|
||||
apiFormat === "openai_responses" ||
|
||||
isAnthropicFormat ||
|
||||
supportsThinking ||
|
||||
supportsEffort;
|
||||
supportsEffort ||
|
||||
!!maxOutputTokens;
|
||||
const [advancedExpanded, setAdvancedExpanded] = useState(hasAnyAdvancedValue);
|
||||
|
||||
// 预设/编辑加载填充高级值后自动展开(仅从折叠→展开,不会自动折叠)
|
||||
@@ -449,15 +468,118 @@ export function CodexFormFields({
|
||||
defaultValue: "Responses(原生)",
|
||||
})}
|
||||
</SelectItem>
|
||||
<SelectItem value="anthropic">
|
||||
{t("codexConfig.upstreamFormatAnthropic", {
|
||||
defaultValue: "Anthropic Messages(需开启路由)",
|
||||
})}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs leading-relaxed text-muted-foreground">
|
||||
{t("codexConfig.upstreamFormatHint", {
|
||||
defaultValue:
|
||||
"供应商原生是 Responses API 就选 Responses(直连,不转换格式);使用 Chat Completions 协议就选 Chat(需开启路由接管才能转换为 Chat Completions)。",
|
||||
"供应商原生是 Responses API 就选 Responses(直连,不转换格式);使用 Chat Completions 协议就选 Chat;供应商只提供原生 Anthropic Messages 协议就选 Anthropic Messages。Chat 与 Anthropic Messages 均需开启路由接管才能转换为 Responses。",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isAnthropicFormat && (
|
||||
<div className="space-y-1.5">
|
||||
<FormLabel htmlFor="codex-anthropic-auth-field">
|
||||
{t("codexConfig.anthropicAuthFieldLabel", {
|
||||
defaultValue: "认证字段",
|
||||
})}
|
||||
</FormLabel>
|
||||
<Select
|
||||
value={anthropicAuthField}
|
||||
onValueChange={(value) =>
|
||||
onAnthropicAuthFieldChange(value as ClaudeApiKeyField)
|
||||
}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="codex-anthropic-auth-field"
|
||||
className="w-full"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ANTHROPIC_AUTH_TOKEN">
|
||||
{t("codexConfig.anthropicAuthFieldAuthToken", {
|
||||
defaultValue:
|
||||
"ANTHROPIC_AUTH_TOKEN(Authorization)",
|
||||
})}
|
||||
</SelectItem>
|
||||
<SelectItem value="ANTHROPIC_API_KEY">
|
||||
{t("codexConfig.anthropicAuthFieldApiKey", {
|
||||
defaultValue: "ANTHROPIC_API_KEY(x-api-key)",
|
||||
})}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs leading-relaxed text-muted-foreground">
|
||||
{t("codexConfig.anthropicAuthFieldHint", {
|
||||
defaultValue:
|
||||
"选择网关接收 API Key 的请求头:ANTHROPIC_AUTH_TOKEN 发送 Authorization: Bearer;ANTHROPIC_API_KEY 发送 x-api-key。两者只发其一。",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAnthropicFormat && (
|
||||
<div className="flex items-center justify-between gap-4 border-t border-border-default pt-3">
|
||||
<div className="space-y-1">
|
||||
<FormLabel>
|
||||
{t("codexConfig.impersonateClaudeCodeLabel", {
|
||||
defaultValue: "模拟 Claude Code 客户端",
|
||||
})}
|
||||
</FormLabel>
|
||||
<p className="text-xs leading-relaxed text-muted-foreground">
|
||||
{t("codexConfig.impersonateClaudeCodeHint", {
|
||||
defaultValue:
|
||||
"网关或其上游限制只能通过 Claude Code 使用时开启:伪装 User-Agent、anthropic-beta、x-app 请求头,并在系统提示首行注入 Claude Code 身份。",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={impersonateClaudeCode}
|
||||
onCheckedChange={onImpersonateClaudeCodeChange}
|
||||
aria-label={t("codexConfig.impersonateClaudeCodeLabel", {
|
||||
defaultValue: "模拟 Claude Code 客户端",
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAnthropicFormat && (
|
||||
<div className="space-y-1.5 border-t border-border-default pt-3">
|
||||
<FormLabel htmlFor="codex-anthropic-max-output-tokens">
|
||||
{t("codexConfig.maxOutputTokensLabel", {
|
||||
defaultValue: "最大输出 tokens",
|
||||
})}
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="codex-anthropic-max-output-tokens"
|
||||
type="number"
|
||||
min={1}
|
||||
inputMode="numeric"
|
||||
value={maxOutputTokens}
|
||||
onChange={(event) =>
|
||||
onMaxOutputTokensChange(
|
||||
event.target.value.replace(/[^\d]/g, ""),
|
||||
)
|
||||
}
|
||||
placeholder={t("codexConfig.maxOutputTokensPlaceholder", {
|
||||
defaultValue: "留空则使用默认 8192",
|
||||
})}
|
||||
/>
|
||||
<p className="text-xs leading-relaxed text-muted-foreground">
|
||||
{t("codexConfig.maxOutputTokensHint", {
|
||||
defaultValue:
|
||||
"Codex 不会把 model_max_output_tokens 写进请求体,默认上限 8192 容易在长回答或深度思考时被截断(stop_reason=max_tokens)。此处设置会作为 Anthropic 的 max_tokens 覆盖请求值。请勿超过该模型/网关的真实输出上限,否则可能 400。留空使用默认 8192。",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ import {
|
||||
} from "@/utils/providerConfigUtils";
|
||||
import { mergeProviderMeta } from "@/utils/providerMetaUtils";
|
||||
import {
|
||||
codexApiFormatFromWireApi,
|
||||
extractCodexWireApi,
|
||||
setCodexWireApi,
|
||||
setCodexModelName as setCodexModelNameInConfig,
|
||||
@@ -131,25 +132,6 @@ type PresetEntry = {
|
||||
| HermesProviderPreset;
|
||||
};
|
||||
|
||||
const codexApiFormatFromWireApi = (
|
||||
wireApi: string | undefined,
|
||||
): CodexApiFormat | undefined => {
|
||||
switch (wireApi?.trim().toLowerCase()) {
|
||||
case "chat":
|
||||
case "chat_completions":
|
||||
case "chat-completions":
|
||||
case "openai_chat":
|
||||
case "openai-chat":
|
||||
return "openai_chat";
|
||||
case "responses":
|
||||
case "openai_responses":
|
||||
case "openai-responses":
|
||||
return "openai_responses";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const normalizeCodexCatalogModelsForSave = (
|
||||
models: CodexCatalogModel[],
|
||||
): CodexCatalogModel[] => {
|
||||
@@ -586,19 +568,43 @@ function ProviderFormFull({
|
||||
const initialCodexApiFormat: CodexApiFormat =
|
||||
initialData?.meta?.apiFormat === "openai_chat"
|
||||
? "openai_chat"
|
||||
: initialData?.meta?.apiFormat === "openai_responses"
|
||||
? "openai_responses"
|
||||
: (codexApiFormatFromWireApi(
|
||||
extractCodexWireApi(
|
||||
typeof initialData?.settingsConfig?.config === "string"
|
||||
? initialData.settingsConfig.config
|
||||
: "",
|
||||
),
|
||||
) ?? "openai_responses");
|
||||
: initialData?.meta?.apiFormat === "anthropic"
|
||||
? "anthropic"
|
||||
: initialData?.meta?.apiFormat === "openai_responses"
|
||||
? "openai_responses"
|
||||
: (codexApiFormatFromWireApi(
|
||||
extractCodexWireApi(
|
||||
typeof initialData?.settingsConfig?.config === "string"
|
||||
? initialData.settingsConfig.config
|
||||
: "",
|
||||
),
|
||||
) ?? "openai_responses");
|
||||
|
||||
const [localCodexApiFormat, setLocalCodexApiFormat] =
|
||||
useState<CodexApiFormat>(initialCodexApiFormat);
|
||||
|
||||
// Auth-field choice for the Anthropic Messages upstream (defaults to the Bearer form)
|
||||
const initialCodexAnthropicAuthField: ClaudeApiKeyField =
|
||||
initialData?.meta?.apiKeyField === "ANTHROPIC_API_KEY"
|
||||
? "ANTHROPIC_API_KEY"
|
||||
: "ANTHROPIC_AUTH_TOKEN";
|
||||
const [localCodexAnthropicAuthField, setLocalCodexAnthropicAuthField] =
|
||||
useState<ClaudeApiKeyField>(initialCodexAnthropicAuthField);
|
||||
|
||||
// Emulate the Claude Code client: off by default, enabled only when the user explicitly turns it on (true)
|
||||
const [localCodexImpersonateClaudeCode, setLocalCodexImpersonateClaudeCode] =
|
||||
useState<boolean>(initialData?.meta?.impersonateClaudeCode === true);
|
||||
|
||||
// Codex → Anthropic output ceiling override (empty string = use the 8192 default).
|
||||
// Kept as a string so the numeric input can be cleared; parsed on save.
|
||||
const [localCodexMaxOutputTokens, setLocalCodexMaxOutputTokens] =
|
||||
useState<string>(
|
||||
typeof initialData?.meta?.maxOutputTokens === "number" &&
|
||||
initialData.meta.maxOutputTokens > 0
|
||||
? String(initialData.meta.maxOutputTokens)
|
||||
: "",
|
||||
);
|
||||
|
||||
const { configError: codexConfigError, debouncedValidate } =
|
||||
useCodexTomlValidation();
|
||||
|
||||
@@ -1502,6 +1508,28 @@ function ProviderFormFull({
|
||||
category !== "official" &&
|
||||
localApiKeyField !== "ANTHROPIC_AUTH_TOKEN"
|
||||
? localApiKeyField
|
||||
: appId === "codex" &&
|
||||
category !== "official" &&
|
||||
localCodexApiFormat === "anthropic" &&
|
||||
localCodexAnthropicAuthField !== "ANTHROPIC_AUTH_TOKEN"
|
||||
? localCodexAnthropicAuthField
|
||||
: undefined,
|
||||
// Off by default; persist true only for codex+anthropic when the user explicitly enables it
|
||||
impersonateClaudeCode:
|
||||
appId === "codex" &&
|
||||
category !== "official" &&
|
||||
localCodexApiFormat === "anthropic" &&
|
||||
localCodexImpersonateClaudeCode
|
||||
? true
|
||||
: undefined,
|
||||
// Persist only for codex+anthropic when a positive value was entered
|
||||
maxOutputTokens:
|
||||
appId === "codex" &&
|
||||
category !== "official" &&
|
||||
localCodexApiFormat === "anthropic" &&
|
||||
localCodexMaxOutputTokens.trim() !== "" &&
|
||||
Number(localCodexMaxOutputTokens) > 0
|
||||
? Number(localCodexMaxOutputTokens)
|
||||
: undefined,
|
||||
isFullUrl:
|
||||
supportsFullUrl && category !== "official" && localIsFullUrl
|
||||
@@ -2142,6 +2170,12 @@ function ProviderFormFull({
|
||||
onAutoSelectChange={setEndpointAutoSelect}
|
||||
apiFormat={localCodexApiFormat}
|
||||
onApiFormatChange={handleCodexApiFormatChange}
|
||||
anthropicAuthField={localCodexAnthropicAuthField}
|
||||
onAnthropicAuthFieldChange={setLocalCodexAnthropicAuthField}
|
||||
impersonateClaudeCode={localCodexImpersonateClaudeCode}
|
||||
onImpersonateClaudeCodeChange={setLocalCodexImpersonateClaudeCode}
|
||||
maxOutputTokens={localCodexMaxOutputTokens}
|
||||
onMaxOutputTokensChange={setLocalCodexMaxOutputTokens}
|
||||
codexChatReasoning={codexChatReasoning}
|
||||
onCodexChatReasoningChange={setCodexChatReasoning}
|
||||
catalogModels={codexCatalogModels}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { extractErrorMessage } from "@/utils/errorUtils";
|
||||
import { openclawKeys } from "@/hooks/useOpenClaw";
|
||||
import {
|
||||
extractCodexWireApi,
|
||||
isCodexAnthropicWireApi,
|
||||
isCodexChatWireApi,
|
||||
} from "@/utils/providerConfigUtils";
|
||||
|
||||
@@ -165,6 +166,16 @@ export function useProviderActions(
|
||||
(provider.settingsConfig as Record<string, any>).config,
|
||||
),
|
||||
)));
|
||||
const isCodexAnthropicFormat =
|
||||
activeApp === "codex" &&
|
||||
(provider.meta?.apiFormat === "anthropic" ||
|
||||
(typeof (provider.settingsConfig as Record<string, any>)?.config ===
|
||||
"string" &&
|
||||
isCodexAnthropicWireApi(
|
||||
extractCodexWireApi(
|
||||
(provider.settingsConfig as Record<string, any>).config,
|
||||
),
|
||||
)));
|
||||
|
||||
// Determine why this provider requires the proxy
|
||||
let proxyRequiredReason: string | null = null;
|
||||
@@ -191,6 +202,13 @@ export function useProviderActions(
|
||||
proxyRequiredReason = t("notifications.proxyReasonOpenAIChat", {
|
||||
defaultValue: "使用 OpenAI Chat 接口格式",
|
||||
});
|
||||
} else if (isCodexAnthropicFormat) {
|
||||
proxyRequiredReason = t(
|
||||
"notifications.proxyReasonAnthropicMessages",
|
||||
{
|
||||
defaultValue: "使用 Anthropic Messages 接口格式",
|
||||
},
|
||||
);
|
||||
} else if (
|
||||
activeApp === "claude-desktop" &&
|
||||
provider.meta?.claudeDesktopMode === "proxy"
|
||||
|
||||
@@ -1283,9 +1283,19 @@
|
||||
"modelNameHint": "Specify the model to use, will be auto-updated in config.toml",
|
||||
"modelName": "Model Name",
|
||||
"upstreamFormatLabel": "Upstream Format",
|
||||
"upstreamFormatHint": "Pick Responses when your provider is natively a Responses API (direct, no format conversion); pick Chat when it uses the Chat Completions protocol (requires routing takeover to convert to Chat Completions).",
|
||||
"upstreamFormatHint": "Pick Responses when your provider is natively a Responses API (direct, no format conversion); pick Chat when it uses the Chat Completions protocol; pick Anthropic Messages when it only offers the native Anthropic Messages protocol. Chat and Anthropic Messages both require routing takeover to convert to Responses.",
|
||||
"upstreamFormatChat": "Chat Completions (routing required)",
|
||||
"upstreamFormatResponses": "Responses (native)",
|
||||
"upstreamFormatAnthropic": "Anthropic Messages (routing required)",
|
||||
"anthropicAuthFieldLabel": "Auth field",
|
||||
"anthropicAuthFieldAuthToken": "ANTHROPIC_AUTH_TOKEN (Authorization)",
|
||||
"anthropicAuthFieldApiKey": "ANTHROPIC_API_KEY (x-api-key)",
|
||||
"anthropicAuthFieldHint": "Choose which header carries the API key to the gateway: ANTHROPIC_AUTH_TOKEN sends Authorization: Bearer; ANTHROPIC_API_KEY sends x-api-key. Only one is sent.",
|
||||
"impersonateClaudeCodeLabel": "Emulate Claude Code client",
|
||||
"impersonateClaudeCodeHint": "Enable when the gateway (or its upstream) restricts usage to Claude Code: spoofs the User-Agent, anthropic-beta and x-app headers, and injects the Claude Code identity as the first system prompt line.",
|
||||
"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.",
|
||||
"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",
|
||||
|
||||
@@ -1283,9 +1283,19 @@
|
||||
"modelNameHint": "使用するモデルを指定します。config.toml に自動更新されます",
|
||||
"modelName": "モデル名",
|
||||
"upstreamFormatLabel": "上流フォーマット",
|
||||
"upstreamFormatHint": "プロバイダーがネイティブ Responses API なら Responses を選択(直結、フォーマット変換なし)。Chat Completions プロトコルなら Chat を選択(Chat Completions へ変換するにはルーティング引き継ぎを有効化する必要があります)。",
|
||||
"upstreamFormatHint": "プロバイダーがネイティブ Responses API なら Responses を選択(直結、フォーマット変換なし)。Chat Completions プロトコルなら Chat を選択。ネイティブ Anthropic Messages プロトコルのみ提供する場合は Anthropic Messages を選択。Chat と Anthropic Messages はどちらも Responses への変換にルーティング引き継ぎの有効化が必要です。",
|
||||
"upstreamFormatChat": "Chat Completions(ルーティング必須)",
|
||||
"upstreamFormatResponses": "Responses(ネイティブ)",
|
||||
"upstreamFormatAnthropic": "Anthropic Messages(ルーティング必須)",
|
||||
"anthropicAuthFieldLabel": "認証フィールド",
|
||||
"anthropicAuthFieldAuthToken": "ANTHROPIC_AUTH_TOKEN(Authorization)",
|
||||
"anthropicAuthFieldApiKey": "ANTHROPIC_API_KEY(x-api-key)",
|
||||
"anthropicAuthFieldHint": "ゲートウェイに API キーを渡すヘッダーを選択します。ANTHROPIC_AUTH_TOKEN は Authorization: Bearer、ANTHROPIC_API_KEY は x-api-key を送信します。送信されるのはどちらか一方のみです。",
|
||||
"impersonateClaudeCodeLabel": "Claude Code クライアントを模倣",
|
||||
"impersonateClaudeCodeHint": "ゲートウェイ(またはその上流)が Claude Code のみに制限している場合に有効化します。User-Agent・anthropic-beta・x-app ヘッダーを偽装し、システムプロンプトの先頭行に Claude Code のアイデンティティを注入します。",
|
||||
"maxOutputTokensLabel": "最大出力トークン",
|
||||
"maxOutputTokensPlaceholder": "空欄の場合はデフォルトの 8192 を使用",
|
||||
"maxOutputTokensHint": "Codex は model_max_output_tokens をリクエストボディに含めないため、デフォルト上限 8192 では長い回答や深い思考時に切り詰められることがあります(stop_reason=max_tokens)。この値は Anthropic 経路で max_tokens を上書きします。モデル/ゲートウェイの実際の出力上限を超えると 400 になる場合があります。空欄でデフォルトの 8192 を使用します。",
|
||||
"upstreamModelName": "上流モデル名",
|
||||
"upstreamModelNameHint": "Chat 形式ではここに実際の上流モデルを入力します。ルーティングで Codex の Responses リクエストを Chat Completions に変換し、このモデルを維持します。",
|
||||
"modelNamePlaceholder": "例: gpt-5-codex",
|
||||
|
||||
@@ -1260,9 +1260,19 @@
|
||||
"autoCompactLimitHint": "上下文 token 數達到此閾值時自動壓縮歷史",
|
||||
"autoCompactLimitPlaceholder": "例如: 90000",
|
||||
"upstreamFormatLabel": "上游格式",
|
||||
"upstreamFormatHint": "供應商原生為 Responses API 就選 Responses(直連,不轉換格式);使用 Chat Completions 協定就選 Chat(需開啟路由接管才能轉換為 Chat Completions)。",
|
||||
"upstreamFormatHint": "供應商原生為 Responses API 就選 Responses(直連,不轉換格式);使用 Chat Completions 協定就選 Chat;供應商只提供原生 Anthropic Messages 協定就選 Anthropic Messages。Chat 與 Anthropic Messages 均需開啟路由接管才能轉換為 Responses。",
|
||||
"upstreamFormatChat": "Chat Completions(需開啟路由)",
|
||||
"upstreamFormatResponses": "Responses(原生)",
|
||||
"upstreamFormatAnthropic": "Anthropic Messages(需開啟路由)",
|
||||
"anthropicAuthFieldLabel": "認證欄位",
|
||||
"anthropicAuthFieldAuthToken": "ANTHROPIC_AUTH_TOKEN(Authorization)",
|
||||
"anthropicAuthFieldApiKey": "ANTHROPIC_API_KEY(x-api-key)",
|
||||
"anthropicAuthFieldHint": "選擇閘道接收 API Key 的請求標頭:ANTHROPIC_AUTH_TOKEN 傳送 Authorization: Bearer;ANTHROPIC_API_KEY 傳送 x-api-key。兩者只傳其一。",
|
||||
"impersonateClaudeCodeLabel": "模擬 Claude Code 用戶端",
|
||||
"impersonateClaudeCodeHint": "閘道或其上游限制只能透過 Claude Code 使用時開啟:偽裝 User-Agent、anthropic-beta、x-app 請求標頭,並在系統提示首行注入 Claude Code 身分。",
|
||||
"maxOutputTokensLabel": "最大輸出 tokens",
|
||||
"maxOutputTokensPlaceholder": "留空則使用預設 8192",
|
||||
"maxOutputTokensHint": "Codex 不會把 model_max_output_tokens 寫進請求體,預設上限 8192 容易在長回答或深度思考時被截斷(stop_reason=max_tokens)。此處設定會作為 Anthropic 的 max_tokens 覆蓋請求值。請勿超過該模型/閘道的真實輸出上限,否則可能 400。留空使用預設 8192。",
|
||||
"modelMappingTitle": "模型映射",
|
||||
"modelMappingHint": "產生 Codex model_catalog_json,讓 /model 指令顯示這些第三方模型名;表中條目按填寫內容原樣儲存。修改後需要重新啟動 Codex 才能重新整理模型清單。",
|
||||
"addCatalogModel": "新增模型",
|
||||
|
||||
@@ -1283,9 +1283,19 @@
|
||||
"modelNameHint": "指定使用的模型,将自动更新到 config.toml 中",
|
||||
"modelName": "模型名称",
|
||||
"upstreamFormatLabel": "上游格式",
|
||||
"upstreamFormatHint": "供应商原生为 Responses API 就选 Responses(直连,不转换格式);使用 Chat Completions 协议就选 Chat(需开启路由接管才能转换为 Chat Completions)。",
|
||||
"upstreamFormatHint": "供应商原生为 Responses API 就选 Responses(直连,不转换格式);使用 Chat Completions 协议就选 Chat;供应商只提供原生 Anthropic Messages 协议就选 Anthropic Messages。Chat 与 Anthropic Messages 均需开启路由接管才能转换为 Responses。",
|
||||
"upstreamFormatChat": "Chat Completions(需开启路由)",
|
||||
"upstreamFormatResponses": "Responses(原生)",
|
||||
"upstreamFormatAnthropic": "Anthropic Messages(需开启路由)",
|
||||
"anthropicAuthFieldLabel": "认证字段",
|
||||
"anthropicAuthFieldAuthToken": "ANTHROPIC_AUTH_TOKEN(Authorization)",
|
||||
"anthropicAuthFieldApiKey": "ANTHROPIC_API_KEY(x-api-key)",
|
||||
"anthropicAuthFieldHint": "选择网关接收 API Key 的请求头:ANTHROPIC_AUTH_TOKEN 发送 Authorization: Bearer;ANTHROPIC_API_KEY 发送 x-api-key。两者只发其一。",
|
||||
"impersonateClaudeCodeLabel": "模拟 Claude Code 客户端",
|
||||
"impersonateClaudeCodeHint": "网关或其上游限制只能通过 Claude Code 使用时开启:伪装 User-Agent、anthropic-beta、x-app 请求头,并在系统提示首行注入 Claude Code 身份。",
|
||||
"maxOutputTokensLabel": "最大输出 tokens",
|
||||
"maxOutputTokensPlaceholder": "留空则使用默认 8192",
|
||||
"maxOutputTokensHint": "Codex 不会把 model_max_output_tokens 写进请求体,默认上限 8192 容易在长回答或深度思考时被截断(stop_reason=max_tokens)。此处设置会作为 Anthropic 的 max_tokens 覆盖请求值。请勿超过该模型/网关的真实输出上限,否则可能 400。留空使用默认 8192。",
|
||||
"upstreamModelName": "上游模型名称",
|
||||
"upstreamModelNameHint": "Chat 格式下这里填写真实上游模型;路由会把 Codex 的 Responses 请求转换为 Chat Completions 并保持该模型。",
|
||||
"modelNamePlaceholder": "例如: gpt-5-codex",
|
||||
|
||||
+10
-1
@@ -224,6 +224,14 @@ export interface ProviderMeta {
|
||||
codexFastMode?: boolean;
|
||||
// Codex Responses -> Chat Completions reasoning capability metadata
|
||||
codexChatReasoning?: CodexChatReasoning;
|
||||
// Codex → Anthropic path: emulate the Claude Code client (disabled by default; only an explicit true enables it)
|
||||
impersonateClaudeCode?: boolean;
|
||||
// Codex → Anthropic path: override the Anthropic max_tokens (output ceiling).
|
||||
// Codex does not forward model_max_output_tokens in the request body; without
|
||||
// this the path falls back to a conservative 8192 default, which can truncate
|
||||
// long/thinking-heavy responses. When set (>0) it takes precedence over the
|
||||
// request value and the default.
|
||||
maxOutputTokens?: number;
|
||||
// Custom User-Agent for local proxy routing. Only applied by the local proxy.
|
||||
customUserAgent?: string;
|
||||
// Local proxy request overrides. Only applied by the local proxy after route transforms.
|
||||
@@ -254,7 +262,8 @@ export type ClaudeApiFormat =
|
||||
// Codex API 格式类型
|
||||
// - "openai_responses": OpenAI Responses API 格式,直接透传
|
||||
// - "openai_chat": OpenAI Chat Completions 格式,需要本地路由转换
|
||||
export type CodexApiFormat = "openai_responses" | "openai_chat";
|
||||
// - "anthropic": native Anthropic Messages format, needs local routing to convert to Responses
|
||||
export type CodexApiFormat = "openai_responses" | "openai_chat" | "anthropic";
|
||||
|
||||
export interface CodexCatalogModel {
|
||||
model: string;
|
||||
|
||||
@@ -1,9 +1,35 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
codexApiFormatFromWireApi,
|
||||
isCodexAnthropicWireApi,
|
||||
isCodexRemoteCompactionEnabled,
|
||||
setCodexRemoteCompaction,
|
||||
} from "./providerConfigUtils";
|
||||
|
||||
describe("Codex wire API helpers", () => {
|
||||
it("recognizes Anthropic Messages aliases", () => {
|
||||
expect(isCodexAnthropicWireApi("anthropic")).toBe(true);
|
||||
expect(isCodexAnthropicWireApi("anthropic_messages")).toBe(true);
|
||||
expect(isCodexAnthropicWireApi("messages")).toBe(true);
|
||||
expect(isCodexAnthropicWireApi("claude")).toBe(true);
|
||||
expect(isCodexAnthropicWireApi("responses")).toBe(false);
|
||||
});
|
||||
|
||||
it("maps every backend-supported Anthropic alias to the form format", () => {
|
||||
for (const wireApi of [
|
||||
"anthropic",
|
||||
"anthropic_messages",
|
||||
"anthropic-messages",
|
||||
"messages",
|
||||
"claude",
|
||||
]) {
|
||||
expect(codexApiFormatFromWireApi(wireApi)).toBe("anthropic");
|
||||
}
|
||||
expect(codexApiFormatFromWireApi("responses")).toBe("openai_responses");
|
||||
expect(codexApiFormatFromWireApi("chat_completions")).toBe("openai_chat");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Codex remote compaction config helpers", () => {
|
||||
it("enables remote compaction by naming the active custom provider OpenAI", () => {
|
||||
const input = `model_provider = "custom"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// 供应商配置处理工具函数
|
||||
|
||||
import type { TemplateValueConfig } from "../config/claudeProviderPresets";
|
||||
import type { CodexApiFormat } from "@/types";
|
||||
import { deepClone } from "@/utils/deepClone";
|
||||
import { normalizeTomlText } from "@/utils/textNormalization";
|
||||
import { parse as parseToml } from "smol-toml";
|
||||
@@ -683,6 +684,32 @@ export const isCodexChatWireApi = (
|
||||
): boolean =>
|
||||
CODEX_CHAT_WIRE_API_VALUES.has((wireApi ?? "").trim().toLowerCase());
|
||||
|
||||
export const isCodexAnthropicWireApi = (
|
||||
wireApi: string | undefined | null,
|
||||
): boolean =>
|
||||
[
|
||||
"anthropic",
|
||||
"anthropic_messages",
|
||||
"anthropic-messages",
|
||||
"messages",
|
||||
"claude",
|
||||
].includes((wireApi ?? "").trim().toLowerCase());
|
||||
|
||||
export const codexApiFormatFromWireApi = (
|
||||
wireApi: string | undefined | null,
|
||||
): CodexApiFormat | undefined => {
|
||||
if (isCodexChatWireApi(wireApi)) return "openai_chat";
|
||||
if (isCodexAnthropicWireApi(wireApi)) return "anthropic";
|
||||
switch ((wireApi ?? "").trim().toLowerCase()) {
|
||||
case "responses":
|
||||
case "openai_responses":
|
||||
case "openai-responses":
|
||||
return "openai_responses";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// 从 Codex 的 TOML 配置文本中提取 wire_api(支持单/双引号)
|
||||
export const extractCodexWireApi = (
|
||||
configText: string | undefined | null,
|
||||
|
||||
Reference in New Issue
Block a user