Skip to content

Commit 58038f3

Browse files
committed
fix: disambiguate nested legacy provider section targeting
1 parent e6f416e commit 58038f3

2 files changed

Lines changed: 151 additions & 7 deletions

File tree

cli.js

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ function collectNestedProviderConfigs(node, pathSegments, collector) {
330330
if (isRecoverableNestedProviderConfig(node)) {
331331
collector.push({
332332
name: segments.join('.'),
333+
segments: segments.slice(),
333334
provider: node
334335
});
335336
return;
@@ -349,10 +350,21 @@ function normalizeLegacyModelProviders(modelProviders) {
349350
const normalized = {};
350351
const addRecovered = (entry) => {
351352
const name = entry && typeof entry.name === 'string' ? entry.name : '';
353+
const segments = entry && Array.isArray(entry.segments) ? entry.segments.slice() : null;
352354
const provider = entry ? entry.provider : null;
353355
if (!name || !isPlainObject(provider)) return;
354356
if (Object.prototype.hasOwnProperty.call(modelProviders, name)) return;
355357
if (Object.prototype.hasOwnProperty.call(normalized, name)) return;
358+
if (Array.isArray(segments) && segments.length > 0) {
359+
try {
360+
Object.defineProperty(provider, '__codexmate_legacy_segments', {
361+
value: segments,
362+
enumerable: false,
363+
configurable: true,
364+
writable: true
365+
});
366+
} catch (e) {}
367+
}
356368
normalized[name] = provider;
357369
changed = true;
358370
};
@@ -391,6 +403,14 @@ function escapeRegex(value) {
391403
return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
392404
}
393405

406+
function areStringArraysEqual(a, b) {
407+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
408+
for (let i = 0; i < a.length; i++) {
409+
if (String(a[i]) !== String(b[i])) return false;
410+
}
411+
return true;
412+
}
413+
394414
function parseTomlDottedKeyExpression(expression) {
395415
const text = String(expression || '');
396416
let index = 0;
@@ -471,9 +491,10 @@ function parseTomlDottedKeyExpression(expression) {
471491
return segments.length > 0 ? segments : null;
472492
}
473493

474-
function findProviderSectionRanges(content, providerName) {
494+
function findProviderSectionRanges(content, providerName, exactSegments = null) {
475495
const text = typeof content === 'string' ? content : '';
476496
const name = typeof providerName === 'string' ? providerName.trim() : '';
497+
const targetSegments = Array.isArray(exactSegments) ? exactSegments.map((item) => String(item)) : null;
477498
if (!text || !name) return [];
478499

479500
const safeName = escapeRegex(name);
@@ -494,14 +515,24 @@ function findProviderSectionRanges(content, providerName) {
494515

495516
const parsedSegments = parseTomlDottedKeyExpression(headerExpr);
496517
if (Array.isArray(parsedSegments) && parsedSegments.length >= 2 && parsedSegments[0] === 'model_providers') {
497-
const parsedName = parsedSegments.slice(1).join('.');
498-
if (parsedName === name) {
518+
const providerSegments = parsedSegments.slice(1);
519+
if (targetSegments && targetSegments.length > 0 && areStringArraysEqual(providerSegments, targetSegments)) {
499520
const prev = targetPriorityByStart.get(start);
500-
if (prev === undefined || -2 < prev) {
501-
targetPriorityByStart.set(start, -2);
521+
if (prev === undefined || -3 < prev) {
522+
targetPriorityByStart.set(start, -3);
502523
}
503524
continue;
504525
}
526+
if (!targetSegments || targetSegments.length === 0) {
527+
const parsedName = providerSegments.join('.');
528+
if (parsedName === name) {
529+
const prev = targetPriorityByStart.get(start);
530+
if (prev === undefined || -2 < prev) {
531+
targetPriorityByStart.set(start, -2);
532+
}
533+
continue;
534+
}
535+
}
505536
}
506537

507538
for (const pattern of headerPatterns) {
@@ -2058,6 +2089,10 @@ function performProviderDeletion(name, options = {}) {
20582089
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
20592090
const lineEnding = content.includes('\r\n') ? '\r\n' : '\n';
20602091
const hasBom = content.charCodeAt(0) === 0xFEFF;
2092+
const providerConfig = config.model_providers[name];
2093+
const providerSegments = providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)
2094+
? providerConfig.__codexmate_legacy_segments
2095+
: null;
20612096

20622097
const remainingProviders = Object.keys(config.model_providers || {}).filter(item => item !== name);
20632098
if (remainingProviders.length === 0) {
@@ -2096,7 +2131,7 @@ function performProviderDeletion(name, options = {}) {
20962131
};
20972132

20982133
let updatedContent = null;
2099-
const ranges = findProviderSectionRanges(content, name);
2134+
const ranges = findProviderSectionRanges(content, name, providerSegments);
21002135
if (ranges.length > 0) {
21012136
const sorted = ranges.sort((a, b) => b.start - a.start);
21022137
let removedContent = content;
@@ -5361,7 +5396,11 @@ function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) {
53615396
}
53625397

53635398
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
5364-
const ranges = findProviderSectionRanges(content, name);
5399+
const providerConfig = config.model_providers[name];
5400+
const providerSegments = providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)
5401+
? providerConfig.__codexmate_legacy_segments
5402+
: null;
5403+
const ranges = findProviderSectionRanges(content, name, providerSegments);
53655404
if (ranges.length === 0) {
53665405
if (!silent) console.error('错误: 无法找到提供商配置块');
53675406
throw new Error('无法找到提供商配置块');

tests/e2e/test-config.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,48 @@ module.exports = async function testConfig(ctx) {
460460
'update-provider should preserve preferred_auth_method inline comment'
461461
);
462462

463+
const multilineStringConfig = [
464+
'model_provider = "foo"',
465+
'model = "gpt-5.3-codex"',
466+
'',
467+
'[model_providers.foo]',
468+
'name = "foo"',
469+
'base_url = "https://api.example.com/v1"',
470+
'wire_api = "responses"',
471+
'requires_openai_auth = false',
472+
'preferred_auth_method = """sk-old',
473+
'line-2""" # keep-triple-comment',
474+
'request_max_retries = 4',
475+
'stream_max_retries = 10',
476+
'stream_idle_timeout_ms = 300000',
477+
'',
478+
'[model_providers.openai]',
479+
'name = "openai"',
480+
'base_url = "https://api.openai.com/v1"',
481+
'wire_api = "responses"',
482+
'requires_openai_auth = false',
483+
'preferred_auth_method = ""',
484+
'request_max_retries = 4',
485+
'stream_max_retries = 10',
486+
'stream_idle_timeout_ms = 300000',
487+
''
488+
].join('\n');
489+
fs.writeFileSync(legacyConfigPath, multilineStringConfig, 'utf-8');
490+
const updateMultilineString = await legacyApi('update-provider', {
491+
name: 'foo',
492+
key: 'sk-triple-updated'
493+
});
494+
assert(updateMultilineString.success === true, 'update-provider should handle multiline TOML string values');
495+
const configAfterMultilineUpdate = fs.readFileSync(legacyConfigPath, 'utf-8');
496+
assert(
497+
configAfterMultilineUpdate.includes('preferred_auth_method = "sk-triple-updated" # keep-triple-comment'),
498+
'update-provider should safely replace multiline TOML string and preserve comment'
499+
);
500+
assert(
501+
!configAfterMultilineUpdate.includes('line-2"""'),
502+
'update-provider should remove previous multiline TOML string tail'
503+
);
504+
463505
const nestedMetadataConfig = [
464506
'model_provider = "foo"',
465507
'model = "gpt-5.3-codex"',
@@ -536,6 +578,69 @@ module.exports = async function testConfig(ctx) {
536578
'nested provider with dotted segment should update apiKey'
537579
);
538580

581+
const ambiguousNestedProviderConfig = [
582+
'model_provider = "foo"',
583+
'model = "gpt-5.3-codex"',
584+
'',
585+
'[model_providers.foo]',
586+
'name = "foo"',
587+
'base_url = "https://api.example.com/v1"',
588+
'wire_api = "responses"',
589+
'requires_openai_auth = false',
590+
'preferred_auth_method = "sk-foo"',
591+
'request_max_retries = 4',
592+
'stream_max_retries = 10',
593+
'stream_idle_timeout_ms = 300000',
594+
'',
595+
"[ model_providers . foo . 'bar.baz' ]",
596+
'base_url = "https://primary.example.com/v1"',
597+
'wire_api = "responses"',
598+
'preferred_auth_method = "sk-primary"',
599+
'',
600+
'[model_providers."foo.bar".baz]',
601+
'base_url = "https://alt.example.com/v1"',
602+
'wire_api = "responses"',
603+
'preferred_auth_method = "sk-alt"',
604+
''
605+
].join('\n');
606+
fs.writeFileSync(legacyConfigPath, ambiguousNestedProviderConfig, 'utf-8');
607+
const ambiguousList = await legacyApi('list');
608+
assert(
609+
ambiguousList.providers.some((item) => item && item.name === 'foo.bar.baz'),
610+
'ambiguous nested provider config should expose flattened provider name'
611+
);
612+
const ambiguousUpdate = await legacyApi('update-provider', {
613+
name: 'foo.bar.baz',
614+
url: 'https://primary-updated.example.com/v1',
615+
key: 'sk-primary-updated'
616+
});
617+
assert(ambiguousUpdate.success === true, 'ambiguous nested provider update should succeed');
618+
const configAfterAmbiguousUpdate = fs.readFileSync(legacyConfigPath, 'utf-8');
619+
const primaryBlockMatch = configAfterAmbiguousUpdate.match(
620+
/(?:^|\n)\s*\[\s*model_providers\s*\.\s*foo\s*\.\s*'bar\.baz'\s*\][\s\S]*?(?=\n\s*\[|$)/
621+
);
622+
assert(primaryBlockMatch, 'primary nested provider block should exist after ambiguous update');
623+
assert(
624+
primaryBlockMatch[0].includes('base_url = "https://primary-updated.example.com/v1"'),
625+
'ambiguous update should target primary nested provider block url'
626+
);
627+
assert(
628+
primaryBlockMatch[0].includes('preferred_auth_method = "sk-primary-updated"'),
629+
'ambiguous update should target primary nested provider block key'
630+
);
631+
const alternateBlockMatch = configAfterAmbiguousUpdate.match(
632+
/(?:^|\n)\s*\[\s*model_providers\s*\.\s*"foo\.bar"\s*\.\s*baz\s*\][\s\S]*?(?=\n\s*\[|$)/
633+
);
634+
assert(alternateBlockMatch, 'alternate nested provider block should exist after ambiguous update');
635+
assert(
636+
alternateBlockMatch[0].includes('base_url = "https://alt.example.com/v1"'),
637+
'ambiguous update should not rewrite alternate nested provider block url'
638+
);
639+
assert(
640+
alternateBlockMatch[0].includes('preferred_auth_method = "sk-alt"'),
641+
'ambiguous update should not rewrite alternate nested provider block key'
642+
);
643+
539644
const ipv6Config = [
540645
'model_provider = "foo"',
541646
'model = "gpt-5.3-codex"',

0 commit comments

Comments
 (0)