Skip to content
Open
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/websearch-services-on-refresh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Provision the managed Kimi Code search/fetch services during provider refresh so the WebSearch tool works after login without a manual `config.toml` edit. Previously the services block was only written on an explicit login, leaving upgraded clients with WebSearch silently disabled.
24 changes: 24 additions & 0 deletions apps/kimi-code/src/tui/utils/refresh-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,19 @@ function asManaged(config: KimiConfig): ManagedKimiConfigShape {
return config as unknown as ManagedKimiConfigShape;
}

// The managed Kimi Code login provisions `services.moonshot_search` /
// `services.moonshot_fetch` — the endpoints behind the WebSearch and URL-fetch
Comment on lines +95 to +96
// tools. A client is "missing" them when either lacks a baseUrl, e.g. it logged
// in on a build that predates service provisioning. Without them WebSearch is
// silently disabled until the user hand-edits config.toml, so a refresh
// backfills them.
function managedServicesMissing(config: KimiConfig): boolean {
return (
config.services?.moonshotSearch?.baseUrl === undefined ||
config.services?.moonshotFetch?.baseUrl === undefined
);
}

function collectModelIdsForAliases(config: KimiConfig, aliasKeys: ReadonlySet<string>): Set<string> {
const ids = new Set<string>();
for (const aliasKey of aliasKeys) {
Expand Down Expand Up @@ -324,6 +337,13 @@ export async function refreshAllProviderModels(
clearDefaultThinkingWhenDefaultRemoved(next, config.defaultModel);

if (providerModelsEqual(config, next, KIMI_CODE_PROVIDER_NAME, refreshedAliasKeys)) {
// Models are identical, but a client provisioned before the managed
// search/fetch services existed has no `services` block — which leaves
// the WebSearch tool disabled. Backfill the services so the refresh
// repairs it, without emitting a spurious model-change report.
if (managedServicesMissing(config)) {
config = await host.setConfig({ services: next.services });

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 Backfill only missing managed service entries

When only one managed service is missing, this patches both generated services from applyManagedKimiCodeConfig, whose defaults include apiKey: ''. Because setConfig merges object patches, a config with services.moonshot_search.base_url plus a user api_key but no fetch service will have the existing search key overwritten while trying to backfill fetch; build the patch per missing service or preserve existing service fields.

Useful? React with 👍 / 👎.

}
unchanged.push(KIMI_CODE_PROVIDER_NAME);
} else {
const { added, removed } = computeChanges(
Expand All @@ -336,6 +356,10 @@ export async function refreshAllProviderModels(
models: next.models,
defaultModel: next.defaultModel,
defaultThinking: next.defaultThinking,
// Persist the managed search/fetch services alongside the model
// refresh; omitting them here is what left upgraded clients with a
// non-functional WebSearch tool.
services: next.services,

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 Preserve service credentials on model refresh

In the model-changing path, every managed model refresh now sends the freshly generated next.services, which resets service fields to the managed defaults (apiKey: '' and no custom headers). For users who already configured services.moonshot_search or services.moonshot_fetch with a manual key/header fallback, the next model-list change will silently clobber those settings; merge only missing/generated endpoint data while carrying over existing service credentials.

Useful? React with 👍 / 👎.

});
changed.push({
providerId: KIMI_CODE_PROVIDER_NAME,
Expand Down
145 changes: 142 additions & 3 deletions apps/kimi-code/test/tui/utils/refresh-providers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,17 +113,156 @@ describe('refreshAllProviderModels', () => {
});
vi.stubGlobal('fetch', fetchMock);

const host = makeRefreshHost(config);
const result = await refreshAllProviderModels({
getConfig: async () => config,
removeProvider: vi.fn(),
setConfig: vi.fn(),
getConfig: async () => host.current(),
removeProvider: host.removeProvider,
setConfig: host.setConfig,
resolveOAuthToken,
});

expect(result.failed).toEqual([]);
expect(result.unchanged).toEqual([KIMI_CODE_PROVIDER_NAME]);
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(resolveOAuthToken).toHaveBeenCalledWith(KIMI_CODE_PROVIDER_NAME, envOauthRef);
// An unchanged-models refresh still backfills the managed search/fetch
// services so the WebSearch tool works without a manual config edit.
expect(host.removeProvider).not.toHaveBeenCalled();
expect(host.current().services?.moonshotSearch).toEqual({
baseUrl: `${envBaseUrl}/search`,
apiKey: '',
oauth: envOauthRef,
});
expect(host.current().services?.moonshotFetch).toEqual({
baseUrl: `${envBaseUrl}/fetch`,
apiKey: '',
oauth: envOauthRef,
});
});

it('provisions managed search/fetch services when a model-changing refresh persists', async () => {
const baseUrl = 'https://api.example.test/coding/v1';
const oauthRef = resolveKimiCodeOAuthRef({ baseUrl });
const config: KimiConfig = {
providers: {
[KIMI_CODE_PROVIDER_NAME]: {
type: 'kimi',
baseUrl,
apiKey: '',
oauth: oauthRef,
},
},
models: {
'kimi-code/kimi-for-coding': {
provider: KIMI_CODE_PROVIDER_NAME,
model: 'kimi-for-coding',
maxContextSize: 262144,
capabilities: ['thinking', 'tool_use'],
},
},
defaultModel: 'kimi-code/kimi-for-coding',
telemetry: true,
};
const host = makeRefreshHost(config);
const fetchMock = vi.fn<FetchMock>(
async () =>
new Response(
JSON.stringify({
data: [
{
id: 'kimi-deep-coder',
context_length: 262144,
supports_reasoning: true,
},
],
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } },
),
);
vi.stubGlobal('fetch', fetchMock);

const result = await refreshAllProviderModels({
getConfig: async () => host.current(),
removeProvider: host.removeProvider,
setConfig: host.setConfig,
resolveOAuthToken: vi.fn(async () => 'oauth-access-token'),
});

expect(result.failed).toEqual([]);
expect(result.changed).toEqual([
{ providerId: KIMI_CODE_PROVIDER_NAME, providerName: 'Kimi Code', added: 1, removed: 1 },
]);
expect(host.removeProvider).toHaveBeenCalledWith(KIMI_CODE_PROVIDER_NAME);
expect(host.current().services?.moonshotSearch).toEqual({
baseUrl: `${baseUrl}/search`,
apiKey: '',
oauth: oauthRef,
});
expect(host.current().services?.moonshotFetch).toEqual({
baseUrl: `${baseUrl}/fetch`,
apiKey: '',
oauth: oauthRef,
});
});

it('leaves already-provisioned managed services untouched on an unchanged refresh', async () => {
const baseUrl = 'https://api.example.test/coding/v1';
const oauthRef = resolveKimiCodeOAuthRef({ baseUrl });
const config: KimiConfig = {
providers: {
[KIMI_CODE_PROVIDER_NAME]: {
type: 'kimi',
baseUrl,
apiKey: '',
oauth: oauthRef,
},
},
models: {
'kimi-code/kimi-for-coding': {
provider: KIMI_CODE_PROVIDER_NAME,
model: 'kimi-for-coding',
maxContextSize: 262144,
capabilities: ['thinking', 'tool_use'],
},
},
defaultModel: 'kimi-code/kimi-for-coding',
services: {
moonshotSearch: { baseUrl: `${baseUrl}/search`, apiKey: 'sk-user-key', oauth: oauthRef },
moonshotFetch: { baseUrl: `${baseUrl}/fetch`, apiKey: 'sk-user-key', oauth: oauthRef },
},
telemetry: true,
};
const host = makeRefreshHost(config);
const fetchMock = vi.fn<FetchMock>(
async () =>
new Response(
JSON.stringify({
data: [
{
id: 'kimi-for-coding',
context_length: 262144,
supports_reasoning: true,
},
],
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } },
),
);
vi.stubGlobal('fetch', fetchMock);

const result = await refreshAllProviderModels({
getConfig: async () => host.current(),
removeProvider: host.removeProvider,
setConfig: host.setConfig,
resolveOAuthToken: vi.fn(async () => 'oauth-access-token'),
});

expect(result.failed).toEqual([]);
expect(result.unchanged).toEqual([KIMI_CODE_PROVIDER_NAME]);
// Services already present (and carrying a user-set api_key) must not be
// rewritten — no setConfig churn, no clobbering the manual key.
expect(host.setConfig).not.toHaveBeenCalled();
expect(host.current().services?.moonshotSearch?.apiKey).toBe('sk-user-key');
});

it('can refresh only the managed OAuth provider without fetching third-party registries', async () => {
Expand Down