feat(subagent): v0.7.5 — 适配 kimi-code 0.36.0 子代理模型池(双写兼容/池管理/force/前置校验)+ 热力图详情按需加载 + models.dev 刷新(GLM-5.3)
Release / Version consistency (push) Canceled after 0s
Release / Build (macos-latest) (push) Canceled after 0s
Release / Build (ubuntu-latest) (push) Canceled after 0s
Release / Build (windows-latest) (push) Canceled after 0s
Release / Attach macOS install script (push) Canceled after 0s

This commit is contained in:
KimiSwitch Dev committed 2026-08-15 23:21:22 +08:00
1 parent 89c71fbfe6
commit 9a3875d990
19 files changed
+152041 -140593

No files matched your search

+293 -18
View File
@@ -99,15 +99,48 @@ export function setExperimentalFlag(
}
// ---------------------------------------------------------------------------
// [secondary_model] — subagent secondary model
// [secondary_model] — subagent model pool
// ---------------------------------------------------------------------------
/**
* Parsed subagent model pool (`[secondary_model]` v2 keys). The currently
* effective default is `defaultModel ?? model` (v2 pool key first, legacy v1
* recipe key as fallback).
*/
export interface SubagentModelPool {
/** v2 pool default (`default_model`). */
defaultModel?: string;
/** Legacy v1 recipe key (`model`), kept in sync with `defaultModel`. */
model?: string;
/** `[models]` table (alias → description); non-string values dropped. */
models: Record<string, string>;
/** Force subagents onto the default alias. */
force: boolean;
}
/** Known v1 patch fields, dropped when re-picking without a pool so stale
* patches are cleaned up. */
const PATCH_FIELDS: ReadonlySet<string> = new Set([
"default_effort",
"max_output_size",
"max_context_size",
]);
/** Read `[secondary_model]` from raw_other; `{}` when absent. */
export function getSecondaryModel(rawOther: unknown): SecondaryModelConfig {
const section = asRecord(rawOther)["secondary_model"];
const cfg = asRecord(section);
const out: SecondaryModelConfig = {};
if (typeof cfg.default_model === "string" && cfg.default_model !== "")
out.default_model = cfg.default_model;
if (typeof cfg.model === "string" && cfg.model !== "") out.model = cfg.model;
const models = asRecord(cfg.models);
const pool: Record<string, string> = {};
for (const [k, v] of Object.entries(models)) {
if (typeof v === "string") pool[k] = v;
}
if (Object.keys(pool).length > 0) out.models = pool;
if (cfg.force === true) out.force = true;
if (typeof cfg.default_effort === "string" && cfg.default_effort !== "")
out.default_effort = cfg.default_effort;
if (typeof cfg.max_output_size === "number" && cfg.max_output_size > 0)
@@ -117,15 +150,40 @@ export function getSecondaryModel(rawOther: unknown): SecondaryModelConfig {
return out;
}
/** Read the v2 subagent model pool from raw_other; `undefined` when there is
* no `[secondary_model]` section or it is empty. The effective default is
* `defaultModel ?? model`. */
export function getSubagentModelPool(
rawOther: unknown
): SubagentModelPool | undefined {
const section = asRecord(asRecord(rawOther)["secondary_model"]);
if (Object.keys(section).length === 0) return undefined;
const models: Record<string, string> = {};
for (const [k, v] of Object.entries(asRecord(section["models"]))) {
if (typeof v === "string") models[k] = v;
}
const out: SubagentModelPool = { models, force: section["force"] === true };
if (typeof section["default_model"] === "string" && section["default_model"] !== "")
out.defaultModel = section["default_model"];
if (typeof section["model"] === "string" && section["model"] !== "")
out.model = section["model"];
return out;
}
/**
* Write `[secondary_model]` with only the `model` field — no patch fields.
* Subagents then bind the pointed model entry directly and inherit its
* settings (context size, capabilities, effort fallback), which also keeps
* usage records keyed on the real model alias instead of the synthesized
* `__secondary__` derived entry. Unknown fields already present in the
* section are preserved; the known patch fields (`default_effort`,
* `max_output_size`, `max_context_size`) are dropped so stale patches are
* cleaned up when the model is re-picked.
* Write `[secondary_model]` picking the given alias as the subagent model.
*
* Pool-aware: when a pool exists (`models` table or `default_model` present)
* the alias becomes the v2 default (added to the `models` table with an empty
* description when missing) and the legacy `model` key is synced; the rest of
* the section — `force`, patch fields, unknown fields — is preserved. This
* keeps usage records keyed on the real model alias instead of the
* synthesized `__secondary__` derived entry.
*
* Without a pool the legacy single-model form is written: only `model`, with
* the known patch fields (`default_effort`, `max_output_size`,
* `max_context_size`) dropped so stale patches are cleaned up when the model
* is re-picked; unknown fields are preserved.
*/
export function setSecondaryModelOnly(
rawOther: unknown,
@@ -133,20 +191,237 @@ export function setSecondaryModelOnly(
): unknown {
const root = { ...asRecord(rawOther) };
const section = asRecord(asRecord(rawOther)["secondary_model"]);
const next: Record<string, unknown> = { model: alias };
const knownFields: ReadonlySet<string> = new Set([
"model",
"default_effort",
"max_output_size",
"max_context_size",
]);
for (const [k, v] of Object.entries(section)) {
if (!knownFields.has(k)) next[k] = v;
const next: Record<string, unknown> = {};
const hasPool =
section["models"] !== undefined || section["default_model"] !== undefined;
if (hasPool) {
for (const [k, v] of Object.entries(section)) next[k] = v;
next.default_model = alias;
next.model = alias;
const models: Record<string, unknown> = {};
for (const [k, v] of Object.entries(asRecord(next.models))) models[k] = v;
if (!(alias in models)) models[alias] = "";
next.models = models;
} else {
for (const [k, v] of Object.entries(section)) {
if (!PATCH_FIELDS.has(k)) next[k] = v;
}
next.model = alias;
}
root.secondary_model = next;
return root;
}
/**
* Rebuild the subagent model pool atomically. `default_model` and `model` are
* written with the same value — the effective default, `defaultModel ?? model`
* — plus the `models` table and `force`; unknown fields in the section are
* preserved. An empty `models` table is omitted: the engine requires
* `default_model` to be a key of the table, so an empty table can never be
* valid, and omitting it keeps the implicit single-entry form.
*/
export function setSubagentModelPool(
rawOther: unknown,
pool: SubagentModelPool
): unknown {
const root = { ...asRecord(rawOther) };
const section = asRecord(asRecord(rawOther)["secondary_model"]);
const next: Record<string, unknown> = {};
for (const [k, v] of Object.entries(section)) next[k] = v;
const effective = pool.defaultModel ?? pool.model;
if (effective) {
next.default_model = effective;
next.model = effective;
} else {
delete next.default_model;
delete next.model;
}
if (Object.keys(pool.models).length > 0) {
next.models = { ...pool.models };
} else {
delete next.models;
}
next.force = pool.force === true;
root.secondary_model = next;
return root;
}
/** Add or update one alias in the pool `models` table. The rest of the
* section (default, force, unknown fields) is preserved untouched. */
export function upsertSubagentPoolEntry(
rawOther: unknown,
alias: string,
description: string
): unknown {
const root = { ...asRecord(rawOther) };
const section = asRecord(asRecord(rawOther)["secondary_model"]);
const next: Record<string, unknown> = {};
for (const [k, v] of Object.entries(section)) next[k] = v;
const models: Record<string, unknown> = {};
for (const [k, v] of Object.entries(asRecord(next.models))) models[k] = v;
models[alias] = description;
next.models = models;
root.secondary_model = next;
return root;
}
/**
* Remove one alias from the pool `models` table. If it was the default, the
* default falls back to the first remaining key (`default_model` and `model`
* are both synced). Removing the last entry drops the `models` table and
* leaves `default_model`/`model` in place — the implicit single-entry pool.
*/
export function removeSubagentPoolEntry(
rawOther: unknown,
alias: string
): unknown {
const root = { ...asRecord(rawOther) };
const section = asRecord(asRecord(rawOther)["secondary_model"]);
const next: Record<string, unknown> = {};
for (const [k, v] of Object.entries(section)) next[k] = v;
const models: Record<string, unknown> = {};
for (const [k, v] of Object.entries(asRecord(next.models))) {
if (k !== alias) models[k] = v;
}
const remaining = Object.keys(models);
if (remaining.length === 0) {
delete next.models;
} else {
next.models = models;
if (next.default_model === alias) {
next.default_model = remaining[0];
next.model = remaining[0];
}
}
root.secondary_model = next;
return root;
}
/** Set the pool default alias, syncing `default_model` and `model`; the alias
* is added to the `models` table with an empty description when missing. */
export function setSubagentDefault(
rawOther: unknown,
alias: string
): unknown {
const root = { ...asRecord(rawOther) };
const section = asRecord(asRecord(rawOther)["secondary_model"]);
const next: Record<string, unknown> = {};
for (const [k, v] of Object.entries(section)) next[k] = v;
const models: Record<string, unknown> = {};
for (const [k, v] of Object.entries(asRecord(next.models))) models[k] = v;
if (!(alias in models)) models[alias] = "";
next.models = models;
next.default_model = alias;
next.model = alias;
root.secondary_model = next;
return root;
}
/**
* Set the `force` flag. Turning force on drops the `models` table — `force`
* and the pool table are mutually exclusive per the engine (the implicit
* `default_model`/`model` form stays); turning it off leaves the table
* untouched. Everything else in the section is preserved.
*/
export function setSubagentForce(
rawOther: unknown,
force: boolean
): unknown {
const root = { ...asRecord(rawOther) };
const section = asRecord(asRecord(rawOther)["secondary_model"]);
const next: Record<string, unknown> = {};
for (const [k, v] of Object.entries(section)) {
if (force && k === "models") continue;
next[k] = v;
}
next.force = force;
root.secondary_model = next;
return root;
}
/** Keys of validation errors, mirroring the kimi-code engine checks. */
export type ValidationErrorKey =
| "primaryReserved"
| "defaultRequired"
| "defaultNotInPool"
| "aliasNotInModels"
| "forceRequiresDefault"
| "forceExcludesModels";
/** A pool validation error; `params` carries the offending alias. */
export interface PoolValidationError {
key: ValidationErrorKey;
params?: Record<string, string>;
}
/**
* Mirror the kimi-code engine validation for the subagent model pool
* (violations fail the kimi session with CONFIG_INVALID):
* 1. `"primary"` is reserved and cannot be a pool key;
* 2. a `models` table requires `default_model`;
* 3. `default_model` must be a key of the `models` table;
* 4. every pool key must resolve in the top-level `[models]` registry;
* 5. `force` requires a default (`default_model` or the `model` fallback);
* 6. `force` and a `models` table are mutually exclusive.
*
* `availableAliases` is the set of top-level `[models]` aliases. A pool
* without a `models` table is the implicit single-entry form: only the
* effective default is checked against the registry.
*/
export function validateSubagentPool(
pool: SubagentModelPool,
availableAliases: string[]
): PoolValidationError[] {
const errors: PoolValidationError[] = [];
const { defaultModel, model, models, force } = pool;
const effectiveDefault = defaultModel ?? model;
const poolKeys = Object.keys(models);
const hasPoolTable = poolKeys.length > 0;
const registry = new Set(availableAliases);
const aliasError = (alias: string) => {
if (alias !== "primary" && !registry.has(alias)) {
errors.push({ key: "aliasNotInModels", params: { alias } });
}
};
// Rule 1: "primary" is reserved as a pool key.
if (effectiveDefault === "primary") {
errors.push({ key: "primaryReserved", params: { alias: "primary" } });
}
for (const key of poolKeys) {
if (key === "primary") {
errors.push({ key: "primaryReserved", params: { alias: key } });
}
}
// Rule 2: a `models` table requires `default_model`.
if (hasPoolTable && !defaultModel) {
errors.push({ key: "defaultRequired" });
}
// Rule 3: `default_model` must be a key of the `models` table.
if (hasPoolTable && defaultModel && !(defaultModel in models)) {
errors.push({ key: "defaultNotInPool", params: { alias: defaultModel } });
}
// Rule 4: every pool key must resolve in the `[models]` registry.
for (const key of poolKeys) aliasError(key);
if (!hasPoolTable && effectiveDefault) aliasError(effectiveDefault);
// Rule 5: `force` requires a default (`default_model` or `model` fallback).
if (force && !effectiveDefault) {
errors.push({ key: "forceRequiresDefault" });
}
// Rule 6: `force` and a `models` table are mutually exclusive.
if (force && hasPoolTable) {
errors.push({ key: "forceExcludesModels" });
}
return errors;
}
/** Remove `[secondary_model]` entirely (the "not configured" option). */
export function clearSecondaryModel(rawOther: unknown): unknown {
const root = { ...asRecord(rawOther) };