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/web-model-picker-full-refresh.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 4 additions & 2 deletions apps/kimi-web/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -360,8 +360,10 @@ async function openModelPicker(): Promise<void> {
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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh the quick model menu before showing it

When the daemon's scheduled/startup catalog refresh adds models after the initial loadModels(), the current server no longer sends event.model_catalog.changed, and the composer model pill still opens its quick dropdown directly from client.models.value without calling openModelPicker(); only the “More models” row reaches this refreshed path. That leaves the quick-switch list stale until the user opens the full picker or reloads, which regresses the cache-warming behavior being removed here. Please trigger the same refresh on the composer dropdown-open path, or keep another cache refresh for scheduled catalog changes.

Useful? React with 👍 / 👎.

} catch {
modelsUnavailable.value = true;
} finally {
Expand Down
14 changes: 0 additions & 14 deletions apps/kimi-web/src/composables/client/useModelProviderState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,19 +192,6 @@ export function useModelProviderState(
}
}

async function refreshOAuthProviderModels(): Promise<void> {
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<void> {
try {
Expand Down Expand Up @@ -460,7 +447,6 @@ export function useModelProviderState(
loadSkillsForSession,
loadSkillsForWorkspace,
loadModels,
refreshOAuthProviderModels,
loadProviders,
setModel,
toggleStarModel,
Expand Down
1 change: 0 additions & 1 deletion apps/kimi-web/src/composables/useKimiWebClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2915,7 +2915,6 @@ export function useKimiWebClient() {
resolveImageUrl: workspaceState.resolveImageUrl,

// Model + Provider actions
refreshOAuthProviderModels: modelProvider.refreshOAuthProviderModels,
loadModels: modelProvider.loadModels,
loadProviders: modelProvider.loadProviders,
skills,
Expand Down
12 changes: 0 additions & 12 deletions packages/kap-server/src/protocol/events-zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -911,7 +900,6 @@ export const agentEventSchema = z.discriminatedUnion('type', [
workspaceDeletedEventSchema,
sessionWorkChangedEventSchema,
sessionStatusChangedEventSchema,
modelCatalogChangedEventSchema,
goalUpdatedEventSchema,
skillActivatedEventSchema,
pluginCommandActivatedEventSchema,
Expand Down
20 changes: 1 addition & 19 deletions packages/kap-server/src/transport/ws/v1/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -185,7 +168,6 @@ export type AgentEvent =
| SessionWorkChangedEvent
| SessionStatusChangedEvent
| ConfigChangedEvent
| ModelCatalogChangedEvent
| PromptSubmittedEvent
| BackgroundTaskStartedEvent
| BackgroundTaskTerminatedEvent;
Expand Down
49 changes: 5 additions & 44 deletions packages/kap-server/src/transport/ws/v1/sessionEventBroadcaster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.')
);
}

Expand All @@ -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
Expand Down Expand Up @@ -1043,25 +1023,6 @@ function interactionResolvedEvent(
}
}

function modelCatalogChangedPayload(
payload: unknown,
): Pick<ModelCatalogChangedEvent, 'changed' | 'unchanged' | 'failed'> | undefined {
if (typeof payload !== 'object' || payload === null) return undefined;
const candidate = payload as Partial<ModelCatalogChangedEvent>;
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
Expand Down
29 changes: 0 additions & 29 deletions packages/kap-server/test/sessionEventBroadcaster.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading