Add Chat Completions routing for Codex providers

- Add a Codex API format selector and routing badge for Chat Completions providers.
- Convert Codex Responses requests to upstream Chat Completions when routing is required.
- Convert Chat Completions JSON and SSE responses back to Responses format.
- Keep generated Codex wire_api values on Responses for Codex compatibility.
- Add i18n labels, provider metadata handling, and focused conversion tests.
This commit is contained in:
Jason
2026-05-19 10:23:48 +08:00
parent 61e68d754c
commit 1c82b8a3fa
19 changed files with 2230 additions and 35 deletions
@@ -14,6 +14,7 @@ import type {
ProviderMeta,
ProviderTestConfig,
ClaudeApiFormat,
CodexApiFormat,
ClaudeApiKeyField,
} from "@/types";
import {
@@ -51,6 +52,10 @@ import {
hasApiKeyField,
} from "@/utils/providerConfigUtils";
import { mergeProviderMeta } from "@/utils/providerMetaUtils";
import {
extractCodexWireApi,
setCodexWireApi,
} from "@/utils/providerConfigUtils";
import { isNonNegativeDecimalString } from "@/types/usage";
import { getCodexCustomTemplate } from "@/config/codexTemplates";
import CodexConfigEditor from "./CodexConfigEditor";
@@ -118,6 +123,25 @@ 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 interface ProviderFormProps {
appId: AppId;
providerId?: string;
@@ -419,6 +443,7 @@ function ProviderFormFull({
codexModelName,
codexAuthError,
setCodexAuth,
setCodexConfig,
handleCodexApiKeyChange,
handleCodexBaseUrlChange,
handleCodexModelNameChange,
@@ -426,17 +451,52 @@ function ProviderFormFull({
resetCodexConfig,
} = useCodexConfigState({ initialData });
const [localCodexApiFormat, setLocalCodexApiFormat] =
useState<CodexApiFormat>(() => {
if (initialData?.meta?.apiFormat === "openai_chat") {
return "openai_chat";
}
if (initialData?.meta?.apiFormat === "openai_responses") {
return "openai_responses";
}
return (
codexApiFormatFromWireApi(
extractCodexWireApi(
typeof initialData?.settingsConfig?.config === "string"
? initialData.settingsConfig.config
: "",
),
) ?? "openai_responses"
);
});
const { configError: codexConfigError, debouncedValidate } =
useCodexTomlValidation();
const handleCodexConfigChange = useCallback(
(value: string) => {
originalHandleCodexConfigChange(value);
const nextFormat = codexApiFormatFromWireApi(extractCodexWireApi(value));
if (nextFormat) {
setLocalCodexApiFormat(nextFormat);
}
debouncedValidate(value);
},
[originalHandleCodexConfigChange, debouncedValidate],
);
const handleCodexApiFormatChange = useCallback(
(format: CodexApiFormat) => {
setLocalCodexApiFormat(format);
setCodexConfig((prev) => {
const updated = setCodexWireApi(prev, "responses");
debouncedValidate(updated);
return updated;
});
},
[setCodexConfig, debouncedValidate],
);
useEffect(() => {
if (appId === "codex" && !initialData && selectedPresetId === "custom") {
const template = getCodexCustomTemplate();
@@ -1049,9 +1109,13 @@ function ProviderFormFull({
if (appId === "codex") {
try {
const authJson = JSON.parse(codexAuth);
const normalizedCodexConfig =
category !== "official" && (codexConfig ?? "").trim()
? setCodexWireApi(codexConfig ?? "", "responses")
: (codexConfig ?? "");
const configObj = {
auth: authJson,
config: codexConfig ?? "",
config: normalizedCodexConfig,
};
settingsConfig = JSON.stringify(configObj);
} catch (err) {
@@ -1236,7 +1300,9 @@ function ProviderFormFull({
apiFormat:
appId === "claude" && category !== "official"
? localApiFormat
: undefined,
: appId === "codex" && category !== "official"
? localCodexApiFormat
: undefined,
apiKeyField:
appId === "claude" &&
category !== "official" &&
@@ -1360,6 +1426,10 @@ function ProviderFormFull({
if (appId === "codex") {
const template = getCodexCustomTemplate();
resetCodexConfig(template.auth, template.config);
setLocalCodexApiFormat(
codexApiFormatFromWireApi(extractCodexWireApi(template.config)) ??
"openai_responses",
);
}
if (appId === "gemini") {
resetGeminiConfig({}, {});
@@ -1396,6 +1466,11 @@ function ProviderFormFull({
const config = preset.config ?? "";
resetCodexConfig(auth, config);
setLocalCodexApiFormat(
preset.apiFormat ??
codexApiFormatFromWireApi(extractCodexWireApi(config)) ??
"openai_responses",
);
form.reset({
name: preset.nameKey ? t(preset.nameKey) : preset.name,
@@ -1860,6 +1935,8 @@ function ProviderFormFull({
}
autoSelect={endpointAutoSelect}
onAutoSelectChange={setEndpointAutoSelect}
apiFormat={localCodexApiFormat}
onApiFormatChange={handleCodexApiFormatChange}
shouldShowModelField={category !== "official"}
modelName={codexModelName}
onModelNameChange={handleCodexModelNameChange}