kimi-code / packages /node-sdk /src /v2 /config-mapper.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
4e23b01 verified
Raw History Blame Contribute Delete
5.88 kB
/**
* v2 config shape mapping — pure functions that project the agent-core-v2
* engine's per-domain config view (`IConfigService.getAll()` /
* `inspect().userValue` / `diagnostics()`) onto the v1 `KimiConfig` /
* `ConfigDiagnostics` shapes the SDK contract returns.
*
* Why a mapping layer exists: v1 loads config.toml as ONE zod-validated
* document (`KimiConfigSchema`), while v2 registers one config section per
* owning domain and resolves each independently. The v1 top-level field
* names line up 1:1 with the v2 camelCase domain names (both derive from
* the same snake_case TOML keys), so the read mapping is a field pick, not
* a reshape. Gaps that cannot be mapped faithfully (the v1-only `raw`
* passthrough document, v2's materialized section defaults) are pinned in
* the parity test's `KNOWN_DIFFS`, not papered over here.
*/
import type { ConfigDiagnostics, KimiConfig } from '#/types';
/**
* Every top-level `KimiConfig` field except `raw` (a v1 write-path
* implementation detail with no v2 counterpart). Each entry is both the v1
* field name and the v2 config domain name.
*/
const KIMI_CONFIG_DOMAINS = [
'providers',
'defaultProvider',
'defaultModel',
'models',
'thinking',
'planMode',
'yolo',
'defaultPermissionMode',
'defaultPlanMode',
'permission',
'hooks',
'services',
'mergeAllAvailableSkills',
'extraSkillDirs',
'loopControl',
'background',
'subagent',
'secondaryModel',
'mcp',
'image',
'modelCatalog',
'experimental',
'telemetry',
] as const;
/**
* Pick the v1-shaped fields out of the v2 engine's resolved config
* (`config.getAll()` — the effective view: file values plus env overlays
* plus registered section defaults). Domains v2 knows but v1 does not
* (`cron`, `tools`, `extraAgentDirs`, ...) are dropped,
* mirroring how v1's schema strips unknown top-level keys.
*/
export function resolvedConfigToKimiConfig(resolved: Record<string, unknown>): KimiConfig {
const config: Record<string, unknown> = {};
for (const domain of KIMI_CONFIG_DOMAINS) {
const value = resolved[domain];
if (value !== undefined) {
config[domain] = value;
}
}
return config as KimiConfig;
}
/** Structural minimum of the v2 engine's `ConfigDiagnostic`. */
export interface V2ConfigDiagnostic {
readonly domain?: string;
readonly severity: string;
readonly message: string;
}
/**
* v1 reports diagnostics as flat warning strings; v2 carries structured
* `{domain, severity, message}` entries. The SDK contract is the v1 shape,
* so the message texts are the warnings (severity/domain stay available to
* v2-native callers through the klient facade).
*/
export function diagnosticsToConfigDiagnostics(
diagnostics: readonly V2ConfigDiagnostic[],
): ConfigDiagnostics {
return { warnings: diagnostics.map((diagnostic) => diagnostic.message) };
}
/** The writes needed to reproduce v1 `removeKimiProvider` semantics. */
export interface ProviderRemovalPlan {
readonly providers: Record<string, unknown>;
readonly models: Record<string, unknown>;
readonly clearDefaultModel: boolean;
readonly clearDefaultProvider: boolean;
}
/**
* Compute the v1 cascade for removing a provider: drop the provider entry,
* drop every model whose `provider` points at it, and clear the default
* pointers when they dangle. The v2 engine's own `providerService.delete`
* only clears the default-provider pointer, so the SDK replays the full v1
* cascade through the config facade. Inputs are the USER-layer values
* (`inspect().userValue`), matching v1's disk-config write base.
*
* The `[secondary_model]` section is deliberately left untouched: it is the
* user's own configuration, and an entry whose model no longer resolves
* fails pool validation on the next session create with a message naming
* the offending alias — a loud error beats a silent rewrite.
*/
export function planProviderRemoval(input: {
readonly providers: Record<string, unknown> | undefined;
readonly models: Record<string, Record<string, unknown>> | undefined;
readonly defaultModel: string | undefined;
readonly defaultProvider: string | undefined;
readonly providerId: string;
}): ProviderRemovalPlan {
const providers = { ...input.providers };
delete providers[input.providerId];
const models: Record<string, unknown> = {};
let removedDefault = false;
for (const [key, model] of Object.entries(input.models ?? {})) {
if (model['provider'] === input.providerId) {
if (input.defaultModel === key) removedDefault = true;
continue;
}
models[key] = model;
}
return {
providers,
models,
clearDefaultModel: removedDefault,
clearDefaultProvider: input.defaultProvider === input.providerId,
};
}
/**
* Apply the v1 remove-provider cascade to a whole `KimiConfig` in memory (no
* persistence): drop the provider entry, every model pointing at it, and the
* default pointers when they dangle. Hosts that stage a removal and fold it
* into a later atomic write (instead of persisting it immediately) build on
* this — the same role the v2 engine's `shapeWithoutProvider` plays for its
* own refresh path.
*/
export function removeProviderFromConfig(config: KimiConfig, providerId: string): KimiConfig {
const plan = planProviderRemoval({
providers: config.providers as Record<string, unknown> | undefined,
models: config.models as Record<string, Record<string, unknown>> | undefined,
defaultModel: config.defaultModel,
defaultProvider: config.defaultProvider,
providerId,
});
return {
...config,
providers: plan.providers as KimiConfig['providers'],
models: plan.models as KimiConfig['models'],
defaultModel: plan.clearDefaultModel ? undefined : config.defaultModel,
defaultProvider: plan.clearDefaultProvider ? undefined : config.defaultProvider,
};
}