From ea0ccfbd7e637fc2bdd02d5ec50bfb3f57023864 Mon Sep 17 00:00:00 2001 From: Tower Test Date: Mon, 7 Sep 2026 18:11:32 +0800 Subject: [PATCH] feat(cli): apply cloud-recommended default thinking effort --- .changeset/recommended-default-effort.md | 5 + apps/kimi-code/src/constant/app.ts | 1 + apps/kimi-code/src/tui/kimi-tui.ts | 19 + apps/kimi-code/src/utils/paths.ts | 5 + .../src/utils/recommended-effort-config.ts | 47 ++ .../utils/recommended-effort-state-store.ts | 31 ++ .../kimi-code/src/utils/recommended-effort.ts | 72 +++ .../utils/recommended-effort-config.test.ts | 219 +++++++++ .../recommended-effort-state-store.test.ts | 66 +++ .../test/utils/recommended-effort.test.ts | 454 ++++++++++++++++++ .../src/app/config/configService.ts | 6 +- .../src/app/config/migrations.ts | 63 --- .../src/app/config/tomlWriteback.ts | 28 -- .../test/app/config/config.test.ts | 73 --- .../test/app/config/tomlWriteback.test.ts | 29 +- .../test/app/config/writeback.test.ts | 12 - packages/oauth/src/managed-usage.ts | 15 +- packages/oauth/test/managed-usage.test.ts | 7 +- 18 files changed, 938 insertions(+), 214 deletions(-) create mode 100644 .changeset/recommended-default-effort.md create mode 100644 apps/kimi-code/src/utils/recommended-effort-config.ts create mode 100644 apps/kimi-code/src/utils/recommended-effort-state-store.ts create mode 100644 apps/kimi-code/src/utils/recommended-effort.ts create mode 100644 apps/kimi-code/test/utils/recommended-effort-config.test.ts create mode 100644 apps/kimi-code/test/utils/recommended-effort-state-store.test.ts create mode 100644 apps/kimi-code/test/utils/recommended-effort.test.ts delete mode 100644 packages/agent-core-v2/src/app/config/migrations.ts diff --git a/.changeset/recommended-default-effort.md b/.changeset/recommended-default-effort.md new file mode 100644 index 00000000000..6a2f4fc1ff4 --- /dev/null +++ b/.changeset/recommended-default-effort.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Upgrade the default thinking effort to the recommended level for eligible users. diff --git a/apps/kimi-code/src/constant/app.ts b/apps/kimi-code/src/constant/app.ts index c5f7bf52b09..af5054f2fe5 100644 --- a/apps/kimi-code/src/constant/app.ts +++ b/apps/kimi-code/src/constant/app.ts @@ -65,6 +65,7 @@ export const KIMI_CODE_INPUT_HISTORY_DIR_NAME = 'user-history'; export const KIMI_CODE_BANNER_DIR_NAME = 'banner'; export const KIMI_CODE_BANNER_STATE_FILE_NAME = 'state.json'; export const KIMI_CODE_SURVEY_STATE_FILE_NAME = 'feedback-survey-state.json'; +export const KIMI_CODE_RECOMMENDED_EFFORT_STATE_FILE_NAME = 'recommended-effort-state.json'; // Managed Kimi auth provider key shared with OAuth/SDK config. export const DEFAULT_OAUTH_PROVIDER_NAME = 'managed:kimi-code'; diff --git a/apps/kimi-code/src/tui/kimi-tui.ts b/apps/kimi-code/src/tui/kimi-tui.ts index 6b2ab9d606d..d56ef47a623 100644 --- a/apps/kimi-code/src/tui/kimi-tui.ts +++ b/apps/kimi-code/src/tui/kimi-tui.ts @@ -40,6 +40,8 @@ import { copyTextToClipboard } from '#/utils/clipboard/clipboard-text'; import { appendInputHistory, loadInputHistory } from '#/utils/history/input-history'; import { openUrl } from '#/utils/open-url'; import { getInputHistoryFile } from '#/utils/paths'; +import { applyRecommendedEffort } from '#/utils/recommended-effort'; +import { getRecommendedEffortConfig } from '#/utils/recommended-effort-config'; import { detectFdPath, ensureFdPath } from '#/utils/process/fd-detect'; import { quoteShellArg } from '#/utils/shell-quote'; import { restoreTerminalModes } from '#/utils/terminal-restore'; @@ -728,6 +730,7 @@ export class KimiTUI { this.state.editorContainer.clear(); this.state.editorContainer.addChild(this.state.editor); this.state.ui.setFocus(this.state.editor); + this.applyRecommendedEffortInBackground(); return shouldReplayHistory; } @@ -774,6 +777,22 @@ export class KimiTUI { }); } + private applyRecommendedEffortInBackground(): void { + void this.backgroundRefreshPromise?.then(async () => { + await applyRecommendedEffort({ + fetchConfig: async () => + getRecommendedEffortConfig({ + accessToken: await this.harness.auth.getCachedAccessToken(), + }), + getConfig: () => this.harness.getConfig(), + setConfig: (patch) => this.harness.setConfig(patch), + track: (event, properties) => { + this.track(event, properties); + }, + }); + }); + } + private async refreshProviderModelsInBackground(): Promise { try { const result = await this.authFlow.refreshProviderModels(); diff --git a/apps/kimi-code/src/utils/paths.ts b/apps/kimi-code/src/utils/paths.ts index 856b8d8a341..0b147d1dcdc 100644 --- a/apps/kimi-code/src/utils/paths.ts +++ b/apps/kimi-code/src/utils/paths.ts @@ -21,6 +21,7 @@ import { KIMI_CODE_NATIVE_STAGED_STATE_FILE_NAME, KIMI_CODE_NATIVE_STAGING_DIR_NAME, KIMI_CODE_PLUGIN_UPDATE_NOTICE_STATE_FILE_NAME, + KIMI_CODE_RECOMMENDED_EFFORT_STATE_FILE_NAME, KIMI_CODE_SURVEY_STATE_FILE_NAME, KIMI_CODE_UPDATE_INSTALL_LOCK_FILE_NAME, KIMI_CODE_UPDATE_INSTALL_STATE_FILE_NAME, @@ -131,6 +132,10 @@ export function getSurveyStateFile(): string { return join(getDataDir(), KIMI_CODE_SURVEY_STATE_FILE_NAME); } +export function getRecommendedEffortStateFile(): string { + return join(getDataDir(), KIMI_CODE_RECOMMENDED_EFFORT_STATE_FILE_NAME); +} + /** * Return the user input history file for a given working directory. * Layout: `/user-history/.jsonl`. diff --git a/apps/kimi-code/src/utils/recommended-effort-config.ts b/apps/kimi-code/src/utils/recommended-effort-config.ts new file mode 100644 index 00000000000..5b06b3e6b2d --- /dev/null +++ b/apps/kimi-code/src/utils/recommended-effort-config.ts @@ -0,0 +1,47 @@ +import { z } from 'zod'; + +import { + getClientConfig, + peekClientConfig, + resetClientConfigCache, + type ClientConfigFetchOptions, +} from '#/utils/client-configs'; + +const CONFIG_NAME = 'recommended_effort'; + +export interface RecommendedEffortEntry { + version: number; + recommended_default_effort: string; +} + +export type RecommendedEffortConfig = Record; + +const recommendedEffortEntrySchema = z.object({ + version: z.number().int().min(0), + recommended_default_effort: z.string(), +}); + +const recommendedEffortConfigSchema = z + .record(z.string(), z.unknown()) + .transform((record): RecommendedEffortConfig => { + const config: RecommendedEffortConfig = {}; + for (const [model, rawEntry] of Object.entries(record)) { + const parsed = recommendedEffortEntrySchema.safeParse(rawEntry); + if (parsed.success) config[model] = parsed.data; + } + return config; + }); + +export async function getRecommendedEffortConfig( + options: ClientConfigFetchOptions = {}, +): Promise { + return getClientConfig(CONFIG_NAME, recommendedEffortConfigSchema, options); +} + +export function peekRecommendedEffortConfig(now?: number): RecommendedEffortConfig | undefined { + return peekClientConfig(CONFIG_NAME, recommendedEffortConfigSchema, now); +} + +export function resetRecommendedEffortConfigCache(): void { + resetClientConfigCache(CONFIG_NAME); +} diff --git a/apps/kimi-code/src/utils/recommended-effort-state-store.ts b/apps/kimi-code/src/utils/recommended-effort-state-store.ts new file mode 100644 index 00000000000..d93dc723a7d --- /dev/null +++ b/apps/kimi-code/src/utils/recommended-effort-state-store.ts @@ -0,0 +1,31 @@ +import { z } from 'zod'; + +import { getRecommendedEffortStateFile } from '#/utils/paths'; +import { readJsonFile, writeJsonFileSync } from '#/utils/persistence'; + +const RecommendedEffortStateSchema = z.record( + z.string(), + z.object({ + version: z.number().int().min(0), + applied_at: z.string(), + }), +); + +export type RecommendedEffortState = z.infer; + +export async function readRecommendedEffortState( + filePath: string = getRecommendedEffortStateFile(), +): Promise { + try { + return await readJsonFile(filePath, RecommendedEffortStateSchema, {}); + } catch { + return {}; + } +} + +export function writeRecommendedEffortState( + state: RecommendedEffortState, + filePath: string = getRecommendedEffortStateFile(), +): void { + writeJsonFileSync(filePath, RecommendedEffortStateSchema, state); +} diff --git a/apps/kimi-code/src/utils/recommended-effort.ts b/apps/kimi-code/src/utils/recommended-effort.ts new file mode 100644 index 00000000000..68b0df17fb6 --- /dev/null +++ b/apps/kimi-code/src/utils/recommended-effort.ts @@ -0,0 +1,72 @@ +import { isManagedKimiCodeBaseUrl } from '@moonshot-ai/kimi-code-oauth'; +import type { KimiConfig, KimiConfigPatch, ModelAlias } from '@moonshot-ai/kimi-code-sdk'; +import type { TelemetryProperties } from '@moonshot-ai/kimi-telemetry'; + +import { getRecommendedEffortStateFile } from '#/utils/paths'; +import type { RecommendedEffortConfig } from '#/utils/recommended-effort-config'; +import { + readRecommendedEffortState, + writeRecommendedEffortState, +} from '#/utils/recommended-effort-state-store'; + +export interface ApplyRecommendedEffortDeps { + fetchConfig: () => Promise; + getConfig: () => Promise; + setConfig: (patch: KimiConfigPatch) => Promise; + track: (event: string, properties?: TelemetryProperties) => void; + stateFile?: string; + now?: () => Date; +} + +function eligibleModelEntry(config: KimiConfig): ModelAlias | undefined { + if (config.thinking?.enabled === false) return undefined; + const alias = config.defaultModel; + if (alias === undefined) return undefined; + const entry = config.models?.[alias]; + if (entry === undefined) return undefined; + const baseUrl = entry.baseUrl ?? config.providers[entry.provider]?.baseUrl; + return isManagedKimiCodeBaseUrl(baseUrl) ? entry : undefined; +} + +export async function applyRecommendedEffort(deps: ApplyRecommendedEffortDeps): Promise { + try { + if (eligibleModelEntry(await deps.getConfig()) === undefined) return; + const cloud = await deps.fetchConfig(); + if (cloud === undefined) return; + + const config = await deps.getConfig(); + const modelEntry = eligibleModelEntry(config); + if (modelEntry === undefined) return; + const campaign = cloud[modelEntry.model]; + if (campaign === undefined) return; + const supportEfforts = modelEntry.overrides?.supportEfforts ?? modelEntry.supportEfforts; + if (!supportEfforts?.includes(campaign.recommended_default_effort)) return; + + const stateFile = deps.stateFile ?? getRecommendedEffortStateFile(); + const state = await readRecommendedEffortState(stateFile); + const applied = state[modelEntry.model]; + if (applied !== undefined && campaign.version <= applied.version) return; + + const previousEffort = config.thinking?.effort; + if (previousEffort !== campaign.recommended_default_effort) { + await deps.setConfig({ thinking: { effort: campaign.recommended_default_effort } }); + } + writeRecommendedEffortState( + { + ...state, + [modelEntry.model]: { + version: campaign.version, + applied_at: (deps.now?.() ?? new Date()).toISOString(), + }, + }, + stateFile, + ); + deps.track('recommended_effort_applied', { + model: modelEntry.model, + version: campaign.version, + effort: campaign.recommended_default_effort, + previous_effort: previousEffort, + }); + } catch { + } +} diff --git a/apps/kimi-code/test/utils/recommended-effort-config.test.ts b/apps/kimi-code/test/utils/recommended-effort-config.test.ts new file mode 100644 index 00000000000..04d901515ed --- /dev/null +++ b/apps/kimi-code/test/utils/recommended-effort-config.test.ts @@ -0,0 +1,219 @@ +import { mkdtemp, readFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { + getRecommendedEffortConfig, + peekRecommendedEffortConfig, + resetRecommendedEffortConfigCache, +} from '#/utils/recommended-effort-config'; + +const CLOUD_CONFIG = { + k3: { version: 1757001600, recommended_default_effort: 'max' }, + 'k3-256k': { version: 2, recommended_default_effort: 'high' }, +}; + +const ENVELOPE = { name: 'recommended_effort', config: CLOUD_CONFIG }; + +const tempDirs: string[] = []; + +function jsonResponse(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { + status, + headers: { 'Content-Type': 'application/json' }, + }); +} + +async function makeCacheFile(): Promise { + const dir = await mkdtemp(join(tmpdir(), 'recommended-effort-config-')); + tempDirs.push(dir); + return join(dir, 'cache.json'); +} + +afterEach(async () => { + resetRecommendedEffortConfigCache(); + await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +describe('getRecommendedEffortConfig', () => { + it('POSTs the recommended_effort name and returns the per-model entries', async () => { + const fetchImpl = vi.fn(async () => jsonResponse(ENVELOPE)); + + const result = await getRecommendedEffortConfig({ + fetchImpl: fetchImpl as typeof fetch, + cacheFile: null, + }); + + expect(result).toEqual(CLOUD_CONFIG); + expect(fetchImpl).toHaveBeenCalledWith( + expect.stringContaining('/client_configs'), + expect.objectContaining({ + method: 'POST', + body: JSON.stringify({ name: 'recommended_effort' }), + }), + ); + }); + + it('ignores unknown entry fields so the contract can evolve', async () => { + const fetchImpl = vi.fn(async () => + jsonResponse({ + name: 'recommended_effort', + config: { + k3: { + version: 3, + recommended_default_effort: 'max', + recommended_current_effort: 'high', + }, + }, + }), + ); + + const result = await getRecommendedEffortConfig({ + fetchImpl: fetchImpl as typeof fetch, + cacheFile: null, + }); + + expect(result).toEqual({ k3: { version: 3, recommended_default_effort: 'max' } }); + }); + + it('drops only the invalid entries and keeps the valid ones', async () => { + const fetchImpl = vi.fn(async () => + jsonResponse({ + name: 'recommended_effort', + config: { + valid: { version: 1, recommended_default_effort: 'max' }, + 'missing-effort': { version: 1 }, + 'missing-version': { recommended_default_effort: 'max' }, + 'negative-version': { version: -1, recommended_default_effort: 'max' }, + 'fractional-version': { version: 1.5, recommended_default_effort: 'max' }, + 'string-version': { version: '1', recommended_default_effort: 'max' }, + 'non-string-effort': { version: 1, recommended_default_effort: 5 }, + 'not-an-object': 'max', + }, + }), + ); + + const result = await getRecommendedEffortConfig({ + fetchImpl: fetchImpl as typeof fetch, + cacheFile: null, + }); + + expect(result).toEqual({ valid: { version: 1, recommended_default_effort: 'max' } }); + }); + + it('returns undefined when the envelope name does not match', async () => { + const fetchImpl = vi.fn(async () => + jsonResponse({ name: 'survey_popup', config: CLOUD_CONFIG }), + ); + + const result = await getRecommendedEffortConfig({ + fetchImpl: fetchImpl as typeof fetch, + cacheFile: null, + }); + + expect(result).toBeUndefined(); + }); + + it('returns undefined when the payload is not an object', async () => { + const fetchImpl = vi.fn(async () => + jsonResponse({ name: 'recommended_effort', config: 'nope' }), + ); + + const result = await getRecommendedEffortConfig({ + fetchImpl: fetchImpl as typeof fetch, + cacheFile: null, + }); + + expect(result).toBeUndefined(); + }); + + it('returns undefined when the fetch fails or the response is not ok', async () => { + const failing = vi.fn(async () => { + throw new Error('offline'); + }); + await expect( + getRecommendedEffortConfig({ fetchImpl: failing as typeof fetch, cacheFile: null }), + ).resolves.toBeUndefined(); + + const notOk = vi.fn(async () => jsonResponse('no', 503)); + await expect( + getRecommendedEffortConfig({ fetchImpl: notOk as typeof fetch, cacheFile: null }), + ).resolves.toBeUndefined(); + }); + + it('serves the in-process cache within a day and refetches after it', async () => { + const fetchImpl = vi.fn(async () => jsonResponse(ENVELOPE)); + const now = Date.now(); + + await getRecommendedEffortConfig({ fetchImpl: fetchImpl as typeof fetch, now, cacheFile: null }); + const cached = await getRecommendedEffortConfig({ + fetchImpl: fetchImpl as typeof fetch, + now: now + 60_000, + cacheFile: null, + }); + expect(cached).toEqual(CLOUD_CONFIG); + expect(fetchImpl).toHaveBeenCalledTimes(1); + + await getRecommendedEffortConfig({ + fetchImpl: fetchImpl as typeof fetch, + now: now + 25 * 60 * 60 * 1000, + cacheFile: null, + }); + expect(fetchImpl).toHaveBeenCalledTimes(2); + }); + + it('persists the fetched config to the disk cache for the next process', async () => { + const cacheFile = await makeCacheFile(); + const fetchImpl = vi.fn(async () => jsonResponse(ENVELOPE)); + const now = Date.now(); + + await getRecommendedEffortConfig({ fetchImpl: fetchImpl as typeof fetch, now, cacheFile }); + + const persisted = JSON.parse(await readFile(cacheFile, 'utf-8')) as { config: unknown }; + expect(persisted.config).toEqual(CLOUD_CONFIG); + + resetRecommendedEffortConfigCache(); + const result = await getRecommendedEffortConfig({ + fetchImpl: vi.fn(async () => { + throw new Error('must not fetch'); + }) as unknown as typeof fetch, + now: now + 60_000, + cacheFile, + }); + expect(result).toEqual(CLOUD_CONFIG); + }); + + it('ignores a stale disk cache and returns undefined when the refetch fails', async () => { + const cacheFile = await makeCacheFile(); + const fetchImpl = vi.fn(async () => jsonResponse(ENVELOPE)); + const now = Date.now(); + + await getRecommendedEffortConfig({ fetchImpl: fetchImpl as typeof fetch, now, cacheFile }); + resetRecommendedEffortConfigCache(); + + const result = await getRecommendedEffortConfig({ + fetchImpl: vi.fn(async () => jsonResponse('no', 503)) as unknown as typeof fetch, + now: now + 25 * 60 * 60 * 1000, + cacheFile, + }); + expect(result).toBeUndefined(); + }); +}); + +describe('peekRecommendedEffortConfig', () => { + it('returns undefined while the cache is cold', () => { + expect(peekRecommendedEffortConfig()).toBeUndefined(); + }); + + it('sees the fetched config once the cache is warm', async () => { + const fetchImpl = vi.fn(async () => jsonResponse(ENVELOPE)); + const now = Date.now(); + + await getRecommendedEffortConfig({ fetchImpl: fetchImpl as typeof fetch, now, cacheFile: null }); + + expect(peekRecommendedEffortConfig(now + 60_000)).toEqual(CLOUD_CONFIG); + expect(peekRecommendedEffortConfig(now + 25 * 60 * 60 * 1000)).toBeUndefined(); + }); +}); diff --git a/apps/kimi-code/test/utils/recommended-effort-state-store.test.ts b/apps/kimi-code/test/utils/recommended-effort-state-store.test.ts new file mode 100644 index 00000000000..73f3a46a346 --- /dev/null +++ b/apps/kimi-code/test/utils/recommended-effort-state-store.test.ts @@ -0,0 +1,66 @@ +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { + readRecommendedEffortState, + writeRecommendedEffortState, +} from '#/utils/recommended-effort-state-store'; + +describe('recommended-effort-state-store', () => { + let dir: string; + let file: string; + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), 'kimi-recommended-effort-state-')); + file = join(dir, 'recommended-effort-state.json'); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it('returns an empty record when the file is missing', async () => { + await expect(readRecommendedEffortState(file)).resolves.toEqual({}); + }); + + it('round-trips per-model applied markers', async () => { + const state = { + k3: { version: 1757001600, applied_at: '2026-09-05T02:00:00.000Z' }, + 'k3-256k': { version: 2, applied_at: '2026-09-06T02:00:00.000Z' }, + }; + writeRecommendedEffortState(state, file); + await expect(readRecommendedEffortState(file)).resolves.toEqual(state); + }); + + it('returns an empty record when the file is corrupt', async () => { + await writeFile(file, 'not json', 'utf-8'); + await expect(readRecommendedEffortState(file)).resolves.toEqual({}); + }); + + it('returns an empty record when the schema does not match', async () => { + await writeFile(file, JSON.stringify({ k3: { version: 'new' } }), 'utf-8'); + await expect(readRecommendedEffortState(file)).resolves.toEqual({}); + }); + + it('merges a new model marker into the existing state', async () => { + writeRecommendedEffortState( + { k3: { version: 1, applied_at: '2026-09-05T02:00:00.000Z' } }, + file, + ); + const state = await readRecommendedEffortState(file); + writeRecommendedEffortState( + { + ...state, + 'k3-256k': { version: 3, applied_at: '2026-09-06T02:00:00.000Z' }, + }, + file, + ); + await expect(readRecommendedEffortState(file)).resolves.toEqual({ + k3: { version: 1, applied_at: '2026-09-05T02:00:00.000Z' }, + 'k3-256k': { version: 3, applied_at: '2026-09-06T02:00:00.000Z' }, + }); + }); +}); diff --git a/apps/kimi-code/test/utils/recommended-effort.test.ts b/apps/kimi-code/test/utils/recommended-effort.test.ts new file mode 100644 index 00000000000..6823bb76b08 --- /dev/null +++ b/apps/kimi-code/test/utils/recommended-effort.test.ts @@ -0,0 +1,454 @@ +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import type { KimiConfig, KimiConfigPatch, ModelAlias } from '@moonshot-ai/kimi-code-sdk'; + +import { applyRecommendedEffort } from '#/utils/recommended-effort'; +import type { RecommendedEffortConfig } from '#/utils/recommended-effort-config'; + +const OFFICIAL_COM = 'https://api.kimi.com/coding/v1'; +const OFFICIAL_AI = 'https://api.kimi.ai/coding/v1'; +const GATEWAY = 'https://gateway.example.com/coding/v1'; + +const NOW = new Date('2026-09-05T02:00:00.000Z'); +const CAMPAIGN = { version: 5, recommended_default_effort: 'max' }; + +function makeModelEntry(overrides: Partial = {}): ModelAlias { + return { + provider: 'managed:kimi-code', + model: 'k3', + maxContextSize: 256000, + supportEfforts: ['low', 'medium', 'high', 'max'], + ...overrides, + }; +} + +function makeConfig(overrides: Partial = {}): KimiConfig { + return { + providers: { + 'managed:kimi-code': { type: 'kimi', baseUrl: OFFICIAL_COM }, + }, + defaultModel: 'main', + models: { main: makeModelEntry() }, + thinking: { effort: 'high' }, + ...overrides, + }; +} + +function makeHarness( + config: KimiConfig, + cloud: RecommendedEffortConfig | undefined, + stateFile: string, +) { + const setConfig = vi.fn(async (_patch: KimiConfigPatch) => ({}) as KimiConfig); + const track = vi.fn(); + const fetchConfig = vi.fn(async () => cloud); + const getConfig = vi.fn(async () => config); + const run = () => + applyRecommendedEffort({ + fetchConfig, + getConfig, + setConfig, + track, + stateFile, + now: () => NOW, + }); + return { setConfig, track, fetchConfig, getConfig, run }; +} + +async function readStateRaw(file: string): Promise { + return JSON.parse(await readFile(file, 'utf-8')) as unknown; +} + +async function expectNoStateFile(file: string): Promise { + await expect(readFile(file, 'utf-8')).rejects.toThrow(); +} + +describe('applyRecommendedEffort', () => { + let dir: string; + let stateFile: string; + const savedBaseUrl = process.env['KIMI_CODE_BASE_URL']; + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), 'kimi-recommended-effort-')); + stateFile = join(dir, 'recommended-effort-state.json'); + delete process.env['KIMI_CODE_BASE_URL']; + }); + + afterEach(async () => { + if (savedBaseUrl === undefined) { + delete process.env['KIMI_CODE_BASE_URL']; + } else { + process.env['KIMI_CODE_BASE_URL'] = savedBaseUrl; + } + await rm(dir, { recursive: true, force: true }); + }); + + it('writes the recommended effort, records the marker, and reports the event', async () => { + const h = makeHarness(makeConfig(), { k3: CAMPAIGN }, stateFile); + + await h.run(); + + expect(h.setConfig).toHaveBeenCalledTimes(1); + expect(h.setConfig).toHaveBeenCalledWith({ thinking: { effort: 'max' } }); + expect(await readStateRaw(stateFile)).toEqual({ + k3: { version: 5, applied_at: NOW.toISOString() }, + }); + expect(h.track).toHaveBeenCalledTimes(1); + expect(h.track).toHaveBeenCalledWith('recommended_effort_applied', { + model: 'k3', + version: 5, + effort: 'max', + previous_effort: 'high', + }); + }); + + it('reports undefined previous_effort when thinking was never configured', async () => { + const h = makeHarness(makeConfig({ thinking: undefined }), { k3: CAMPAIGN }, stateFile); + + await h.run(); + + expect(h.setConfig).toHaveBeenCalledWith({ thinking: { effort: 'max' } }); + expect(h.track).toHaveBeenCalledWith('recommended_effort_applied', { + model: 'k3', + version: 5, + effort: 'max', + previous_effort: undefined, + }); + }); + + it('does not add an enabled key when thinking.enabled is absent or explicit true', async () => { + for (const thinking of [{ enabled: true }, { effort: 'low' }]) { + const h = makeHarness(makeConfig({ thinking }), { k3: CAMPAIGN }, stateFile); + + await h.run(); + + expect(h.setConfig).toHaveBeenCalledWith({ thinking: { effort: 'max' } }); + await rm(stateFile, { force: true }); + } + }); + + it('records only the marker when the current effort already equals the recommendation', async () => { + const h = makeHarness(makeConfig({ thinking: { effort: 'max' } }), { k3: CAMPAIGN }, stateFile); + + await h.run(); + + expect(h.setConfig).not.toHaveBeenCalled(); + expect(await readStateRaw(stateFile)).toEqual({ + k3: { version: 5, applied_at: NOW.toISOString() }, + }); + expect(h.track).toHaveBeenCalledWith('recommended_effort_applied', { + model: 'k3', + version: 5, + effort: 'max', + previous_effort: 'max', + }); + }); + + it('does nothing when thinking is explicitly disabled', async () => { + const h = makeHarness( + makeConfig({ thinking: { enabled: false, effort: 'high' } }), + { k3: CAMPAIGN }, + stateFile, + ); + + await h.run(); + + expect(h.fetchConfig).not.toHaveBeenCalled(); + expect(h.setConfig).not.toHaveBeenCalled(); + expect(h.track).not.toHaveBeenCalled(); + await expectNoStateFile(stateFile); + }); + + it('does nothing without a default model or its models entry', async () => { + for (const config of [ + makeConfig({ defaultModel: undefined }), + makeConfig({ models: undefined }), + makeConfig({ models: {} }), + ]) { + const h = makeHarness(config, { k3: CAMPAIGN }, stateFile); + + await h.run(); + + expect(h.setConfig).not.toHaveBeenCalled(); + expect(h.track).not.toHaveBeenCalled(); + } + await expectNoStateFile(stateFile); + }); + + it('judges by config.defaultModel only, even when another alias matches the cloud list', async () => { + const h = makeHarness( + makeConfig({ + models: { + main: makeModelEntry({ model: 'k3' }), + alt: makeModelEntry({ model: 'k3-256k' }), + }, + }), + { 'k3-256k': CAMPAIGN }, + stateFile, + ); + + await h.run(); + + expect(h.setConfig).not.toHaveBeenCalled(); + expect(h.track).not.toHaveBeenCalled(); + await expectNoStateFile(stateFile); + }); + + it('does nothing when the model is not in the cloud config or the fetch fails', async () => { + for (const cloud of [{}, undefined] as const) { + const h = makeHarness(makeConfig(), cloud, stateFile); + + await h.run(); + + expect(h.setConfig).not.toHaveBeenCalled(); + expect(h.track).not.toHaveBeenCalled(); + } + await expectNoStateFile(stateFile); + }); + + it('matches the global official endpoint as well', async () => { + const h = makeHarness( + makeConfig({ + providers: { 'managed:kimi-code': { type: 'kimi', baseUrl: OFFICIAL_AI } }, + }), + { k3: CAMPAIGN }, + stateFile, + ); + + await h.run(); + + expect(h.setConfig).toHaveBeenCalledWith({ thinking: { effort: 'max' } }); + }); + + it('prefers the entry-level base_url over the provider one, both ways', async () => { + const entryOfficial = makeHarness( + makeConfig({ + providers: { 'managed:kimi-code': { type: 'kimi', baseUrl: GATEWAY } }, + models: { main: makeModelEntry({ baseUrl: OFFICIAL_COM }) }, + }), + { k3: CAMPAIGN }, + stateFile, + ); + await entryOfficial.run(); + expect(entryOfficial.setConfig).toHaveBeenCalledTimes(1); + + await rm(stateFile, { force: true }); + + const entryGateway = makeHarness( + makeConfig({ models: { main: makeModelEntry({ baseUrl: GATEWAY }) } }), + { k3: CAMPAIGN }, + stateFile, + ); + await entryGateway.run(); + expect(entryGateway.setConfig).not.toHaveBeenCalled(); + expect(entryGateway.track).not.toHaveBeenCalled(); + await expectNoStateFile(stateFile); + }); + + it('does nothing for self-hosted endpoints', async () => { + for (const config of [ + makeConfig({ providers: { 'managed:kimi-code': { type: 'kimi', baseUrl: GATEWAY } } }), + makeConfig({ providers: { 'managed:kimi-code': { type: 'kimi' } } }), + ]) { + const h = makeHarness(config, { k3: CAMPAIGN }, stateFile); + + await h.run(); + + expect(h.setConfig).not.toHaveBeenCalled(); + expect(h.track).not.toHaveBeenCalled(); + } + await expectNoStateFile(stateFile); + }); + + it('treats KIMI_CODE_BASE_URL as the sole official benchmark when set', async () => { + process.env['KIMI_CODE_BASE_URL'] = GATEWAY; + const h = makeHarness(makeConfig(), { k3: CAMPAIGN }, stateFile); + + await h.run(); + + expect(h.setConfig).not.toHaveBeenCalled(); + expect(h.track).not.toHaveBeenCalled(); + await expectNoStateFile(stateFile); + }); + + it('does nothing when the model does not support the recommended effort', async () => { + for (const entry of [ + makeModelEntry({ supportEfforts: ['low', 'medium', 'high'] }), + makeModelEntry({ supportEfforts: undefined }), + makeModelEntry({ overrides: { supportEfforts: ['low', 'medium', 'high'] } }), + ]) { + const h = makeHarness(makeConfig({ models: { main: entry } }), { k3: CAMPAIGN }, stateFile); + + await h.run(); + + expect(h.setConfig).not.toHaveBeenCalled(); + expect(h.track).not.toHaveBeenCalled(); + } + await expectNoStateFile(stateFile); + }); + + it('applies when entry overrides widen supportEfforts to include the recommendation', async () => { + const h = makeHarness( + makeConfig({ + models: { + main: makeModelEntry({ + supportEfforts: ['low', 'medium', 'high'], + overrides: { supportEfforts: ['low', 'medium', 'high', 'max'] }, + }), + }, + }), + { k3: CAMPAIGN }, + stateFile, + ); + + await h.run(); + + expect(h.setConfig).toHaveBeenCalledWith({ thinking: { effort: 'max' } }); + }); + + it('applies only a strictly newer campaign version', async () => { + const applied = { k3: { version: 5, applied_at: '2026-09-01T00:00:00.000Z' } }; + await writeFile(stateFile, JSON.stringify(applied), 'utf-8'); + + for (const version of [5, 4]) { + const h = makeHarness(makeConfig(), { k3: { ...CAMPAIGN, version } }, stateFile); + await h.run(); + expect(h.setConfig).not.toHaveBeenCalled(); + expect(h.track).not.toHaveBeenCalled(); + } + expect(await readStateRaw(stateFile)).toEqual(applied); + + const newer = makeHarness(makeConfig(), { k3: { ...CAMPAIGN, version: 6 } }, stateFile); + await newer.run(); + expect(newer.setConfig).toHaveBeenCalledWith({ thinking: { effort: 'max' } }); + expect(await readStateRaw(stateFile)).toEqual({ + k3: { version: 6, applied_at: NOW.toISOString() }, + }); + }); + + it('applies version 0 when nothing was ever applied', async () => { + const h = makeHarness( + makeConfig(), + { k3: { version: 0, recommended_default_effort: 'max' } }, + stateFile, + ); + + await h.run(); + + expect(h.setConfig).toHaveBeenCalledWith({ thinking: { effort: 'max' } }); + expect(await readStateRaw(stateFile)).toEqual({ + k3: { version: 0, applied_at: NOW.toISOString() }, + }); + }); + + it('treats a corrupt marker file as never applied', async () => { + await writeFile(stateFile, 'not json', 'utf-8'); + const h = makeHarness(makeConfig(), { k3: CAMPAIGN }, stateFile); + + await h.run(); + + expect(h.setConfig).toHaveBeenCalledWith({ thinking: { effort: 'max' } }); + expect(await readStateRaw(stateFile)).toEqual({ + k3: { version: 5, applied_at: NOW.toISOString() }, + }); + }); + + it('keeps other models markers when recording a new one', async () => { + await writeFile( + stateFile, + JSON.stringify({ 'k3-256k': { version: 2, applied_at: '2026-09-01T00:00:00.000Z' } }), + 'utf-8', + ); + const h = makeHarness(makeConfig(), { k3: CAMPAIGN }, stateFile); + + await h.run(); + + expect(await readStateRaw(stateFile)).toEqual({ + 'k3-256k': { version: 2, applied_at: '2026-09-01T00:00:00.000Z' }, + k3: { version: 5, applied_at: NOW.toISOString() }, + }); + }); + + it('re-reads the live config after the fetch and honors a mid-flight opt-out', async () => { + const h = makeHarness(makeConfig(), { k3: CAMPAIGN }, stateFile); + h.getConfig + .mockResolvedValueOnce(makeConfig()) + .mockResolvedValueOnce(makeConfig({ thinking: { enabled: false } })); + + await h.run(); + + expect(h.setConfig).not.toHaveBeenCalled(); + expect(h.track).not.toHaveBeenCalled(); + await expectNoStateFile(stateFile); + }); + + it('reports previous_effort from the fresh config, not the pre-fetch one', async () => { + const h = makeHarness(makeConfig(), { k3: CAMPAIGN }, stateFile); + h.getConfig + .mockResolvedValueOnce(makeConfig()) + .mockResolvedValueOnce(makeConfig({ thinking: { effort: 'medium' } })); + + await h.run(); + + expect(h.track).toHaveBeenCalledWith('recommended_effort_applied', { + model: 'k3', + version: 5, + effort: 'max', + previous_effort: 'medium', + }); + }); + + it('updates the marker without writing when a stale marker meets an already-equal effort', async () => { + await writeFile( + stateFile, + JSON.stringify({ k3: { version: 3, applied_at: '2026-09-01T00:00:00.000Z' } }), + 'utf-8', + ); + const h = makeHarness(makeConfig({ thinking: { effort: 'max' } }), { k3: CAMPAIGN }, stateFile); + + await h.run(); + + expect(h.setConfig).not.toHaveBeenCalled(); + expect(await readStateRaw(stateFile)).toEqual({ + k3: { version: 5, applied_at: NOW.toISOString() }, + }); + expect(h.track).toHaveBeenCalledWith('recommended_effort_applied', { + model: 'k3', + version: 5, + effort: 'max', + previous_effort: 'max', + }); + }); + + it('records no marker and reports nothing when the config write fails', async () => { + const h = makeHarness(makeConfig(), { k3: CAMPAIGN }, stateFile); + h.setConfig.mockRejectedValue(new Error('disk full')); + + await h.run(); + + expect(h.track).not.toHaveBeenCalled(); + await expectNoStateFile(stateFile); + }); + + it('reports nothing when the marker write fails after a successful config write', async () => { + const dirAsStateFile = join(dir, 'state-file-is-a-directory'); + await mkdir(dirAsStateFile); + const h = makeHarness(makeConfig(), { k3: CAMPAIGN }, dirAsStateFile); + + await expect(h.run()).resolves.toBeUndefined(); + + expect(h.setConfig).toHaveBeenCalledTimes(1); + expect(h.track).not.toHaveBeenCalled(); + }); + + it('never throws, whatever the dependencies do', async () => { + const h = makeHarness(makeConfig(), { k3: CAMPAIGN }, stateFile); + h.setConfig.mockRejectedValue(new Error('boom')); + + await expect(h.run()).resolves.toBeUndefined(); + }); +}); diff --git a/packages/agent-core-v2/src/app/config/configService.ts b/packages/agent-core-v2/src/app/config/configService.ts index a80dd508fc2..fdc2dc6f5aa 100644 --- a/packages/agent-core-v2/src/app/config/configService.ts +++ b/packages/agent-core-v2/src/app/config/configService.ts @@ -43,7 +43,6 @@ import { } from './configSectionContributions'; import { getConfigOverlayContributions } from './configOverlayContributions'; import { collectKeyDeprecations } from './deprecations'; -import { migrateThinkingEffortMaxToHigh } from './migrations'; import { applySectionToToml, camelToSnake, @@ -332,10 +331,7 @@ export class ConfigService extends Disposable implements IConfigService { const { configKey } = this; const { homeDir } = this.bootstrap; this.seedInitialLoad(); - this.ready = (async () => { - await migrateThinkingEffortMaxToHigh(this.documentStore, configKey, homeDir); - await this.load('load'); - })(); + this.ready = this.load('load'); const configFile = join(homeDir, configKey); const handle = watch(homeDir, { depth: 0 }); this._register(handle); diff --git a/packages/agent-core-v2/src/app/config/migrations.ts b/packages/agent-core-v2/src/app/config/migrations.ts deleted file mode 100644 index dde5c66555a..00000000000 --- a/packages/agent-core-v2/src/app/config/migrations.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; -import { join } from 'pathe'; - -import { type IAtomicTomlDocumentStore } from '#/persistence/interface/atomicDocumentStore'; - -import { isPlainObject } from './configPure'; -import { replaceThinkingEffortMax } from './tomlWriteback'; - -const MIGRATIONS_FILE = 'migrations-effort.json'; -const THINKING_EFFORT_MAX_TO_HIGH = 'thinking-effort-max-to-high'; -const CONFIG_SCOPE = ''; - -function readMigrationMarkers(homeDir: string): Record { - try { - const parsed: unknown = JSON.parse(readFileSync(join(homeDir, MIGRATIONS_FILE), 'utf-8')); - if (isPlainObject(parsed)) return parsed as Record; - } catch { - } - return {}; -} - -function writeMigrationMarker(homeDir: string, key: string): void { - try { - mkdirSync(homeDir, { recursive: true, mode: 0o700 }); - const markers = readMigrationMarkers(homeDir); - markers[key] = new Date().toISOString(); - writeFileSync(join(homeDir, MIGRATIONS_FILE), `${JSON.stringify(markers, null, 2)}\n`, { - mode: 0o600, - }); - } catch { - } -} - -export async function migrateThinkingEffortMaxToHigh( - documentStore: IAtomicTomlDocumentStore, - configKey: string, - homeDir: string, -): Promise { - try { - if (readMigrationMarkers(homeDir)[THINKING_EFFORT_MAX_TO_HIGH] !== undefined) return; - let doc: Record | undefined; - let text: string | undefined; - try { - text = await documentStore.getText(CONFIG_SCOPE, configKey); - const data = await documentStore.get>(CONFIG_SCOPE, configKey); - doc = data !== undefined && isPlainObject(data) ? data : {}; - } catch { - return; - } - const thinking = doc['thinking']; - if (isPlainObject(thinking) && thinking['effort'] === 'max') { - const migrated = text === undefined ? undefined : replaceThinkingEffortMax(text); - if (migrated === undefined) { - doc['thinking'] = { ...thinking, effort: 'high' }; - await documentStore.set(CONFIG_SCOPE, configKey, doc); - } else if (migrated !== text) { - await documentStore.setText(CONFIG_SCOPE, configKey, migrated); - } - } - writeMigrationMarker(homeDir, THINKING_EFFORT_MAX_TO_HIGH); - } catch { - } -} diff --git a/packages/agent-core-v2/src/app/config/tomlWriteback.ts b/packages/agent-core-v2/src/app/config/tomlWriteback.ts index f5258ccffce..8b184084038 100644 --- a/packages/agent-core-v2/src/app/config/tomlWriteback.ts +++ b/packages/agent-core-v2/src/app/config/tomlWriteback.ts @@ -751,31 +751,3 @@ export function planConfigWriteback( if (!verifyPlannedText(text, expected)) return undefined; return text; } - -export function replaceThinkingEffortMax(originalText: string): string | undefined { - const scanned = scanRootRegions(originalText); - if (scanned === undefined) return undefined; - const regions = scanned.segments.flatMap((segment) => - segment.kind === 'region' && segment.region.rootKey === 'thinking' ? [segment.region] : [], - ); - const region = regions.length === 1 ? regions[0]! : undefined; - if (region === undefined || region.dotted) return undefined; - const scan = scanDomainRegion(originalText, scanned.lines, scanned.offsets, region, 'thinking'); - if (scan === undefined || scan.ambiguous) return undefined; - const block = scan.blocks.find((candidate) => candidate.path.length === 0 && !candidate.isArray); - const statement = block?.statements.find((candidate) => candidate.key === 'effort'); - if (block === undefined || statement === undefined) return undefined; - if (originalText.slice(statement.valueStart, statement.valueEnd) !== '"max"') return undefined; - return applyLineEdits( - scanned.lines, - [ - { - type: 'replace', - startLine: statement.startLine, - endLine: statement.endLine, - text: renderStatement(originalText, statement, '"high"', scanned.eol), - }, - ], - scanned.eol, - ); -} diff --git a/packages/agent-core-v2/test/app/config/config.test.ts b/packages/agent-core-v2/test/app/config/config.test.ts index 3fd9fb391bf..294e3bc235c 100644 --- a/packages/agent-core-v2/test/app/config/config.test.ts +++ b/packages/agent-core-v2/test/app/config/config.test.ts @@ -1,8 +1,5 @@ import type { ModelCapability } from '#/llm-adapter/contract/capability'; import type { ToolCall } from '#human/llm/message'; -import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'pathe'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { IAgentProfileService, type ResolvedAgentProfile } from '#/agent/profile/profile'; @@ -2650,76 +2647,6 @@ function toolNames(value: unknown): string[] { .filter((name): name is string => name !== null); } -describe('ConfigService thinking effort max migration', () => { - let homeDir: string; - - beforeEach(() => { - homeDir = mkdtempSync(join(tmpdir(), 'kimi-v2-cfg-migrate-')); - }); - - afterEach(() => { - rmSync(homeDir, { recursive: true, force: true }); - }); - - async function createMigratingConfig(toml: string) { - const disposables = new DisposableStore(); - const ix = disposables.add(new TestInstantiationService()); - const storage = new InMemoryStorageService(); - await storage.write('', 'config.toml', new TextEncoder().encode(toml)); - ix.stub(ILogService, stubLog()); - ix.stub(IBootstrapService, stubBootstrap(homeDir)); - ix.stub(IFileSystemStorageService, storage); - ix.set(IAtomicTomlDocumentStore, new SyncDescriptor(TomlAtomicDocumentStore)); - ix.set(IConfigRegistry, new SyncDescriptor(ConfigRegistry)); - ix.set(IConfigService, new SyncDescriptor(ConfigService)); - const config = ix.get(IConfigService); - await config.ready; - return { config, disposables }; - } - - function readMarkers(): Record { - return JSON.parse(readFileSync(join(homeDir, 'migrations-effort.json'), 'utf-8')) as Record< - string, - string - >; - } - - it('rewrites a persisted max to high on first load and records the marker', async () => { - const { config, disposables } = await createMigratingConfig( - '[thinking]\nenabled = true\neffort = "max"\n', - ); - - expect(config.get(THINKING_SECTION)).toEqual({ - enabled: true, - effort: 'high', - }); - expect(readMarkers()['thinking-effort-max-to-high']).toBeDefined(); - - disposables.dispose(); - }); - - it('honors a hand-set max once the marker exists', async () => { - writeFileSync( - join(homeDir, 'migrations-effort.json'), - JSON.stringify({ 'thinking-effort-max-to-high': new Date().toISOString() }), - ); - const { config, disposables } = await createMigratingConfig('[thinking]\neffort = "max"\n'); - - expect(config.get(THINKING_SECTION)).toEqual({ effort: 'max' }); - - disposables.dispose(); - }); - - it('records the marker even when nothing needs migrating', async () => { - const { config, disposables } = await createMigratingConfig('[thinking]\neffort = "low"\n'); - - expect(config.get(THINKING_SECTION)).toEqual({ effort: 'low' }); - expect(readMarkers()['thinking-effort-max-to-high']).toBeDefined(); - - disposables.dispose(); - }); -}); - describe('ConfigService replaceSections', () => { const SEED_TOML = [ 'default_model = "acme/m1"', diff --git a/packages/agent-core-v2/test/app/config/tomlWriteback.test.ts b/packages/agent-core-v2/test/app/config/tomlWriteback.test.ts index 2143583b573..bf96599ba26 100644 --- a/packages/agent-core-v2/test/app/config/tomlWriteback.test.ts +++ b/packages/agent-core-v2/test/app/config/tomlWriteback.test.ts @@ -12,7 +12,7 @@ import { IMAGE_SECTION, type ImageConfig } from '#/agent/media/configSection'; import { IBootstrapService } from '#/app/bootstrap/bootstrap'; import { IConfigRegistry, IConfigService } from '#/app/config/config'; import { ConfigRegistry, ConfigService } from '#/app/config/configService'; -import { planConfigWriteback, replaceThinkingEffortMax, type DomainUpdate } from '#/app/config/tomlWriteback'; +import { planConfigWriteback, type DomainUpdate } from '#/app/config/tomlWriteback'; import { InMemoryStorageService } from '#/persistence/backends/memory/inMemoryStorageService'; import { TomlAtomicDocumentStore } from '#/persistence/backends/node-fs/atomicDocumentStore'; import { IAtomicTomlDocumentStore } from '#/persistence/interface/atomicDocumentStore'; @@ -312,33 +312,6 @@ describe('planConfigWriteback', () => { }); }); -describe('replaceThinkingEffortMax', () => { - it('replaces effort = "max" with "high" inside the thinking region', () => { - const text = '# thinking config\n[thinking]\n# do not touch\neffort = "max"\n'; - expect(replaceThinkingEffortMax(text)).toBe('# thinking config\n[thinking]\n# do not touch\neffort = "high"\n'); - }); - - it('keeps the trailing comment on the effort line', () => { - const text = '[thinking]\neffort = "max" # legacy\n'; - expect(replaceThinkingEffortMax(text)).toBe('[thinking]\neffort = "high" # legacy\n'); - }); - - it('handles CRLF files', () => { - const text = '[thinking]\r\neffort = "max"\r\n'; - expect(replaceThinkingEffortMax(text)).toBe('[thinking]\r\neffort = "high"\r\n'); - }); - - it('returns undefined when there is no single thinking region', () => { - expect(replaceThinkingEffortMax('[other]\nx = 1\n')).toBeUndefined(); - expect(replaceThinkingEffortMax('[thinking]\neffort = "high"\n[thinking]\neffort = "max"\n')).toBeUndefined(); - }); - - it('returns undefined when effort is not a plain "max" literal', () => { - expect(replaceThinkingEffortMax('[thinking]\neffort = "medium"\n')).toBeUndefined(); - expect(replaceThinkingEffortMax('[thinking]\neffort = """\nmax\n"""\n')).toBeUndefined(); - }); -}); - describe('ConfigService key-level writeback', () => { let homeDir: string; diff --git a/packages/agent-core-v2/test/app/config/writeback.test.ts b/packages/agent-core-v2/test/app/config/writeback.test.ts index 9831f74c4f3..df2d8a26233 100644 --- a/packages/agent-core-v2/test/app/config/writeback.test.ts +++ b/packages/agent-core-v2/test/app/config/writeback.test.ts @@ -178,16 +178,4 @@ describe('config.toml writeback preservation', () => { disposables.dispose(); }); - it('migrates thinking effort max to high without dropping the surrounding comments', async () => { - const seed = '# 思考配置\n[thinking]\n# 不要动我\neffort = "max"\n'; - const { config, disposables, readText } = await setup(seed); - - const text = await readText(); - expect(text.includes('# 思考配置')).toBe(true); - expect(text.includes('effort = "high"')).toBe(true); - expect(text.includes('effort = "max"')).toBe(false); - expect(config.get(THINKING_SECTION)).toEqual({ effort: 'high' }); - - disposables.dispose(); - }); }); diff --git a/packages/oauth/src/managed-usage.ts b/packages/oauth/src/managed-usage.ts index f004795002a..601ef3a365e 100644 --- a/packages/oauth/src/managed-usage.ts +++ b/packages/oauth/src/managed-usage.ts @@ -27,6 +27,7 @@ import { isRecord } from './utils'; const MANAGED_PREFIX = 'managed:'; const KIMI_CODE_PLATFORM_ID = 'kimi-code'; export const DEFAULT_KIMI_CODE_BASE_URL = 'https://api.kimi.com/coding/v1'; +export const GLOBAL_KIMI_CODE_BASE_URL = 'https://api.kimi.ai/coding/v1'; export function isManagedKimiCode(providerKey?: string | null): boolean { if (!providerKey) return false; @@ -47,17 +48,25 @@ export function kimiCodeUsageUrl(): string { } /** - * Strict match against the managed Kimi Code endpoint: both URLs are parsed + * Strict match against the managed Kimi Code endpoints: both URLs are parsed * and compared by lowercase origin + pathname without trailing slashes. * Anything that fails to parse — or differs in host or path, e.g. a proxy, * gateway, or self-hosted mirror — is NOT the managed endpoint and must not * be auto-refreshed, because its `/models` schema cannot be trusted. + * + * The managed set is the `KIMI_CODE_BASE_URL` override when set (the env + * keeps full control); otherwise both official deployments (.com and .ai). */ export function isManagedKimiCodeBaseUrl(baseUrl: string | undefined): boolean { if (baseUrl === undefined) return false; - const managed = parseNormalizedUrl(kimiCodeBaseUrl()); const candidate = parseNormalizedUrl(baseUrl); - return managed !== undefined && candidate !== undefined && managed === candidate; + if (candidate === undefined) return false; + const envOverride = process.env['KIMI_CODE_BASE_URL']; + const managed = + envOverride !== undefined + ? [envOverride] + : [DEFAULT_KIMI_CODE_BASE_URL, GLOBAL_KIMI_CODE_BASE_URL]; + return managed.some((url) => parseNormalizedUrl(url) === candidate); } function parseNormalizedUrl(value: string): string | undefined { diff --git a/packages/oauth/test/managed-usage.test.ts b/packages/oauth/test/managed-usage.test.ts index 6e3cd70291e..4b2ddbb5e5c 100644 --- a/packages/oauth/test/managed-usage.test.ts +++ b/packages/oauth/test/managed-usage.test.ts @@ -28,15 +28,18 @@ describe('kimiCodeBaseUrl', () => { }); describe('isManagedKimiCodeBaseUrl', () => { - it('matches the default managed endpoint, with or without a trailing slash', () => { + it('matches both official managed endpoints, with or without a trailing slash', () => { expect(isManagedKimiCodeBaseUrl('https://api.kimi.com/coding/v1')).toBe(true); expect(isManagedKimiCodeBaseUrl('https://api.kimi.com/coding/v1/')).toBe(true); + expect(isManagedKimiCodeBaseUrl('https://api.kimi.ai/coding/v1')).toBe(true); + expect(isManagedKimiCodeBaseUrl('https://api.kimi.ai/coding/v1/')).toBe(true); }); - it('matches against the KIMI_CODE_BASE_URL override', () => { + it('matches against the KIMI_CODE_BASE_URL override as the sole benchmark', () => { vi.stubEnv('KIMI_CODE_BASE_URL', 'https://gw.example.com/coding/v1/'); expect(isManagedKimiCodeBaseUrl('https://gw.example.com/coding/v1')).toBe(true); expect(isManagedKimiCodeBaseUrl('https://api.kimi.com/coding/v1')).toBe(false); + expect(isManagedKimiCodeBaseUrl('https://api.kimi.ai/coding/v1')).toBe(false); }); it('is case-insensitive on the origin but strict on the path', () => {