From 773724dc8abf0982e73e8974718672ad7b447259 Mon Sep 17 00:00:00 2001 From: "haozhe.yang" Date: Thu, 16 Jul 2026 16:20:57 +0800 Subject: [PATCH] refactor(model-catalog): drop WS catalog-changed event; refresh on picker open - kap-server: remove event.model_catalog.changed from the v1 WS union, broadcaster forwarding, and the zod event registry - web: refresh all providers (POST /providers:refresh) before loading models when the model picker opens, replacing the event-driven refresh - keep domain publishers, the protocol schema, and the web receiver for compatibility with older daemons --- .changeset/web-model-picker-full-refresh.md | 5 ++ apps/kimi-web/src/App.vue | 6 ++- .../client/useModelProviderState.ts | 14 ------ .../src/composables/useKimiWebClient.ts | 1 - .../kap-server/src/protocol/events-zod.ts | 12 ----- .../kap-server/src/transport/ws/v1/events.ts | 20 +------- .../ws/v1/sessionEventBroadcaster.ts | 49 ++----------------- .../test/sessionEventBroadcaster.test.ts | 29 ----------- 8 files changed, 15 insertions(+), 121 deletions(-) create mode 100644 .changeset/web-model-picker-full-refresh.md diff --git a/.changeset/web-model-picker-full-refresh.md b/.changeset/web-model-picker-full-refresh.md new file mode 100644 index 0000000000..dd1e098617 --- /dev/null +++ b/.changeset/web-model-picker-full-refresh.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Refresh the model catalog for all providers when opening the model picker, so newly available models always show up. diff --git a/apps/kimi-web/src/App.vue b/apps/kimi-web/src/App.vue index 6585832c45..5708bcc9bf 100644 --- a/apps/kimi-web/src/App.vue +++ b/apps/kimi-web/src/App.vue @@ -360,8 +360,10 @@ async function openModelPicker(): Promise { modelsUnavailable.value = false; showModelPicker.value = true; try { - await client.refreshOAuthProviderModels(); - await client.loadModels(); + // Full refresh first (every refreshable provider, not just OAuth), so the + // list always reflects the live catalog — the WS model-catalog event that + // used to keep the cache warm is no longer forwarded by the daemon. + await client.refreshAllProviders(); } catch { modelsUnavailable.value = true; } finally { diff --git a/apps/kimi-web/src/composables/client/useModelProviderState.ts b/apps/kimi-web/src/composables/client/useModelProviderState.ts index 98227e68f8..4c56916c3b 100644 --- a/apps/kimi-web/src/composables/client/useModelProviderState.ts +++ b/apps/kimi-web/src/composables/client/useModelProviderState.ts @@ -192,19 +192,6 @@ export function useModelProviderState( } } - async function refreshOAuthProviderModels(): Promise { - try { - const result = await getKimiWebApi().refreshOAuthProviderModels(); - for (const failure of result.failed) { - pushOperationFailure('refreshOAuthProviderModels', new Error(failure.reason), { - message: failure.provider, - }); - } - } catch { - // Older daemons may not expose this endpoint; model listing still works. - } - } - /** Load providers */ async function loadProviders(): Promise { try { @@ -460,7 +447,6 @@ export function useModelProviderState( loadSkillsForSession, loadSkillsForWorkspace, loadModels, - refreshOAuthProviderModels, loadProviders, setModel, toggleStarModel, diff --git a/apps/kimi-web/src/composables/useKimiWebClient.ts b/apps/kimi-web/src/composables/useKimiWebClient.ts index 06dd098c6b..063319c599 100644 --- a/apps/kimi-web/src/composables/useKimiWebClient.ts +++ b/apps/kimi-web/src/composables/useKimiWebClient.ts @@ -2915,7 +2915,6 @@ export function useKimiWebClient() { resolveImageUrl: workspaceState.resolveImageUrl, // Model + Provider actions - refreshOAuthProviderModels: modelProvider.refreshOAuthProviderModels, loadModels: modelProvider.loadModels, loadProviders: modelProvider.loadProviders, skills, diff --git a/packages/kap-server/src/protocol/events-zod.ts b/packages/kap-server/src/protocol/events-zod.ts index abb13c3524..9b66bd1c2f 100644 --- a/packages/kap-server/src/protocol/events-zod.ts +++ b/packages/kap-server/src/protocol/events-zod.ts @@ -77,10 +77,6 @@ import type { import type { UsageStatus } from '@moonshot-ai/agent-core-v2/agent/usage/usage'; import type { FinishReason } from '@moonshot-ai/agent-core-v2/app/llmProtocol/finishReason'; import type { TokenUsage } from '@moonshot-ai/agent-core-v2/app/llmProtocol/usage'; -import { - providerRefreshChangeSchema, - providerRefreshFailureSchema, -} from '@moonshot-ai/agent-core-v2/app/modelCatalog/modelCatalog'; import type { SubagentCompletedEvent, SubagentFailedEvent, @@ -592,13 +588,6 @@ export const configChangedEventSchema = z.object({ config: configResponseSchema, }); -export const modelCatalogChangedEventSchema = z.object({ - type: z.literal('event.model_catalog.changed'), - changed: z.array(providerRefreshChangeSchema), - unchanged: z.array(z.string().min(1)), - failed: z.array(providerRefreshFailureSchema), -}); - export const goalUpdatedEventSchema = z.object({ type: z.literal('goal.updated'), snapshot: goalSnapshotSchema.nullable(), @@ -911,7 +900,6 @@ export const agentEventSchema = z.discriminatedUnion('type', [ workspaceDeletedEventSchema, sessionWorkChangedEventSchema, sessionStatusChangedEventSchema, - modelCatalogChangedEventSchema, goalUpdatedEventSchema, skillActivatedEventSchema, pluginCommandActivatedEventSchema, diff --git a/packages/kap-server/src/transport/ws/v1/events.ts b/packages/kap-server/src/transport/ws/v1/events.ts index 461211c162..0ce5c16b6d 100644 --- a/packages/kap-server/src/transport/ws/v1/events.ts +++ b/packages/kap-server/src/transport/ws/v1/events.ts @@ -4,7 +4,7 @@ * Most frames are the engine's own `DomainEvent`s (turn / tool / subagent / * compaction / mcp / …), re-exported here as the stream's backbone. The * remaining interfaces are the v1-only frames this transport synthesizes - * (session/workspace lifecycle, config/model-catalog changes, the merged + * (session/workspace lifecycle, config changes, the merged * legacy status overlay, and the legacy background-task spellings) — they * never had an engine-side producer, so they are defined here, next to the * broadcaster that emits them. @@ -14,10 +14,6 @@ import type { DomainEvent } from '@moonshot-ai/agent-core-v2/app/event/eventBus' import type { MessageContent } from '@moonshot-ai/agent-core-v2/agent/contextMemory/protocolMessage'; import type { PermissionMode } from '@moonshot-ai/agent-core-v2/agent/permissionPolicy/types'; import type { UsageStatus } from '@moonshot-ai/agent-core-v2/agent/usage/usage'; -import type { - ProviderRefreshChange, - ProviderRefreshFailure, -} from '@moonshot-ai/agent-core-v2/app/modelCatalog/modelCatalog'; import type { AgentPhase } from '../../../services/legacyStatus/legacyStatus'; import type { ConfigResponse } from '../../../protocol/rest-config'; import type { Session, SessionPendingInteraction } from '../../../protocol/session'; @@ -92,19 +88,6 @@ export interface ConfigChangedEvent { readonly config: ConfigResponse; } -/** - * Pushed when the daemon refreshes provider model metadata (manual or - * scheduled) and the effective catalog changed. Carries the per-provider - * diff so clients can both refresh their model/provider caches and surface a - * summary ("3 models added") without re-diffing the whole config. - */ -export interface ModelCatalogChangedEvent { - readonly type: 'event.model_catalog.changed'; - readonly changed: readonly ProviderRefreshChange[]; - readonly unchanged: readonly string[]; - readonly failed: readonly ProviderRefreshFailure[]; -} - export interface PromptSubmittedEvent { readonly type: 'prompt.submitted'; readonly promptId: string; @@ -185,7 +168,6 @@ export type AgentEvent = | SessionWorkChangedEvent | SessionStatusChangedEvent | ConfigChangedEvent - | ModelCatalogChangedEvent | PromptSubmittedEvent | BackgroundTaskStartedEvent | BackgroundTaskTerminatedEvent; diff --git a/packages/kap-server/src/transport/ws/v1/sessionEventBroadcaster.ts b/packages/kap-server/src/transport/ws/v1/sessionEventBroadcaster.ts index 44f6613186..1fb4ff5e43 100644 --- a/packages/kap-server/src/transport/ws/v1/sessionEventBroadcaster.ts +++ b/packages/kap-server/src/transport/ws/v1/sessionEventBroadcaster.ts @@ -52,12 +52,7 @@ import { MAIN_AGENT_ID, } from '@moonshot-ai/agent-core-v2'; import type { TurnEndReason } from '@moonshot-ai/agent-core-v2/agent/loop/turnEvents'; -import type { - ModelCatalogChangedEvent, - SessionCreatedEvent, - SessionMetaUpdatedEvent, - Event, -} from './events'; +import type { SessionCreatedEvent, SessionMetaUpdatedEvent, Event } from './events'; import { isVolatileEventType } from './events'; import type { SessionCursor } from '../../../protocol/ws-control'; import type { InFlightTurn, SnapshotSubagent } from '../../../protocol/rest-snapshot'; @@ -420,20 +415,6 @@ export class SessionEventBroadcaster { } private onCoreEvent(event: GlobalEvent): void { - if (event.type === 'event.model_catalog.changed') { - const payload = modelCatalogChangedPayload(event.payload); - if (payload === undefined) return; - const modelEvent: ModelCatalogChangedEvent = { - type: 'event.model_catalog.changed', - ...payload, - }; - void this.dispatchGlobal({ - ...modelEvent, - agentId: 'main', - sessionId: GLOBAL_SESSION_ID, - }); - return; - } if (event.type === 'event.session.created') { const payload = sessionCreatedPayload(event.payload); if (payload === undefined) return; @@ -803,7 +784,7 @@ export class SessionEventBroadcaster { } if (isGlobalEvent(event.type)) { - // Global events (session/workspace/config/model-catalog) are not agent + // Global events (session/workspace/config) are not agent // events — fan out to every subscriber regardless of any agent filter. for (const target of this.allTargets()) { try { @@ -928,14 +909,13 @@ function legacyTaskEvent(event: DomainEvent, agentId: string, sessionId: string) return { ...event, type: legacyType, agentId, sessionId } as unknown as Event; } -/** Session/workspace/config/model-catalog events are broadcast to every connection. */ +/** Session/workspace/config events are broadcast to every connection. */ function isGlobalEvent(type: string): boolean { return ( type === 'session.meta.updated' || type.startsWith('event.session.') || type.startsWith('event.workspace.') || - type.startsWith('event.config.') || - type.startsWith('event.model_catalog.') + type.startsWith('event.config.') ); } @@ -945,7 +925,7 @@ function isGlobalEvent(type: string): boolean { * `filter`: * - `filter === undefined` → receive every agent (legacy session-grained * behavior); - * - global events (session/workspace/config/model-catalog) are not agent + * - global events (session/workspace/config) are not agent * events and always pass; * - events without a string `agentId` (should not happen on the v1 wire, * where the broadcaster stamps every event) pass defensively rather than @@ -1043,25 +1023,6 @@ function interactionResolvedEvent( } } -function modelCatalogChangedPayload( - payload: unknown, -): Pick | undefined { - if (typeof payload !== 'object' || payload === null) return undefined; - const candidate = payload as Partial; - if ( - !Array.isArray(candidate.changed) || - !Array.isArray(candidate.unchanged) || - !Array.isArray(candidate.failed) - ) { - return undefined; - } - return { - changed: candidate.changed, - unchanged: candidate.unchanged, - failed: candidate.failed, - }; -} - /** * Validate the `session.meta.updated` payload published on the core * `IEventService`. Both the first-prompt auto-title path diff --git a/packages/kap-server/test/sessionEventBroadcaster.test.ts b/packages/kap-server/test/sessionEventBroadcaster.test.ts index 48e6bb9f62..0659546654 100644 --- a/packages/kap-server/test/sessionEventBroadcaster.test.ts +++ b/packages/kap-server/test/sessionEventBroadcaster.test.ts @@ -499,35 +499,6 @@ describe('SessionEventBroadcaster', () => { expect(next.subagents).toEqual([]); }); - it('fans core model-catalog changes out to every session subscriber', async () => { - const lc = new FakeLifecycle(); - lc.addAgent('main'); - sessions.set('s1', lc); - const { target, envelopes } = collectingTarget(); - await bc.subscribe('s1', target); - - eventBus.emit({ - type: 'event.model_catalog.changed', - payload: { - changed: [{ provider_id: 'managed:kimi-code', provider_name: 'Kimi Code', added: 1, removed: 0 }], - unchanged: [], - failed: [], - }, - }); - - await vi.waitFor(() => expect(envelopes).toHaveLength(1)); - expect(envelopes[0]).toMatchObject({ - type: 'event.model_catalog.changed', - seq: 1, - session_id: '__global__', - payload: { - type: 'event.model_catalog.changed', - agentId: 'main', - sessionId: '__global__', - }, - }); - }); - it('subscribe returns false for an unknown session', async () => { const { target } = collectingTarget(); expect(await bc.subscribe('nope', target)).toBe(false);