Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sync-registry-providers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Sync custom registry provider additions, removals, and rotated registry keys during startup refresh.
5 changes: 3 additions & 2 deletions apps/kimi-code/src/cli/sub/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
*
* `add` writes the same `source = { kind: 'apiJson', url, apiKey }` blob the
* TUI does; the next launch's `refreshAllProviderModels`
* (apps/kimi-code/src/tui/utils/refresh-providers.ts) groups by `{url, apiKey}`
* and re-fetches the model list, so periodic refresh is automatic.
* (apps/kimi-code/src/tui/utils/refresh-providers.ts) groups by URL, retries
* available API-key candidates, and re-fetches the model list, so periodic
* refresh is automatic.
*/

import {
Expand Down
8 changes: 4 additions & 4 deletions apps/kimi-code/src/tui/kimi-tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,10 +475,10 @@ export class KimiTUI {
try {
const result = await this.authFlow.refreshProviderModels();
for (const c of result.changed) {
const parts: string[] = [c.providerName];
if (c.added > 0) parts.push(`+${String(c.added)} model${c.added > 1 ? 's' : ''}`);
if (c.removed > 0) parts.push(`-${String(c.removed)} model${c.removed > 1 ? 's' : ''}`);
this.showStatus(parts.join(' · ') + '.');
if (c.added <= 0) continue;
this.showStatus(
`${c.providerName} · +${String(c.added)} model${c.added > 1 ? 's' : ''}.`,
);
}
for (const f of result.failed) {
this.showStatus(`Skipped refreshing ${f.provider}: ${f.reason}`, 'warning');
Expand Down
135 changes: 119 additions & 16 deletions apps/kimi-code/src/tui/utils/refresh-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
filterModelsByPrefix,
getOpenPlatformById,
isOpenPlatformId,
removeCustomRegistryProvider,
resolveKimiCodeRuntimeAuth,
type CustomRegistrySource,
type ManagedKimiConfigShape,
Expand Down Expand Up @@ -42,7 +43,7 @@ export interface RefreshResult {
function readCustomRegistrySource(provider: ProviderConfig): CustomRegistrySource | undefined {
const source = provider.source;
if (typeof source !== 'object' || source === null) return undefined;
const candidate = source as Record<string, unknown>;
const candidate = source;
if (candidate['kind'] !== 'apiJson') return undefined;
const url = candidate['url'];
const apiKey = candidate['apiKey'];
Expand All @@ -51,6 +52,36 @@ function readCustomRegistrySource(provider: ProviderConfig): CustomRegistrySourc
return { kind: 'apiJson', url, apiKey };
}

function customRegistrySourceKey(source: CustomRegistrySource): string {
return JSON.stringify([source.url]);
}

function customRegistrySourceCredentialKey(source: CustomRegistrySource): string {
return JSON.stringify([source.url, source.apiKey]);
}

async function fetchCustomRegistryFromSources(
sources: readonly CustomRegistrySource[],
): Promise<{
readonly entries: Awaited<ReturnType<typeof fetchCustomRegistry>>;
readonly source: CustomRegistrySource;
}> {
let lastError: unknown;
for (const source of sources) {
try {
return {
entries: await fetchCustomRegistry(source),
source,
};
} catch (error) {
lastError = error;
}
}
if (lastError instanceof Error) throw lastError;
if (typeof lastError === 'string') throw new Error(lastError);
throw new Error('No custom registry sources configured.');
}

function asManaged(config: KimiConfig): ManagedKimiConfigShape {
return config as unknown as ManagedKimiConfigShape;
}
Expand Down Expand Up @@ -143,6 +174,14 @@ function providerModelsEqual(
);
}

function providerConfigSnapshot(config: KimiConfig, providerId: string): string {
return JSON.stringify(config.providers[providerId] ?? null);
}

function providerConfigEqual(config: KimiConfig, nextConfig: KimiConfig, providerId: string): boolean {
return providerConfigSnapshot(config, providerId) === providerConfigSnapshot(nextConfig, providerId);
}

function providerRefreshAliasKeys(
config: KimiConfig,
nextConfig: KimiConfig,
Expand Down Expand Up @@ -199,6 +238,15 @@ function clampDanglingDefault(config: KimiConfig): void {
}
}

function clearDefaultThinkingWhenDefaultRemoved(
config: KimiConfig,
previousDefaultModel: string | undefined,
): void {
if (previousDefaultModel !== undefined && config.defaultModel === undefined) {
config.defaultThinking = undefined;
}
}

function pickDefaultModel(config: KimiConfig, providerId: string, models: Array<{ id: string }>): string {
const firstModel = models[0];
if (firstModel === undefined) return '';
Expand Down Expand Up @@ -263,6 +311,7 @@ export async function refreshAllProviderModels(host: RefreshProviderHost): Promi
);
restoreDefaultSelection(next, config.defaultModel, config.defaultThinking);
clampDanglingDefault(next);
clearDefaultThinkingWhenDefaultRemoved(next, config.defaultModel);

if (providerModelsEqual(config, next, KIMI_CODE_PROVIDER_NAME, refreshedAliasKeys)) {
unchanged.push(KIMI_CODE_PROVIDER_NAME);
Expand Down Expand Up @@ -332,6 +381,7 @@ export async function refreshAllProviderModels(host: RefreshProviderHost): Promi
restoreProviderAliases(next, preserveUserProviderAliases(config, providerId, refreshedAliasKeys));
restoreDefaultSelection(next, config.defaultModel, config.defaultThinking);
clampDanglingDefault(next);
clearDefaultThinkingWhenDefaultRemoved(next, config.defaultModel);

if (providerModelsEqual(config, next, providerId, refreshedAliasKeys)) {
unchanged.push(providerId);
Expand Down Expand Up @@ -363,26 +413,42 @@ export async function refreshAllProviderModels(host: RefreshProviderHost): Promi
}

// -------------------------------------------------------------------------
// 3. Custom Registry providers (grouped by {url, apiKey})
// 3. Custom Registry providers (grouped by URL, with API-key candidates)
// -------------------------------------------------------------------------
const customSources = new Map<string, { source: CustomRegistrySource; providerIds: string[] }>();
const customSources = new Map<
string,
{
readonly sources: CustomRegistrySource[];
readonly sourceKeys: Set<string>;
readonly providerIds: string[];
}
>();
for (const [providerId, providerConfig] of Object.entries(config.providers)) {
if (providerId === KIMI_CODE_PROVIDER_NAME) continue;
if (isOpenPlatformId(providerId)) continue;
const source = readCustomRegistrySource(providerConfig);
if (source === undefined) continue;
const key = `${source.url}${source.apiKey}`;
const key = customRegistrySourceKey(source);
const sourceKey = customRegistrySourceCredentialKey(source);
const entry = customSources.get(key);
if (entry !== undefined) {
if (!entry.sourceKeys.has(sourceKey)) {
entry.sources.push(source);
entry.sourceKeys.add(sourceKey);
}
entry.providerIds.push(providerId);
} else {
customSources.set(key, { source, providerIds: [providerId] });
customSources.set(key, {
sources: [source],
sourceKeys: new Set([sourceKey]),
providerIds: [providerId],
});
}
}

for (const { source, providerIds } of customSources.values()) {
for (const { sources, providerIds } of customSources.values()) {
try {
const entries = await fetchCustomRegistry(source);
const { entries, source } = await fetchCustomRegistryFromSources(sources);
// Build the whole batch on one clone so that several changed providers
// from the same source do not overwrite each other's aliases, and so the
// config we compare is exactly the config we persist.
Expand All @@ -393,17 +459,47 @@ export async function refreshAllProviderModels(host: RefreshProviderHost): Promi
readonly added: number;
readonly removed: number;
}> = [];
const providersToRemoveBeforeSet = new Set<string>();
let hasUnreportedConfigChange = false;
const remoteEntries = Object.values(entries);
const remoteEntriesByProviderId = new Map(
remoteEntries.map((entry) => [entry.id, entry]),
);
const providerIdsToSync = new Set(providerIds);
for (const entry of remoteEntries) providerIdsToSync.add(entry.id);
Comment thread
liruifengv marked this conversation as resolved.

for (const providerId of providerIdsToSync) {
const entry = remoteEntriesByProviderId.get(providerId);
if (entry === undefined) {
const oldIds = collectModelIdsForAliases(config, providerAliasKeys(config, providerId));
removeCustomRegistryProvider(asManaged(next), providerId);
changedProviders.push({
providerId,
providerName: providerId,
added: 0,
removed: oldIds.size,
});
providersToRemoveBeforeSet.add(providerId);
continue;
}

for (const providerId of providerIds) {
const entry = entries[providerId];
if (entry === undefined) continue;

const existed = config.providers[providerId] !== undefined;
applyCustomRegistryProvider(asManaged(next), entry, source);
const refreshedAliasKeys = providerRefreshAliasKeys(config, next, providerId, `${providerId}/`);
restoreProviderAliases(next, preserveUserProviderAliases(config, providerId, refreshedAliasKeys));
if (existed) {
restoreProviderAliases(next, preserveUserProviderAliases(config, providerId, refreshedAliasKeys));
}

if (providerModelsEqual(config, next, providerId, refreshedAliasKeys)) {
if (
existed &&
providerModelsEqual(config, next, providerId, refreshedAliasKeys) &&
providerConfigEqual(config, next, providerId)
) {
unchanged.push(providerId);
} else if (existed && providerModelsEqual(config, next, providerId, refreshedAliasKeys)) {
unchanged.push(providerId);
providersToRemoveBeforeSet.add(providerId);
hasUnreportedConfigChange = true;
} else {
const { added, removed } = computeChanges(
collectModelIdsForAliases(config, refreshedAliasKeys),
Expand All @@ -415,13 +511,15 @@ export async function refreshAllProviderModels(host: RefreshProviderHost): Promi
added,
removed,
});
if (existed) providersToRemoveBeforeSet.add(providerId);
}
}

if (changedProviders.length > 0) {
if (changedProviders.length > 0 || hasUnreportedConfigChange) {
restoreDefaultSelection(next, config.defaultModel, config.defaultThinking);
clampDanglingDefault(next);
for (const { providerId } of changedProviders) {
clearDefaultThinkingWhenDefaultRemoved(next, config.defaultModel);
for (const providerId of providersToRemoveBeforeSet) {
await host.removeProvider(providerId);
}
config = await host.setConfig({
Expand All @@ -431,7 +529,12 @@ export async function refreshAllProviderModels(host: RefreshProviderHost): Promi
defaultThinking: next.defaultThinking,
});
for (const change of changedProviders) {
changed.push(change);
changed.push({
providerId: change.providerId,
providerName: change.providerName,
added: change.added,
removed: change.removed,
});
}
}
} catch (error) {
Expand Down
Loading
Loading