mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-22 15:16:07 +00:00
* feat(server): auto-refresh provider models and push change events
- add scheduled provider-model refresh in the daemon (configurable
interval + refresh-on-start) plus manual endpoints:
POST /providers:refresh and POST /providers/{id}:refresh
- publish global event.model_catalog.changed when a refresh changes
the catalog so connected clients can resync
- extract the refresh orchestrator into @moonshot-ai/kimi-code-oauth so
the CLI and server share managed/open-platform/custom-registry logic
- wire the web daemon client to the new refresh endpoints
* chore: add changeset for provider model auto-refresh
* fix(web): reload model and provider caches on catalog change events
When the daemon's scheduled refresh changes the catalog, the pushed
event.model_catalog.changed only advanced the websocket sequence, leaving
the web composer's model/provider refs stale until an unrelated reload.
Reload both caches when the event arrives.
* test(sdk): cover event.model_catalog.changed in event exhaustiveness
116 lines
3.8 KiB
TypeScript
116 lines
3.8 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import type { ICoreProcessService, ILogService, IModelCatalogService } from '@moonshot-ai/agent-core';
|
|
|
|
import { ModelCatalogRefreshScheduler } from '../src/services/modelCatalog/modelCatalogRefreshScheduler';
|
|
|
|
interface MockCatalog {
|
|
refreshProviderModels: ReturnType<typeof vi.fn>;
|
|
}
|
|
|
|
function makeCore(modelCatalog?: { refreshIntervalMs?: number; refreshOnStart?: boolean }): ICoreProcessService {
|
|
return {
|
|
_serviceBrand: undefined,
|
|
rpc: {
|
|
getKimiConfig: vi.fn(async () => ({ providers: {}, modelCatalog })),
|
|
},
|
|
ready: async () => undefined,
|
|
dispose: () => undefined,
|
|
} as unknown as ICoreProcessService;
|
|
}
|
|
|
|
function makeLogger(): ILogService {
|
|
const logger = {
|
|
_serviceBrand: undefined,
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
debug: vi.fn(),
|
|
trace: vi.fn(),
|
|
fatal: vi.fn(),
|
|
child: () => logger,
|
|
};
|
|
return logger as unknown as ILogService;
|
|
}
|
|
|
|
describe('ModelCatalogRefreshScheduler', () => {
|
|
let catalog: MockCatalog;
|
|
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
catalog = {
|
|
refreshProviderModels: vi.fn(async () => ({ changed: [], unchanged: [], failed: [] })),
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
it('refreshes on start and then on the configured interval', async () => {
|
|
const scheduler = new ModelCatalogRefreshScheduler(
|
|
catalog as unknown as IModelCatalogService,
|
|
makeCore({ refreshIntervalMs: 60_000, refreshOnStart: true }),
|
|
makeLogger(),
|
|
);
|
|
|
|
await scheduler.start();
|
|
// The startup refresh is fire-and-forget; wait for its microtask to land.
|
|
await vi.waitFor(() => {
|
|
expect(catalog.refreshProviderModels).toHaveBeenCalledTimes(1);
|
|
});
|
|
expect(catalog.refreshProviderModels).toHaveBeenCalledWith({ scope: 'all' });
|
|
|
|
await vi.advanceTimersByTimeAsync(60_000);
|
|
expect(catalog.refreshProviderModels).toHaveBeenCalledTimes(2);
|
|
|
|
scheduler.dispose();
|
|
await vi.advanceTimersByTimeAsync(120_000);
|
|
expect(catalog.refreshProviderModels).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('does nothing when interval is 0 and refreshOnStart is false', async () => {
|
|
const scheduler = new ModelCatalogRefreshScheduler(
|
|
catalog as unknown as IModelCatalogService,
|
|
makeCore({ refreshIntervalMs: 0, refreshOnStart: false }),
|
|
makeLogger(),
|
|
);
|
|
|
|
await scheduler.start();
|
|
await vi.advanceTimersByTimeAsync(600_000);
|
|
expect(catalog.refreshProviderModels).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('honors env overrides for interval and refresh-on-start', async () => {
|
|
vi.stubEnv('KIMI_CODE_MODEL_CATALOG_REFRESH_INTERVAL_MS', '1000');
|
|
vi.stubEnv('KIMI_CODE_MODEL_CATALOG_REFRESH_ON_START', '0');
|
|
const scheduler = new ModelCatalogRefreshScheduler(
|
|
catalog as unknown as IModelCatalogService,
|
|
// Config says 6h + refresh on start, but env should win.
|
|
makeCore({ refreshIntervalMs: 6 * 60 * 60 * 1000, refreshOnStart: true }),
|
|
makeLogger(),
|
|
);
|
|
|
|
await scheduler.start();
|
|
await vi.advanceTimersByTimeAsync(999);
|
|
expect(catalog.refreshProviderModels).not.toHaveBeenCalled();
|
|
|
|
await vi.advanceTimersByTimeAsync(1);
|
|
expect(catalog.refreshProviderModels).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('swallows refresh errors so a failing tick does not break the schedule', async () => {
|
|
catalog.refreshProviderModels.mockRejectedValue(new Error('network down'));
|
|
const scheduler = new ModelCatalogRefreshScheduler(
|
|
catalog as unknown as IModelCatalogService,
|
|
makeCore({ refreshIntervalMs: 1000, refreshOnStart: false }),
|
|
makeLogger(),
|
|
);
|
|
|
|
await scheduler.start();
|
|
await vi.advanceTimersByTimeAsync(1000);
|
|
await vi.advanceTimersByTimeAsync(1000);
|
|
expect(catalog.refreshProviderModels).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|