mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-27 16:26:16 +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:
@@ -18,7 +18,11 @@ import { PROVIDER_TYPES } from "@/config/constants";
|
||||
import { isHermesReadOnlyProvider } from "@/config/hermesProviderPresets";
|
||||
import { ProviderHealthBadge } from "@/components/providers/ProviderHealthBadge";
|
||||
import { FailoverPriorityBadge } from "@/components/providers/FailoverPriorityBadge";
|
||||
import { extractCodexBaseUrl } from "@/utils/providerConfigUtils";
|
||||
import {
|
||||
extractCodexBaseUrl,
|
||||
extractCodexWireApi,
|
||||
isCodexChatWireApi,
|
||||
} from "@/utils/providerConfigUtils";
|
||||
import { useProviderHealth } from "@/lib/query/failover";
|
||||
import { useUsageQuery } from "@/lib/query/queries";
|
||||
|
||||
@@ -191,6 +195,20 @@ export function ProviderCard({
|
||||
appId === "hermes" && isHermesReadOnlyProvider(provider.settingsConfig);
|
||||
const isCodexOauth =
|
||||
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;
|
||||
const config = (provider.settingsConfig as Record<string, any>)?.config;
|
||||
return (
|
||||
typeof config === "string" &&
|
||||
isCodexChatWireApi(extractCodexWireApi(config))
|
||||
);
|
||||
}, [
|
||||
appId,
|
||||
provider.category,
|
||||
provider.meta?.apiFormat,
|
||||
(provider.settingsConfig as Record<string, any>)?.config,
|
||||
]);
|
||||
const isClaudeThirdParty =
|
||||
appId === "claude" && provider.category === "third_party";
|
||||
|
||||
@@ -345,6 +363,14 @@ export function ProviderCard({
|
||||
</span>
|
||||
)}
|
||||
|
||||
{codexNeedsRouting && (
|
||||
<span className="inline-flex items-center rounded-md bg-sky-100 px-1.5 py-0.5 text-[10px] font-semibold text-sky-700 dark:bg-sky-900/40 dark:text-sky-300">
|
||||
{t("codex.needsRouting", {
|
||||
defaultValue: "需要路由",
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{appId === "claude" && provider.category === "official" && (
|
||||
<span className="inline-flex items-center rounded-md bg-slate-200 px-1.5 py-0.5 text-[10px] font-semibold text-slate-700 dark:bg-slate-700/60 dark:text-slate-200">
|
||||
{t("claudeCode.noRoutingSupport", {
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { FormLabel } from "@/components/ui/form";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { toast } from "sonner";
|
||||
import { Download, Loader2 } from "lucide-react";
|
||||
import EndpointSpeedTest from "./EndpointSpeedTest";
|
||||
@@ -10,7 +18,7 @@ import {
|
||||
showFetchModelsError,
|
||||
type FetchedModel,
|
||||
} from "@/lib/api/model-fetch";
|
||||
import type { ProviderCategory } from "@/types";
|
||||
import type { CodexApiFormat, ProviderCategory } from "@/types";
|
||||
|
||||
interface EndpointCandidate {
|
||||
url: string;
|
||||
@@ -39,6 +47,10 @@ interface CodexFormFieldsProps {
|
||||
autoSelect: boolean;
|
||||
onAutoSelectChange: (checked: boolean) => void;
|
||||
|
||||
// API Format
|
||||
apiFormat: CodexApiFormat;
|
||||
onApiFormatChange: (format: CodexApiFormat) => void;
|
||||
|
||||
// Model Name
|
||||
shouldShowModelField?: boolean;
|
||||
modelName?: string;
|
||||
@@ -67,6 +79,8 @@ export function CodexFormFields({
|
||||
onCustomEndpointsChange,
|
||||
autoSelect,
|
||||
onAutoSelectChange,
|
||||
apiFormat,
|
||||
onApiFormatChange,
|
||||
shouldShowModelField = true,
|
||||
modelName = "",
|
||||
onModelNameChange,
|
||||
@@ -143,6 +157,43 @@ export function CodexFormFields({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Codex API 格式选择 */}
|
||||
{shouldShowSpeedTest && (
|
||||
<div className="space-y-2">
|
||||
<FormLabel htmlFor="codexApiFormat">
|
||||
{t("providerForm.apiFormat", { defaultValue: "API 格式" })}
|
||||
</FormLabel>
|
||||
<Select
|
||||
value={apiFormat}
|
||||
onValueChange={(value) =>
|
||||
onApiFormatChange(value as CodexApiFormat)
|
||||
}
|
||||
>
|
||||
<SelectTrigger id="codexApiFormat" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="openai_responses">
|
||||
{t("providerForm.codexApiFormatResponses", {
|
||||
defaultValue: "OpenAI Responses API (原生)",
|
||||
})}
|
||||
</SelectItem>
|
||||
<SelectItem value="openai_chat">
|
||||
{t("providerForm.codexApiFormatOpenAIChat", {
|
||||
defaultValue: "OpenAI Chat Completions (需开启路由)",
|
||||
})}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("providerForm.codexApiFormatHint", {
|
||||
defaultValue:
|
||||
"选择供应商真实支持的 Codex API 格式;Chat Completions 会通过本地路由自动转换为 Responses。",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Codex Model Name 输入框 */}
|
||||
{shouldShowModelField && onModelNameChange && (
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Codex 预设供应商配置模板
|
||||
*/
|
||||
import { ProviderCategory } from "../types";
|
||||
import type { CodexApiFormat } from "../types";
|
||||
import type { PresetTheme } from "./claudeProviderPresets";
|
||||
|
||||
export interface CodexProviderPreset {
|
||||
@@ -24,6 +25,8 @@ export interface CodexProviderPreset {
|
||||
// 图标配置
|
||||
icon?: string; // 图标名称
|
||||
iconColor?: string; // 图标颜色
|
||||
// Codex API 格式
|
||||
apiFormat?: CodexApiFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,6 +19,10 @@ import {
|
||||
} from "@/lib/query";
|
||||
import { extractErrorMessage } from "@/utils/errorUtils";
|
||||
import { openclawKeys } from "@/hooks/useOpenClaw";
|
||||
import {
|
||||
extractCodexWireApi,
|
||||
isCodexChatWireApi,
|
||||
} from "@/utils/providerConfigUtils";
|
||||
|
||||
/**
|
||||
* Hook for managing provider actions (add, update, delete, switch)
|
||||
@@ -149,6 +153,16 @@ export function useProviderActions(
|
||||
const isCopilotProvider =
|
||||
activeApp === "claude" &&
|
||||
provider.meta?.providerType === "github_copilot";
|
||||
const isCodexChatFormat =
|
||||
activeApp === "codex" &&
|
||||
(provider.meta?.apiFormat === "openai_chat" ||
|
||||
(typeof (provider.settingsConfig as Record<string, any>)?.config ===
|
||||
"string" &&
|
||||
isCodexChatWireApi(
|
||||
extractCodexWireApi(
|
||||
(provider.settingsConfig as Record<string, any>).config,
|
||||
),
|
||||
)));
|
||||
|
||||
// Determine why this provider requires the proxy
|
||||
let proxyRequiredReason: string | null = null;
|
||||
@@ -171,6 +185,10 @@ export function useProviderActions(
|
||||
proxyRequiredReason = t("notifications.proxyReasonOpenAIResponses", {
|
||||
defaultValue: "使用 OpenAI Responses 接口格式",
|
||||
});
|
||||
} else if (isCodexChatFormat) {
|
||||
proxyRequiredReason = t("notifications.proxyReasonOpenAIChat", {
|
||||
defaultValue: "使用 OpenAI Chat 接口格式",
|
||||
});
|
||||
} else if (
|
||||
activeApp === "claude-desktop" &&
|
||||
provider.meta?.claudeDesktopMode === "proxy"
|
||||
|
||||
@@ -178,6 +178,7 @@
|
||||
"noRoutingSupport": "No Routing Support"
|
||||
},
|
||||
"codex": {
|
||||
"needsRouting": "Needs Routing",
|
||||
"noRoutingSupport": "No Routing Support"
|
||||
},
|
||||
"claudeDesktop": {
|
||||
@@ -908,6 +909,9 @@
|
||||
"apiFormatOpenAIChat": "OpenAI Chat Completions (Requires routing)",
|
||||
"apiFormatOpenAIResponses": "OpenAI Responses API (Requires routing)",
|
||||
"apiFormatGeminiNative": "Gemini Native generateContent (Requires routing)",
|
||||
"codexApiFormatResponses": "OpenAI Responses API (Native)",
|
||||
"codexApiFormatOpenAIChat": "OpenAI Chat Completions (Requires routing)",
|
||||
"codexApiFormatHint": "Select the Codex API format actually supported by this provider; the config stays on Responses for newer Codex versions, while Chat Completions is converted through local routing.",
|
||||
"authField": "Auth Field",
|
||||
"authFieldAuthToken": "ANTHROPIC_AUTH_TOKEN (Default)",
|
||||
"authFieldApiKey": "ANTHROPIC_API_KEY",
|
||||
|
||||
@@ -178,6 +178,7 @@
|
||||
"noRoutingSupport": "ルーティング非対応"
|
||||
},
|
||||
"codex": {
|
||||
"needsRouting": "ルーティングが必要",
|
||||
"noRoutingSupport": "ルーティング非対応"
|
||||
},
|
||||
"claudeDesktop": {
|
||||
@@ -908,6 +909,9 @@
|
||||
"apiFormatOpenAIChat": "OpenAI Chat Completions(ルーティングが必要)",
|
||||
"apiFormatOpenAIResponses": "OpenAI Responses API(ルーティングが必要)",
|
||||
"apiFormatGeminiNative": "Gemini Native generateContent(ルーティングが必要)",
|
||||
"codexApiFormatResponses": "OpenAI Responses API(ネイティブ)",
|
||||
"codexApiFormatOpenAIChat": "OpenAI Chat Completions(ルーティングが必要)",
|
||||
"codexApiFormatHint": "このプロバイダーが実際に対応している Codex API フォーマットを選択します。新しい Codex との互換性のため設定は Responses のままにし、Chat Completions はローカルルーティングで変換します。",
|
||||
"authField": "認証フィールド",
|
||||
"authFieldAuthToken": "ANTHROPIC_AUTH_TOKEN(デフォルト)",
|
||||
"authFieldApiKey": "ANTHROPIC_API_KEY",
|
||||
|
||||
@@ -178,6 +178,7 @@
|
||||
"noRoutingSupport": "不支持路由"
|
||||
},
|
||||
"codex": {
|
||||
"needsRouting": "需要路由",
|
||||
"noRoutingSupport": "不支持路由"
|
||||
},
|
||||
"claudeDesktop": {
|
||||
@@ -908,6 +909,9 @@
|
||||
"apiFormatOpenAIChat": "OpenAI Chat Completions (需开启路由)",
|
||||
"apiFormatOpenAIResponses": "OpenAI Responses API (需开启路由)",
|
||||
"apiFormatGeminiNative": "Gemini Native generateContent (需开启路由)",
|
||||
"codexApiFormatResponses": "OpenAI Responses API (原生)",
|
||||
"codexApiFormatOpenAIChat": "OpenAI Chat Completions (需开启路由)",
|
||||
"codexApiFormatHint": "选择供应商真实支持的 Codex API 格式;配置仍保持 Responses 以兼容新版 Codex,Chat Completions 会通过本地路由自动转换。",
|
||||
"authField": "认证字段",
|
||||
"authFieldAuthToken": "ANTHROPIC_AUTH_TOKEN(默认)",
|
||||
"authFieldApiKey": "ANTHROPIC_API_KEY",
|
||||
|
||||
+6
-1
@@ -161,7 +161,7 @@ export interface ProviderMeta {
|
||||
costMultiplier?: string;
|
||||
// 供应商计费模式来源
|
||||
pricingModelSource?: string;
|
||||
// Claude API 格式(仅 Claude 供应商使用)
|
||||
// API 格式(Claude / Codex 供应商使用)
|
||||
// - "anthropic": 原生 Anthropic Messages API 格式,直接透传
|
||||
// - "openai_chat": OpenAI Chat Completions 格式,需要格式转换
|
||||
// - "openai_responses": OpenAI Responses API 格式,需要格式转换
|
||||
@@ -204,6 +204,11 @@ export type ClaudeApiFormat =
|
||||
| "openai_responses"
|
||||
| "gemini_native";
|
||||
|
||||
// Codex API 格式类型
|
||||
// - "openai_responses": OpenAI Responses API 格式,直接透传
|
||||
// - "openai_chat": OpenAI Chat Completions 格式,需要本地路由转换
|
||||
export type CodexApiFormat = "openai_responses" | "openai_chat";
|
||||
|
||||
// Claude 认证字段类型
|
||||
export type ClaudeApiKeyField = "ANTHROPIC_AUTH_TOKEN" | "ANTHROPIC_API_KEY";
|
||||
|
||||
|
||||
@@ -418,6 +418,8 @@ const TOML_SECTION_HEADER_PATTERN = /^\s*\[([^\]\r\n]+)\]\s*$/;
|
||||
const TOML_BASE_URL_PATTERN =
|
||||
/^\s*base_url\s*=\s*(["'])([^"'\r\n]+)\1\s*(?:#.*)?$/;
|
||||
const TOML_MODEL_PATTERN = /^\s*model\s*=\s*(["'])([^"'\r\n]+)\1\s*(?:#.*)?$/;
|
||||
const TOML_WIRE_API_PATTERN =
|
||||
/^\s*wire_api\s*=\s*(["'])([^"'\r\n]+)\1\s*(?:#.*)?$/;
|
||||
const TOML_MODEL_PROVIDER_LINE_PATTERN =
|
||||
/^\s*model_provider\s*=\s*(["'])([^"'\r\n]+)\1\s*(?:#.*)?$/;
|
||||
const TOML_MODEL_PROVIDER_PATTERN =
|
||||
@@ -585,6 +587,8 @@ const getRecoverableBaseUrlAssignments = (
|
||||
!isOtherProviderSection(sectionName, targetSectionName),
|
||||
);
|
||||
|
||||
const getRecoverableCodexProviderAssignments = getRecoverableBaseUrlAssignments;
|
||||
|
||||
const getTopLevelModelProviderLineIndex = (lines: string[]): number => {
|
||||
const topLevelEndIndex = getTopLevelEndIndex(lines);
|
||||
|
||||
@@ -597,6 +601,149 @@ const getTopLevelModelProviderLineIndex = (lines: string[]): number => {
|
||||
return -1;
|
||||
};
|
||||
|
||||
const CODEX_CHAT_WIRE_API_VALUES = new Set([
|
||||
"chat",
|
||||
"chat_completions",
|
||||
"chat-completions",
|
||||
"openai_chat",
|
||||
"openai-chat",
|
||||
"openai_chat_completions",
|
||||
]);
|
||||
|
||||
// 判断给定的 wire_api 字符串是否表示 Codex 的 Chat Completions 协议
|
||||
export const isCodexChatWireApi = (
|
||||
wireApi: string | undefined | null,
|
||||
): boolean =>
|
||||
CODEX_CHAT_WIRE_API_VALUES.has(
|
||||
(wireApi ?? "").trim().toLowerCase(),
|
||||
);
|
||||
|
||||
// 从 Codex 的 TOML 配置文本中提取 wire_api(支持单/双引号)
|
||||
export const extractCodexWireApi = (
|
||||
configText: string | undefined | null,
|
||||
): string | undefined => {
|
||||
try {
|
||||
const raw = typeof configText === "string" ? configText : "";
|
||||
const text = normalizeTomlText(raw);
|
||||
if (!text) return undefined;
|
||||
|
||||
const lines = text.split("\n");
|
||||
const targetSectionName = getCodexProviderSectionName(text);
|
||||
|
||||
if (targetSectionName) {
|
||||
const sectionRange = getTomlSectionRange(lines, targetSectionName);
|
||||
if (sectionRange) {
|
||||
const match = findTomlAssignmentInRange(
|
||||
lines,
|
||||
TOML_WIRE_API_PATTERN,
|
||||
sectionRange.bodyStartIndex,
|
||||
sectionRange.bodyEndIndex,
|
||||
targetSectionName,
|
||||
);
|
||||
if (match?.value) {
|
||||
return match.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const topLevelMatch = findTomlAssignmentInRange(
|
||||
lines,
|
||||
TOML_WIRE_API_PATTERN,
|
||||
0,
|
||||
getTopLevelEndIndex(lines),
|
||||
);
|
||||
if (topLevelMatch?.value) {
|
||||
return topLevelMatch.value;
|
||||
}
|
||||
|
||||
const fallbackAssignments = getRecoverableCodexProviderAssignments(
|
||||
findTomlAssignments(lines, TOML_WIRE_API_PATTERN),
|
||||
targetSectionName,
|
||||
);
|
||||
return fallbackAssignments.length === 1
|
||||
? fallbackAssignments[0].value
|
||||
: undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// 在 Codex 的 TOML 配置文本中写入或更新 wire_api 字段
|
||||
export const setCodexWireApi = (
|
||||
configText: string,
|
||||
wireApi: "responses" | "chat",
|
||||
): string => {
|
||||
const normalizedText = normalizeTomlText(configText);
|
||||
const lines = normalizedText ? normalizedText.split("\n") : [];
|
||||
const targetSectionName = getCodexProviderSectionName(normalizedText);
|
||||
const replacementLine = `wire_api = "${wireApi}"`;
|
||||
const allAssignments = findTomlAssignments(lines, TOML_WIRE_API_PATTERN);
|
||||
const recoverableAssignments = getRecoverableCodexProviderAssignments(
|
||||
allAssignments,
|
||||
targetSectionName,
|
||||
);
|
||||
|
||||
if (targetSectionName) {
|
||||
let targetSectionRange = getTomlSectionRange(lines, targetSectionName);
|
||||
const targetMatch = targetSectionRange
|
||||
? findTomlAssignmentInRange(
|
||||
lines,
|
||||
TOML_WIRE_API_PATTERN,
|
||||
targetSectionRange.bodyStartIndex,
|
||||
targetSectionRange.bodyEndIndex,
|
||||
targetSectionName,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (targetMatch) {
|
||||
lines[targetMatch.index] = replacementLine;
|
||||
return finalizeTomlText(lines);
|
||||
}
|
||||
|
||||
if (recoverableAssignments.length === 1) {
|
||||
lines.splice(recoverableAssignments[0].index, 1);
|
||||
targetSectionRange = getTomlSectionRange(lines, targetSectionName);
|
||||
}
|
||||
|
||||
if (targetSectionRange) {
|
||||
const insertIndex = getTomlSectionInsertIndex(lines, targetSectionRange);
|
||||
lines.splice(insertIndex, 0, replacementLine);
|
||||
return finalizeTomlText(lines);
|
||||
}
|
||||
|
||||
if (lines.length > 0 && lines[lines.length - 1].trim() !== "") {
|
||||
lines.push("");
|
||||
}
|
||||
lines.push(`[${targetSectionName}]`, replacementLine);
|
||||
return finalizeTomlText(lines);
|
||||
}
|
||||
|
||||
const topLevelEndIndex = getTopLevelEndIndex(lines);
|
||||
const topLevelMatch = findTomlAssignmentInRange(
|
||||
lines,
|
||||
TOML_WIRE_API_PATTERN,
|
||||
0,
|
||||
topLevelEndIndex,
|
||||
);
|
||||
if (topLevelMatch) {
|
||||
lines[topLevelMatch.index] = replacementLine;
|
||||
return finalizeTomlText(lines);
|
||||
}
|
||||
|
||||
const modelProviderIndex = getTopLevelModelProviderLineIndex(lines);
|
||||
if (modelProviderIndex !== -1) {
|
||||
lines.splice(modelProviderIndex + 1, 0, replacementLine);
|
||||
return finalizeTomlText(lines);
|
||||
}
|
||||
|
||||
if (lines.length === 0) {
|
||||
return `${replacementLine}\n`;
|
||||
}
|
||||
|
||||
lines.splice(topLevelEndIndex, 0, replacementLine);
|
||||
return finalizeTomlText(lines);
|
||||
};
|
||||
|
||||
// 从 Codex 的 TOML 配置文本中提取 base_url(支持单/双引号)
|
||||
export const extractCodexBaseUrl = (
|
||||
configText: string | undefined | null,
|
||||
|
||||
Reference in New Issue
Block a user