mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-01 12:22:09 +08:00
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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user