mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-10 01:39:25 +00:00
fix(oauth): fix multi-provider custom registry import losing providers
- Add `applyCustomRegistryEntries` helper to apply all entries in-memory before a single write\n- Update CLI Add Platform flow to use it, replacing interleaved in-memory mutations with removeProvider RPCs\n- Add regression tests for repeated imports and provider field refreshing
This commit is contained in:
parent
0d074f1bae
commit
c604ee71be
5 changed files with 117 additions and 15 deletions
6
.changeset/fix-custom-registry-multi-provider.md
Normal file
6
.changeset/fix-custom-registry-multi-provider.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code-oauth": patch
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Fix loss of providers when importing a multi-provider custom registry.
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import {
|
||||
applyCustomRegistryProvider,
|
||||
applyCustomRegistryEntries,
|
||||
fetchCustomRegistry,
|
||||
type CustomRegistrySource,
|
||||
type ManagedKimiConfigShape,
|
||||
} from '@moonshot-ai/kimi-code-oauth';
|
||||
import {
|
||||
applyCatalogProvider,
|
||||
|
|
@ -268,7 +269,7 @@ async function handleCustomRegistryAddViaDialog(host: SlashCommandHost): Promise
|
|||
apiKey: value.apiKey,
|
||||
};
|
||||
|
||||
let entries: Record<string, { id: string; name: string; api: string; type: string; models: Record<string, unknown> }>;
|
||||
let entries: Awaited<ReturnType<typeof fetchCustomRegistry>>;
|
||||
try {
|
||||
entries = await fetchCustomRegistry(source);
|
||||
} catch (err) {
|
||||
|
|
@ -276,20 +277,14 @@ async function handleCustomRegistryAddViaDialog(host: SlashCommandHost): Promise
|
|||
return false;
|
||||
}
|
||||
|
||||
const addedProviderIds: string[] = [];
|
||||
const addedProviderIds = Object.values(entries).map((entry) => entry.id);
|
||||
try {
|
||||
let config = await host.harness.getConfig();
|
||||
for (const entry of Object.values(entries)) {
|
||||
if (config.providers[entry.id] !== undefined) {
|
||||
config = await host.harness.removeProvider(entry.id);
|
||||
}
|
||||
applyCustomRegistryProvider(
|
||||
config as unknown as Parameters<typeof applyCustomRegistryProvider>[0],
|
||||
entry as Parameters<typeof applyCustomRegistryProvider>[1],
|
||||
source,
|
||||
);
|
||||
addedProviderIds.push(entry.id);
|
||||
}
|
||||
const config = await host.harness.getConfig();
|
||||
applyCustomRegistryEntries(
|
||||
config as unknown as ManagedKimiConfigShape,
|
||||
entries,
|
||||
source,
|
||||
);
|
||||
await host.harness.setConfig({
|
||||
providers: config.providers,
|
||||
models: config.models,
|
||||
|
|
|
|||
|
|
@ -349,3 +349,28 @@ export function removeCustomRegistryProvider(
|
|||
config['defaultProvider'] = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies every entry from a single api.json import in memory. Mirrors the
|
||||
* "remove if present, then apply" sequence the Add Platform flow used to do
|
||||
* via the `removeProvider` RPC, but stays purely in-memory so callers can
|
||||
* persist the whole batch with a single write at the end.
|
||||
*
|
||||
* Bug fixed: previously the caller interleaved in-memory `applyCustomRegistry-
|
||||
* Provider` with the disk-writing `removeProvider` RPC inside a loop. Each
|
||||
* RPC re-read disk and returned a fresh config object, discarding entries that
|
||||
* had already been merged in-memory from earlier iterations. Re-importing a
|
||||
* multi-provider api.json silently lost N-1 of N providers.
|
||||
*/
|
||||
export function applyCustomRegistryEntries(
|
||||
config: ManagedKimiConfigShape,
|
||||
entries: Record<string, CustomRegistryProviderEntry>,
|
||||
source: CustomRegistrySource,
|
||||
): void {
|
||||
for (const entry of Object.values(entries)) {
|
||||
if (entry.id in config.providers) {
|
||||
removeCustomRegistryProvider(config, entry.id);
|
||||
}
|
||||
applyCustomRegistryProvider(config, entry, source);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ export type {
|
|||
} from './open-platform';
|
||||
|
||||
export {
|
||||
applyCustomRegistryEntries,
|
||||
applyCustomRegistryProvider,
|
||||
capabilitiesFromCustomEntry,
|
||||
CustomRegistryApiError,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
applyCustomRegistryEntries,
|
||||
applyCustomRegistryProvider,
|
||||
CUSTOM_REGISTRY_DEFAULT_CAPABILITIES,
|
||||
CUSTOM_REGISTRY_DEFAULT_MAX_CONTEXT,
|
||||
|
|
@ -382,6 +383,80 @@ describe('removeCustomRegistryProvider', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('applyCustomRegistryEntries', () => {
|
||||
// Regression: re-importing the same multi-provider api.json used to lose all
|
||||
// but the last provider because the caller mixed in-memory mutations with the
|
||||
// `harness.removeProvider` RPC (which read/wrote disk inside the loop and
|
||||
// returned a fresh config object, discarding prior iterations' additions).
|
||||
// The pure in-memory helper must keep every entry across repeated imports.
|
||||
it('keeps every provider when the same multi-provider source is applied twice', () => {
|
||||
const source: CustomRegistrySource = {
|
||||
kind: 'apiJson',
|
||||
url: 'https://registry.example.test/v1/models/api.json',
|
||||
apiKey: 'sk-token',
|
||||
};
|
||||
const entries: Record<string, CustomRegistryProviderEntry> = {
|
||||
a: { id: 'a', name: 'A', api: 'https://a.test/v1', type: 'openai', models: { 'm1': { id: 'm1' } } },
|
||||
b: { id: 'b', name: 'B', api: 'https://b.test/v1', type: 'openai', models: { 'm1': { id: 'm1' } } },
|
||||
c: { id: 'c', name: 'C', api: 'https://c.test/v1', type: 'openai', models: { 'm1': { id: 'm1' } } },
|
||||
};
|
||||
|
||||
const config: ManagedKimiConfigShape = { providers: {} };
|
||||
applyCustomRegistryEntries(config, entries, source);
|
||||
applyCustomRegistryEntries(config, entries, source);
|
||||
|
||||
expect(Object.keys(config.providers).sort()).toEqual(['a', 'b', 'c']);
|
||||
expect(config.models?.['a/m1']).toBeDefined();
|
||||
expect(config.models?.['b/m1']).toBeDefined();
|
||||
expect(config.models?.['c/m1']).toBeDefined();
|
||||
});
|
||||
|
||||
it('refreshes provider fields, drops stale aliases, and clears defaultModel that no longer exists', () => {
|
||||
const source: CustomRegistrySource = {
|
||||
kind: 'apiJson',
|
||||
url: 'https://registry.example.test/api.json',
|
||||
apiKey: 'sk-new',
|
||||
};
|
||||
const config: ManagedKimiConfigShape = {
|
||||
providers: {
|
||||
x: { type: 'openai', baseUrl: 'https://x-old.test/v1', apiKey: 'sk-old' },
|
||||
},
|
||||
models: {
|
||||
'x/old-model': { provider: 'x', model: 'old-model', maxContextSize: 1000 },
|
||||
'other/keep': { provider: 'other', model: 'keep', maxContextSize: 1000 },
|
||||
},
|
||||
defaultModel: 'x/old-model',
|
||||
};
|
||||
|
||||
applyCustomRegistryEntries(
|
||||
config,
|
||||
{
|
||||
x: {
|
||||
id: 'x',
|
||||
name: 'X',
|
||||
api: 'https://x-new.test/v1',
|
||||
type: 'openai',
|
||||
models: { 'new-model': { id: 'new-model' } },
|
||||
},
|
||||
},
|
||||
source,
|
||||
);
|
||||
|
||||
expect(config.providers['x']).toMatchObject({
|
||||
type: 'openai',
|
||||
baseUrl: 'https://x-new.test/v1',
|
||||
apiKey: 'sk-new',
|
||||
});
|
||||
expect(config.models?.['x/old-model']).toBeUndefined();
|
||||
expect(config.models?.['x/new-model']).toBeDefined();
|
||||
expect(config.models?.['other/keep']).toBeDefined();
|
||||
// defaultModel pointed at the now-removed alias, so it must be cleared
|
||||
// (matches the old harness.removeProvider semantics that the caller relied
|
||||
// on before the refactor).
|
||||
expect(config.defaultModel).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('capabilitiesFromCustomEntry', () => {
|
||||
it('returns an empty array when no rich fields are present', () => {
|
||||
expect(capabilitiesFromCustomEntry({ id: 'm' })).toEqual([]);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue