fix: restore env-injected fields to on-disk values in stripEnvModelConfig; update tests for thinking behavior

This commit is contained in:
Kaiyi 2026-05-29 21:37:20 +08:00
parent 18a86297a2
commit 3719ec031d
2 changed files with 70 additions and 19 deletions

View file

@ -171,9 +171,10 @@ export function applyEnvModelConfig(config: KimiConfig, env: Env = process.env):
* into the in-memory runtime config; this guarantees they never reach
* config.toml including via a `getConfig` -> `setConfig` patch round-trip,
* where the runtime config (carrying the env provider and its shell API key)
* would otherwise be merged back and written out. A `default_model` pointing at
* the env alias is restored to the on-disk value (from `config.raw`) rather than
* erased, so a real default_model already in config.toml survives the round-trip.
* would otherwise be merged back and written out. Every env-injected top-level
* field (default_model, thinking, default_thinking) is restored to its on-disk
* value from `config.raw` rather than erased, so real values already in
* config.toml survive the round-trip.
*/
export function stripEnvModelConfig(config: KimiConfig): KimiConfig {
const hasProvider = ENV_MODEL_PROVIDER_KEY in config.providers;
@ -194,9 +195,14 @@ export function stripEnvModelConfig(config: KimiConfig): KimiConfig {
...config,
providers,
...(models !== undefined ? { models } : {}),
// Restore the on-disk default (from raw) instead of erasing it when the
// runtime default points at the env alias.
// Restore env-injected top-level fields from raw instead of persisting the
// shell overrides: the env default_model (when it points at the env alias),
// and the env thinking / default_thinking. Reaching here means env-model
// mode is active (the synthetic provider/model exist), so these may be env
// values; an unset raw field restores to undefined (i.e. drops it).
...(defaultIsEnv ? { defaultModel: rawDefaultModel(config) } : {}),
thinking: rawThinking(config),
defaultThinking: rawDefaultThinking(config),
};
}
@ -204,3 +210,15 @@ function rawDefaultModel(config: KimiConfig): string | undefined {
const raw = config.raw?.['default_model'];
return typeof raw === 'string' ? raw : undefined;
}
function rawDefaultThinking(config: KimiConfig): boolean | undefined {
const raw = config.raw?.['default_thinking'];
return typeof raw === 'boolean' ? raw : undefined;
}
function rawThinking(config: KimiConfig): ThinkingConfig | undefined {
const raw = config.raw?.['thinking'];
return typeof raw === 'object' && raw !== null && !Array.isArray(raw)
? (raw as ThinkingConfig)
: undefined;
}

View file

@ -1,4 +1,4 @@
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { describe, expect, it } from 'vitest';
import { join } from 'pathe';
@ -213,27 +213,60 @@ describe('stripEnvModelConfig (write-back guard)', () => {
});
describe('writeConfigFile never persists the env model', () => {
it('strips env entries even when a runtime config is written back', async () => {
it('strips env entries (incl. thinking) when a runtime config is written back', async () => {
const dir = mkdtempSync(join(tmpdir(), 'kimi-env-write-'));
const path = join(dir, 'config.toml');
writeFileSync(
path,
'default_model = "x"\ndefault_thinking = false\n[thinking]\nmode = "auto"\n[providers.x]\ntype = "kimi"\napi_key = "k"\n[models.x]\nprovider = "x"\nmodel = "x"\nmax_context_size = 1000\n',
);
try {
// Reproduces the /login round-trip: a runtime config carrying the env
// model AND env thinking overrides is written back and must persist none.
const runtime = loadRuntimeConfig(path, {
...MIN,
KIMI_MODEL_THINKING_MODE: 'on',
KIMI_MODEL_DEFAULT_THINKING: 'true',
});
// Sanity: env overrides are active at runtime.
expect(runtime.providers[ENV_MODEL_PROVIDER_KEY]).toBeDefined();
expect(runtime.thinking?.mode).toBe('on');
expect(runtime.defaultThinking).toBe(true);
await writeConfigFile(path, runtime);
const onDisk = readConfigFile(path);
// Env provider/model are gone; the user's stay; default_model is restored.
expect(onDisk.providers[ENV_MODEL_PROVIDER_KEY]).toBeUndefined();
expect(onDisk.models?.[ENV_MODEL_ALIAS_KEY]).toBeUndefined();
expect(onDisk.providers['x']).toBeDefined();
expect(onDisk.models?.['x']).toBeDefined();
expect(onDisk.defaultModel).toBe('x');
// Thinking is restored to the on-disk original, not the env override.
expect(onDisk.thinking?.mode).toBe('auto');
expect(onDisk.defaultThinking).toBe(false);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it('output never contains env-injected identifiers', async () => {
const dir = mkdtempSync(join(tmpdir(), 'kimi-env-write2-'));
const path = join(dir, 'config.toml');
writeFileSync(
path,
'default_model = "x"\n[providers.x]\ntype = "kimi"\napi_key = "k"\n[models.x]\nprovider = "x"\nmodel = "x"\nmax_context_size = 1000\n',
);
try {
// Reproduces the /login round-trip: a runtime config carrying the env
// model is written back through writeConfigFile and must not persist it.
const runtime = loadRuntimeConfig(path, { ...MIN });
expect(runtime.providers[ENV_MODEL_PROVIDER_KEY]).toBeDefined();
const runtime = loadRuntimeConfig(path, {
...MIN,
KIMI_MODEL_THINKING_EFFORT: 'low',
KIMI_MODEL_DEFAULT_THINKING: 'true',
});
await writeConfigFile(path, runtime);
const onDisk = readConfigFile(path);
expect(onDisk.providers[ENV_MODEL_PROVIDER_KEY]).toBeUndefined();
expect(onDisk.models?.[ENV_MODEL_ALIAS_KEY]).toBeUndefined();
expect(onDisk.providers['x']).toBeDefined();
expect(onDisk.models?.['x']).toBeDefined();
// The env default_model is stripped, but the on-disk original is restored
// (not erased), so a real default_model in config.toml survives.
expect(onDisk.defaultModel).toBe('x');
const text = readFileSync(path, 'utf-8');
// Hard invariant: no env-synthesized identifiers ever reach config.toml.
expect(text).not.toContain(ENV_MODEL_PROVIDER_KEY);
expect(text).not.toContain(ENV_MODEL_ALIAS_KEY);
} finally {
rmSync(dir, { recursive: true, force: true });
}