fix(codex): match quoted provider table headers when writing base_url (#8276)

This commit is contained in:
HEOJUNFO authored and GitHub committed 2026-10-15 16:18:01 +08:00
1 parent 51ac86f1c5
commit c1051f8e4e
2 files changed
+228 -16

No files matched your search

+111 -16
View File
@@ -244,6 +244,10 @@ export const setApiKeyInConfig = (
// ========== Codex base_url utils ========== // ========== Codex base_url utils ==========
const TOML_SECTION_HEADER_PATTERN = /^\s*\[([^\]\r\n]+)\]\s*$/; const TOML_SECTION_HEADER_PATTERN = /^\s*\[([^\]\r\n]+)\]\s*$/;
const TOML_BARE_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
// 点分键的一段(基本字符串、字面字符串或裸键)及其后的 "." 或结尾
const TOML_DOTTED_KEY_SEGMENT_PATTERN =
/\s*(?:"((?:\\.|[^"\\\r\n])*)"|'([^'\r\n]*)'|([A-Za-z0-9_-]+))\s*(\.|$)/y;
const TOML_BASE_URL_PATTERN = const TOML_BASE_URL_PATTERN =
/^\s*base_url\s*=\s*(?:"((?:\\.|[^"\\\r\n])*)"|'([^'\r\n]*)')\s*(?:#.*)?$/; /^\s*base_url\s*=\s*(?:"((?:\\.|[^"\\\r\n])*)"|'([^'\r\n]*)')\s*(?:#.*)?$/;
const TOML_EXPERIMENTAL_BEARER_TOKEN_PATTERN = const TOML_EXPERIMENTAL_BEARER_TOKEN_PATTERN =
@@ -298,20 +302,103 @@ const finalizeTomlText = (lines: string[]): string =>
.replace(/\n{3,}/g, "\n\n") .replace(/\n{3,}/g, "\n\n")
.replace(/^\n+/, ""); .replace(/^\n+/, "");
// 表名的规范写法:能用裸键的段用裸键,其余用基本字符串
const tomlTableName = (...segments: string[]): string =>
segments
.map((segment) =>
TOML_BARE_KEY_PATTERN.test(segment) ? segment : tomlBasicString(segment),
)
.join(".");
// `[model_providers."custom"]`、`[ model_providers . custom ]` 与
// `[model_providers.custom]` 是同一张表(#8271),比较表头前先统一成规范写法;
// 认不出的表头原样返回
const normalizeTomlTableName = (header: string): string => {
const segments: string[] = [];
let index = 0;
for (;;) {
TOML_DOTTED_KEY_SEGMENT_PATTERN.lastIndex = index;
const match = TOML_DOTTED_KEY_SEGMENT_PATTERN.exec(header);
if (!match) return header;
const [, basic, literal, bare, separator] = match;
segments.push(
basic !== undefined ? unescapeTomlBasicString(basic) : (literal ?? bare),
);
if (separator !== ".") return tomlTableName(...segments);
index = TOML_DOTTED_KEY_SEGMENT_PATTERN.lastIndex;
}
};
// 标出以多行字符串(""" 或 ''')内部开头的行。这些行是字符串内容,像表头或赋值也不算:
// 说明文字里的 `[model_providers."custom"]` 示例不能被当成真正的 provider 表。
// 逐字符跟踪字符串和注释状态;单行字符串和注释到行尾为止。
const getTomlMultilineStringLines = (lines: string[]): boolean[] => {
let delimiter: '"""' | "'''" | undefined;
return lines.map((line) => {
const insideString = delimiter !== undefined;
let index = 0;
while (index < line.length) {
if (delimiter) {
if (delimiter === '"""' && line[index] === "\\") {
index += 2;
continue;
}
if (line.startsWith(delimiter, index)) {
// 结束符前最多可以再贴两个引号,它们属于字符串内容
const quote = delimiter[0];
let end = index + 3;
while (end < index + 5 && line[end] === quote) end += 1;
delimiter = undefined;
index = end;
continue;
}
index += 1;
continue;
}
const ch = line[index];
if (ch === "#") break;
if (line.startsWith('"""', index) || line.startsWith("'''", index)) {
delimiter = line.startsWith('"""', index) ? '"""' : "'''";
index += 3;
continue;
}
if (ch === '"') {
index += 1;
while (index < line.length && line[index] !== '"') {
index += line[index] === "\\" ? 2 : 1;
}
index += 1;
continue;
}
if (ch === "'") {
const end = line.indexOf("'", index + 1);
index = end === -1 ? line.length : end + 1;
continue;
}
index += 1;
}
return insideString;
});
};
const getTomlSectionRange = ( const getTomlSectionRange = (
lines: string[], lines: string[],
sectionName: string, sectionName: string,
): TomlSectionRange | undefined => { ): TomlSectionRange | undefined => {
const targetName = normalizeTomlTableName(sectionName);
const stringLines = getTomlMultilineStringLines(lines);
let headerLineIndex = -1; let headerLineIndex = -1;
for (let index = 0; index < lines.length; index += 1) { for (let index = 0; index < lines.length; index += 1) {
if (stringLines[index]) continue;
const match = lines[index].match(TOML_SECTION_HEADER_PATTERN); const match = lines[index].match(TOML_SECTION_HEADER_PATTERN);
if (!match) { if (!match) {
continue; continue;
} }
if (headerLineIndex === -1) { if (headerLineIndex === -1) {
if (match[1] === sectionName) { if (normalizeTomlTableName(match[1]) === targetName) {
headerLineIndex = index; headerLineIndex = index;
} }
continue; continue;
@@ -336,8 +423,10 @@ const getTomlSectionRange = (
}; };
const getTopLevelEndIndex = (lines: string[]): number => { const getTopLevelEndIndex = (lines: string[]): number => {
const firstSectionIndex = lines.findIndex((line) => const stringLines = getTomlMultilineStringLines(lines);
TOML_SECTION_HEADER_PATTERN.test(line), const firstSectionIndex = lines.findIndex(
(line, index) =>
!stringLines[index] && TOML_SECTION_HEADER_PATTERN.test(line),
); );
return firstSectionIndex === -1 ? lines.length : firstSectionIndex; return firstSectionIndex === -1 ? lines.length : firstSectionIndex;
}; };
@@ -414,7 +503,9 @@ const getCodexProviderSectionName = (
configText: string, configText: string,
): string | undefined => { ): string | undefined => {
const providerName = getCodexModelProviderName(configText); const providerName = getCodexModelProviderName(configText);
return providerName ? `model_providers.${providerName}` : undefined; return providerName
? tomlTableName("model_providers", providerName)
: undefined;
}; };
const isCustomCodexModelProviderId = (providerName: string): boolean => { const isCustomCodexModelProviderId = (providerName: string): boolean => {
@@ -430,7 +521,7 @@ const getCodexCustomProviderSectionName = (
): string | undefined => { ): string | undefined => {
const providerName = getCodexModelProviderName(configText); const providerName = getCodexModelProviderName(configText);
return providerName && isCustomCodexModelProviderId(providerName) return providerName && isCustomCodexModelProviderId(providerName)
? `model_providers.${providerName}` ? tomlTableName("model_providers", providerName)
: undefined; : undefined;
}; };
@@ -441,7 +532,9 @@ const findTomlAssignmentInRange = (
endIndex: number, endIndex: number,
sectionName?: string, sectionName?: string,
): TomlAssignmentMatch | undefined => { ): TomlAssignmentMatch | undefined => {
const stringLines = getTomlMultilineStringLines(lines);
for (let index = startIndex; index < endIndex; index += 1) { for (let index = startIndex; index < endIndex; index += 1) {
if (stringLines[index]) continue;
const match = lines[index].match(pattern); const match = lines[index].match(pattern);
const value = match?.[2] ?? match?.[1]; const value = match?.[2] ?? match?.[1];
if (value) { if (value) {
@@ -464,8 +557,10 @@ const findTomlAssignmentsInRange = (
sectionName?: string, sectionName?: string,
): TomlAssignmentMatch[] => { ): TomlAssignmentMatch[] => {
const matches: TomlAssignmentMatch[] = []; const matches: TomlAssignmentMatch[] = [];
const stringLines = getTomlMultilineStringLines(lines);
for (let index = startIndex; index < endIndex; index += 1) { for (let index = startIndex; index < endIndex; index += 1) {
if (stringLines[index]) continue;
const match = lines[index].match(pattern); const match = lines[index].match(pattern);
const value = match?.[2] ?? match?.[1]; const value = match?.[2] ?? match?.[1];
if (value) { if (value) {
@@ -482,8 +577,9 @@ const findTomlLineInRange = (
startIndex: number, startIndex: number,
endIndex: number, endIndex: number,
): number => { ): number => {
const stringLines = getTomlMultilineStringLines(lines);
for (let index = startIndex; index < endIndex; index += 1) { for (let index = startIndex; index < endIndex; index += 1) {
if (pattern.test(lines[index])) { if (!stringLines[index] && pattern.test(lines[index])) {
return index; return index;
} }
} }
@@ -497,11 +593,13 @@ const findTomlAssignments = (
): TomlAssignmentMatch[] => { ): TomlAssignmentMatch[] => {
const assignments: TomlAssignmentMatch[] = []; const assignments: TomlAssignmentMatch[] = [];
let currentSectionName: string | undefined; let currentSectionName: string | undefined;
const stringLines = getTomlMultilineStringLines(lines);
lines.forEach((line, index) => { lines.forEach((line, index) => {
if (stringLines[index]) return;
const sectionMatch = line.match(TOML_SECTION_HEADER_PATTERN); const sectionMatch = line.match(TOML_SECTION_HEADER_PATTERN);
if (sectionMatch) { if (sectionMatch) {
currentSectionName = sectionMatch[1]; currentSectionName = normalizeTomlTableName(sectionMatch[1]);
return; return;
} }
@@ -550,15 +648,12 @@ const getRecoverableBaseUrlAssignments = (
const getRecoverableCodexProviderAssignments = getRecoverableBaseUrlAssignments; const getRecoverableCodexProviderAssignments = getRecoverableBaseUrlAssignments;
const getTopLevelModelProviderLineIndex = (lines: string[]): number => { const getTopLevelModelProviderLineIndex = (lines: string[]): number => {
const topLevelEndIndex = getTopLevelEndIndex(lines); return findTomlLineInRange(
lines,
for (let index = 0; index < topLevelEndIndex; index += 1) { TOML_MODEL_PROVIDER_LINE_PATTERN,
if (TOML_MODEL_PROVIDER_LINE_PATTERN.test(lines[index])) { 0,
return index; getTopLevelEndIndex(lines),
} );
}
return -1;
}; };
const TOML_BASIC_STRING_ESCAPES: Record<string, string> = { const TOML_BASIC_STRING_ESCAPES: Record<string, string> = {
@@ -242,6 +242,123 @@ describe("Codex TOML utils", () => {
expect(extractCodexBaseUrl(output)).toBe("https://relay.example/v1"); expect(extractCodexBaseUrl(output)).toBe("https://relay.example/v1");
}); });
it("finds the active provider table when its header quotes the key", () => {
for (const header of [
'[model_providers."custom"]',
"[model_providers.'custom']",
"[ model_providers . custom ]",
'["model_providers".custom]',
]) {
const input = [
'model_provider = "custom"',
"",
header,
'name = "custom"',
'wire_api = "responses"',
"",
"[features]",
"",
].join("\n");
const output = setCodexBaseUrl(input, "https://relay.example/v1");
const parsed = parseToml(output) as Record<string, any>;
expect(parsed.model_providers.custom, header).toEqual({
name: "custom",
wire_api: "responses",
base_url: "https://relay.example/v1",
});
expect(output.split(/^\s*\[/m), header).toHaveLength(3);
expect(extractCodexBaseUrl(output), header).toBe(
"https://relay.example/v1",
);
expect(
extractCodexBaseUrl(setCodexBaseUrl(output, "")),
header,
).toBeUndefined();
}
});
it("writes base_url into a quoted provider table whose id needs quoting", () => {
const input = [
'model_provider = "my.relay"',
"",
'[model_providers."my.relay"]',
'name = "my.relay"',
"",
].join("\n");
const output = setCodexBaseUrl(input, "https://relay.example/v1");
const parsed = parseToml(output) as Record<string, any>;
expect(Object.keys(parsed.model_providers)).toEqual(["my.relay"]);
expect(parsed.model_providers["my.relay"].base_url).toBe(
"https://relay.example/v1",
);
});
it("updates the bearer token in a provider table whose header quotes the key", () => {
const input = [
'model_provider = "custom"',
"",
'[model_providers."custom"]',
'name = "custom"',
'experimental_bearer_token = "old"',
"",
].join("\n");
const output = updateCodexExperimentalBearerToken(input, "new");
expect(extractCodexExperimentalBearerToken(output)).toBe("new");
expect(
(parseToml(output) as Record<string, any>).model_providers.custom
.experimental_bearer_token,
).toBe("new");
});
it("skips provider tables quoted inside a multi-line string", () => {
for (const delimiter of ['"""', "'''"]) {
const instructions = [
"Example:",
'[model_providers."custom"]',
'base_url = "https://example.invalid/v1"',
'experimental_bearer_token = "example-token"',
"",
].join("\n");
const input = [
'model_provider = "custom"',
`developer_instructions = ${delimiter}`,
`${instructions}${delimiter}`,
"",
"[model_providers.custom]",
'name = "custom"',
'base_url = "https://old.invalid/v1"',
'experimental_bearer_token = "old-token"',
"",
].join("\n");
expect(extractCodexBaseUrl(input), delimiter).toBe(
"https://old.invalid/v1",
);
expect(extractCodexExperimentalBearerToken(input), delimiter).toBe(
"old-token",
);
const output = updateCodexExperimentalBearerToken(
setCodexBaseUrl(input, "https://new.invalid/v1"),
"new-token",
);
const parsed = parseToml(output) as Record<string, any>;
expect(parsed.developer_instructions, delimiter).toBe(instructions);
expect(parsed.model_providers.custom, delimiter).toEqual({
name: "custom",
base_url: "https://new.invalid/v1",
experimental_bearer_token: "new-token",
});
}
});
it("never redefines a custom table the line scan cannot see", () => { it("never redefines a custom table the line scan cannot see", () => {
for (const input of [ for (const input of [
'model = "m"\n\n[model_providers.custom] # mine\nname = "custom"\nwire_api = "responses"\n', 'model = "m"\n\n[model_providers.custom] # mine\nname = "custom"\nwire_api = "responses"\n',