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/media-registrar-stale-alias.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix an `[unexpected] Error2: Model "<alias>" is not configured in config.toml` error printed on startup when a restored session references a model that is no longer configured (e.g. after logging out of the managed Kimi Code account). Media tool registration now degrades gracefully instead of throwing from the `agent.status.updated` listener.
17 changes: 15 additions & 2 deletions packages/agent-core-v2/src/agent/media/mediaToolsRegistrar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@
* converts `video_url` takes the inline fallback when no upload hook
* exists.
*
* The alias re-resolved on every refresh comes from persisted profile
* state that resume replays without catalog validation, so it may no
* longer resolve (e.g. its config.toml entry was removed on logout).
* A failed resolution degrades to "no model" — registration proceeds
* from the profile-reported capabilities without a model-bound video
* uploader — instead of throwing out of the event listener, where the
* escape would surface as an `[unexpected]` error.
*
* The plain-data state (`registeredKey`) is registered into `agentState`
* (`IAgentStateService`) and read/written through it; `registration` stays an
* instance field (the live `IDisposable` tool-registration handle, not plain
Expand Down Expand Up @@ -124,8 +132,13 @@ export class AgentMediaToolsRegistrar extends Service implements IAgentMediaTool
let requester: ModelRequester | undefined;
let model: Model | undefined;
if (modelAlias !== '') {
requester = this.modelCatalog.getRequester(modelAlias);
model = requester.model;
try {
requester = this.modelCatalog.getRequester(modelAlias);
model = requester.model;
} catch {
requester = undefined;
model = undefined;
}
}
this.registration = registerMediaTools(this.toolRegistry, {
runtime,
Expand Down
43 changes: 38 additions & 5 deletions packages/agent-core-v2/test/agent/media/tools/read-media.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@

import * as posixPath from 'node:path/posix';

import type { ModelCapability } from '#/kosong/contract/capability';
import { UNKNOWN_CAPABILITY, type ModelCapability } from '#/kosong/contract/capability';
import type { ContentPart } from '#/kosong/contract/message';
import { VideoUploadUnsupportedError } from '#/kosong/contract/errors';
import { Jimp } from 'jimp';
import { afterEach, describe, expect, it, vi } from 'vitest';

import { Emitter } from '#/_base/event';
import {
resetUnexpectedErrorHandler,
setUnexpectedErrorHandler,
} from '#/_base/errors/unexpectedError';
import type { IHostFileSystem } from '#/os/interface/hostFileSystem';
import type { IHostEnvironment } from '#/os/interface/hostEnvironment';
import type { IAgentRuntimeService } from '#/agent/runtimeBinding/agentRuntime';
Expand Down Expand Up @@ -872,10 +876,14 @@ describe('AgentMediaToolsRegistrar', () => {
getModelCapabilities: () => state.capabilities,
getModel: () => state.alias,
} as unknown as IAgentProfileService;
const brokenAliases = new Set<string>();
const modelCatalog = {
getRequester: (id: string) => ({
model: { id, name: id, providerName: 'test', protocol: 'openai' },
}),
getRequester: (id: string) => {
if (brokenAliases.has(id)) {
throw new Error(`Model "${id}" is not configured in config.toml.`);
}
return { model: { id, name: id, providerName: 'test', protocol: 'openai' } };
},
} as unknown as IModelCatalog;
const workspaceCtx = {
workDir: '/workspace',
Expand Down Expand Up @@ -914,7 +922,13 @@ describe('AgentMediaToolsRegistrar', () => {
runtimeAvailable = available;
runtimeChanges.fire();
};
return { registry, registrar, bindModel, setRuntimeAvailable };
const breakAlias = (alias: string): void => {
brokenAliases.add(alias);
};
const healAlias = (alias: string): void => {
brokenAliases.delete(alias);
};
return { registry, registrar, bindModel, setRuntimeAvailable, breakAlias, healAlias };
}

it('registers nothing until a media-capable model binds, then registers ReadMediaFile', () => {
Expand Down Expand Up @@ -968,6 +982,25 @@ describe('AgentMediaToolsRegistrar', () => {
expect(registry.resolve('ReadMediaFile')).toBe(first);
});

it('survives an unconfigured bound alias and recovers when it resolves again', () => {
const unexpected: unknown[] = [];
setUnexpectedErrorHandler((err) => unexpected.push(err));
try {
const { registry, bindModel, breakAlias, healAlias } = createRegistrarHarness();
breakAlias('stale-model');
bindModel('stale-model', UNKNOWN_CAPABILITY);
expect(unexpected).toHaveLength(0);
expect(registry.resolve('ReadMediaFile')).toBeUndefined();

healAlias('stale-model');
bindModel('stale-model', capabilities({ image_in: true, video_in: true }));
expect(registry.resolve('ReadMediaFile')).toBeInstanceOf(ReadMediaFileTool);
expect(unexpected).toHaveLength(0);
} finally {
resetUnexpectedErrorHandler();
}
});

it('unregisters on dispose', () => {
const { registry, registrar, bindModel } = createRegistrarHarness();
bindModel('vision-model', capabilities({ image_in: true, video_in: true }));
Expand Down
Loading