From caa912e3a39c60330fad641b295ae8b13cdea586 Mon Sep 17 00:00:00 2001 From: Sean Wang Date: Tue, 16 Jun 2026 17:01:01 +0800 Subject: [PATCH] fix: prevent duplicate codex base_url entries (#4316) --- src/utils/providerConfigUtils.ts | 68 ++++++++++++++----- tests/utils/providerConfigUtils.codex.test.ts | 42 ++++++++++++ 2 files changed, 92 insertions(+), 18 deletions(-) diff --git a/src/utils/providerConfigUtils.ts b/src/utils/providerConfigUtils.ts index 6d953642c..6d5576a3d 100644 --- a/src/utils/providerConfigUtils.ts +++ b/src/utils/providerConfigUtils.ts @@ -400,7 +400,7 @@ export const hasTomlCommonConfigSnippet = ( const TOML_SECTION_HEADER_PATTERN = /^\s*\[([^\]\r\n]+)\]\s*$/; const TOML_BASE_URL_PATTERN = - /^\s*base_url\s*=\s*(["'])([^"'\r\n]+)\1\s*(?:#.*)?$/; + /^\s*base_url\s*=\s*(?:"((?:\\.|[^"\\\r\n])*)"|'([^'\r\n]*)')\s*(?:#.*)?$/; const TOML_EXPERIMENTAL_BEARER_TOKEN_PATTERN = /^\s*experimental_bearer_token\s*=\s*(["'])([^"'\r\n]+)\1\s*(?:#.*)?$/; const TOML_EXPERIMENTAL_BEARER_TOKEN_REPLACE_PATTERN = @@ -553,11 +553,12 @@ const findTomlAssignmentInRange = ( ): TomlAssignmentMatch | undefined => { for (let index = startIndex; index < endIndex; index += 1) { const match = lines[index].match(pattern); - if (match?.[2]) { + const value = match?.[2] ?? match?.[1]; + if (value) { return { index, sectionName, - value: match[2], + value, }; } } @@ -565,6 +566,26 @@ const findTomlAssignmentInRange = ( return undefined; }; +const findTomlAssignmentsInRange = ( + lines: string[], + pattern: RegExp, + startIndex: number, + endIndex: number, + sectionName?: string, +): TomlAssignmentMatch[] => { + const matches: TomlAssignmentMatch[] = []; + + for (let index = startIndex; index < endIndex; index += 1) { + const match = lines[index].match(pattern); + const value = match?.[2] ?? match?.[1]; + if (value) { + matches.push({ index, sectionName, value }); + } + } + + return matches; +}; + const findTomlLineInRange = ( lines: string[], pattern: RegExp, @@ -595,14 +616,15 @@ const findTomlAssignments = ( } const match = line.match(pattern); - if (!match?.[2]) { + const value = match?.[2] ?? match?.[1]; + if (!value) { return; } assignments.push({ index, sectionName: currentSectionName, - value: match[2], + value, }); }); @@ -1228,18 +1250,20 @@ export const setCodexBaseUrl = ( if (targetSectionName) { const sectionRange = getTomlSectionRange(lines, targetSectionName); - const targetMatch = sectionRange - ? findTomlAssignmentInRange( + const targetMatches = sectionRange + ? findTomlAssignmentsInRange( lines, TOML_BASE_URL_PATTERN, sectionRange.bodyStartIndex, sectionRange.bodyEndIndex, targetSectionName, ) - : undefined; + : []; - if (targetMatch) { - lines.splice(targetMatch.index, 1); + if (targetMatches.length > 0) { + for (const match of [...targetMatches].reverse()) { + lines.splice(match.index, 1); + } return finalizeTomlText(lines); } } @@ -1257,18 +1281,22 @@ export const setCodexBaseUrl = ( if (targetSectionName) { let targetSectionRange = getTomlSectionRange(lines, targetSectionName); - const targetMatch = targetSectionRange - ? findTomlAssignmentInRange( + const targetMatches = targetSectionRange + ? findTomlAssignmentsInRange( lines, TOML_BASE_URL_PATTERN, targetSectionRange.bodyStartIndex, targetSectionRange.bodyEndIndex, targetSectionName, ) - : undefined; + : []; - if (targetMatch) { - lines[targetMatch.index] = replacementLine; + if (targetMatches.length > 0) { + const [firstMatch, ...duplicateMatches] = targetMatches; + lines[firstMatch.index] = replacementLine; + for (const match of [...duplicateMatches].reverse()) { + lines.splice(match.index, 1); + } return finalizeTomlText(lines); } @@ -1291,14 +1319,18 @@ export const setCodexBaseUrl = ( } const topLevelEndIndex = getTopLevelEndIndex(lines); - const topLevelMatch = findTomlAssignmentInRange( + const topLevelMatches = findTomlAssignmentsInRange( lines, TOML_BASE_URL_PATTERN, 0, topLevelEndIndex, ); - if (topLevelMatch) { - lines[topLevelMatch.index] = replacementLine; + if (topLevelMatches.length > 0) { + const [firstMatch, ...duplicateMatches] = topLevelMatches; + lines[firstMatch.index] = replacementLine; + for (const match of [...duplicateMatches].reverse()) { + lines.splice(match.index, 1); + } return finalizeTomlText(lines); } diff --git a/tests/utils/providerConfigUtils.codex.test.ts b/tests/utils/providerConfigUtils.codex.test.ts index 31b7af15a..108ccaaeb 100644 --- a/tests/utils/providerConfigUtils.codex.test.ts +++ b/tests/utils/providerConfigUtils.codex.test.ts @@ -63,6 +63,48 @@ describe("Codex TOML utils", () => { expect(extractCodexModelName(output2)).toBe("new-model"); }); + it("updates a double-quoted base_url containing single quotes without duplicating it", () => { + const input = [ + 'model_provider = "custom"', + 'model = "gpt-5.4"', + "", + "[model_providers.custom]", + 'name = "custom"', + "base_url = \"https://su'us.codes/v1\"", + 'wire_api = "responses"', + 'requires_openai_auth = true', + "", + ].join("\n"); + + const output = setCodexBaseUrl(input, "https://su'us'd.codes/v1"); + + expect(extractCodexBaseUrl(output)).toBe("https://su'us'd.codes/v1"); + expect(output.match(/^\s*base_url\s*=/gm)).toHaveLength(1); + expect(output).toContain("base_url = \"https://su'us'd.codes/v1\""); + }); + + it("collapses duplicate base_url lines when editing the active provider section", () => { + const input = [ + 'model_provider = "custom"', + 'model = "gpt-5.4"', + "", + "[model_providers.custom]", + 'name = "custom"', + 'base_url = "https://old.example/v1"', + 'base_url = "https://older.example/v1"', + 'wire_api = "responses"', + 'requires_openai_auth = true', + "", + ].join("\n"); + + const output = setCodexBaseUrl(input, "https://new.example/v1"); + + expect(extractCodexBaseUrl(output)).toBe("https://new.example/v1"); + expect(output.match(/^\s*base_url\s*=/gm)).toHaveLength(1); + expect(output).toContain('base_url = "https://new.example/v1"'); + expect(output).not.toContain("older.example"); + }); + it("reads and writes base_url in the active provider section", () => { const input = [ 'model_provider = "custom"',