File size: 5,877 Bytes
4e23b01
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
/**
 * 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,
  };
}