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
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:
1 parent
89c71fbfe6
commit
9a3875d990
19 files changed
+152041
-140593
No files matched your search
+83635
-78361
File diff suppressed because it is too large.
Load diff
+66672
-62143
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,426 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
getSecondaryModel,
|
||||
getSubagentModelPool,
|
||||
removeSubagentPoolEntry,
|
||||
setSecondaryModelOnly,
|
||||
setSubagentDefault,
|
||||
setSubagentForce,
|
||||
setSubagentModelPool,
|
||||
upsertSubagentPoolEntry,
|
||||
validateSubagentPool,
|
||||
type SubagentModelPool,
|
||||
} from "./subagent-settings";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers — drill into the `[secondary_model]` section of the written result
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function rawWith(section: Record<string, unknown>): unknown {
|
||||
return { secondary_model: section };
|
||||
}
|
||||
|
||||
function section(raw: unknown): Record<string, unknown> {
|
||||
const root = raw as { secondary_model?: unknown };
|
||||
const sec = root?.secondary_model;
|
||||
return sec && typeof sec === "object" && !Array.isArray(sec)
|
||||
? (sec as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
function modelsOf(raw: unknown): Record<string, unknown> {
|
||||
const m = section(raw)["models"];
|
||||
return m && typeof m === "object" && !Array.isArray(m)
|
||||
? (m as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Reading — four config combinations + effective-default normalization
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("getSecondaryModel / getSubagentModelPool", () => {
|
||||
it("returns an empty config / undefined pool when the section is absent or empty", () => {
|
||||
expect(getSecondaryModel({})).toEqual({});
|
||||
expect(getSubagentModelPool({})).toBeUndefined();
|
||||
expect(getSubagentModelPool(rawWith({}))).toBeUndefined();
|
||||
});
|
||||
|
||||
it("legacy `model` alone forms the effective default", () => {
|
||||
const raw = rawWith({ model: "kimi-k2" });
|
||||
expect(getSecondaryModel(raw)).toEqual({ model: "kimi-k2" });
|
||||
const pool = getSubagentModelPool(raw);
|
||||
expect(pool).toEqual({ model: "kimi-k2", models: {}, force: false });
|
||||
expect(pool?.defaultModel ?? pool?.model).toBe("kimi-k2");
|
||||
});
|
||||
|
||||
it("`default_model` alone forms the effective default", () => {
|
||||
const raw = rawWith({ default_model: "kimi-k2" });
|
||||
expect(getSecondaryModel(raw)).toEqual({ default_model: "kimi-k2" });
|
||||
const pool = getSubagentModelPool(raw);
|
||||
expect(pool?.defaultModel).toBe("kimi-k2");
|
||||
expect(pool?.model).toBeUndefined();
|
||||
expect(pool?.defaultModel ?? pool?.model).toBe("kimi-k2");
|
||||
});
|
||||
|
||||
it("a divergent `model` + `default_model` resolves the effective default to `default_model`", () => {
|
||||
const raw = rawWith({ model: "kimi-k2", default_model: "kimi-k1" });
|
||||
expect(getSecondaryModel(raw)).toEqual({
|
||||
model: "kimi-k2",
|
||||
default_model: "kimi-k1",
|
||||
});
|
||||
const pool = getSubagentModelPool(raw);
|
||||
expect(pool?.model).toBe("kimi-k2");
|
||||
expect(pool?.defaultModel).toBe("kimi-k1");
|
||||
expect(pool?.defaultModel ?? pool?.model).toBe("kimi-k1");
|
||||
});
|
||||
|
||||
it("reads the models table filtering non-string values, and force", () => {
|
||||
const raw = rawWith({
|
||||
default_model: "kimi-k2",
|
||||
models: { "kimi-k2": "Kimi K2", "kimi-k2-thinking": 42, broken: true },
|
||||
force: true,
|
||||
});
|
||||
expect(getSecondaryModel(raw).models).toEqual({ "kimi-k2": "Kimi K2" });
|
||||
expect(getSecondaryModel(raw).force).toBe(true);
|
||||
expect(getSubagentModelPool(raw)).toMatchObject({
|
||||
models: { "kimi-k2": "Kimi K2" },
|
||||
force: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("reports `force` only when true and tolerates a malformed models table", () => {
|
||||
expect(getSecondaryModel(rawWith({ force: false })).force).toBeUndefined();
|
||||
expect(getSubagentModelPool(rawWith({ force: false }))?.force).toBe(false);
|
||||
const pool = getSubagentModelPool(rawWith({ models: "nope", model: "kimi-k2" }));
|
||||
expect(pool?.models).toEqual({});
|
||||
expect(pool?.model).toBe("kimi-k2");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// setSecondaryModelOnly — pool-aware pick
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("setSecondaryModelOnly", () => {
|
||||
it("creates the section when absent", () => {
|
||||
const next = setSecondaryModelOnly({}, "kimi-k2");
|
||||
expect(section(next)).toEqual({ model: "kimi-k2" });
|
||||
});
|
||||
|
||||
it("without a pool: writes only `model`, drops known patch fields, keeps unknown fields", () => {
|
||||
const raw = rawWith({
|
||||
model: "old",
|
||||
default_effort: "high",
|
||||
max_output_size: 64000,
|
||||
max_context_size: 128000,
|
||||
custom_field: "keep",
|
||||
});
|
||||
const next = setSecondaryModelOnly(raw, "kimi-k2");
|
||||
expect(section(next)).toEqual({ model: "kimi-k2", custom_field: "keep" });
|
||||
});
|
||||
|
||||
it("pool via default_model: syncs both keys, adds a missing alias to the table, preserves the rest", () => {
|
||||
const raw = rawWith({
|
||||
default_model: "kimi-k1",
|
||||
models: { "kimi-k1": "K1" },
|
||||
force: true,
|
||||
unknown_field: "keep",
|
||||
});
|
||||
const next = setSecondaryModelOnly(raw, "kimi-k2");
|
||||
expect(section(next)).toEqual({
|
||||
default_model: "kimi-k2",
|
||||
model: "kimi-k2",
|
||||
models: { "kimi-k1": "K1", "kimi-k2": "" },
|
||||
force: true,
|
||||
unknown_field: "keep",
|
||||
});
|
||||
});
|
||||
|
||||
it("pool via models table: keeps existing descriptions", () => {
|
||||
const raw = rawWith({ models: { "kimi-k1": "K1", "kimi-k2": "K2" } });
|
||||
const next = setSecondaryModelOnly(raw, "kimi-k2");
|
||||
expect(modelsOf(next)).toEqual({ "kimi-k1": "K1", "kimi-k2": "K2" });
|
||||
expect(section(next).default_model).toBe("kimi-k2");
|
||||
expect(section(next).model).toBe("kimi-k2");
|
||||
});
|
||||
|
||||
it("heals a divergent legacy + pool default to the picked alias", () => {
|
||||
const raw = rawWith({
|
||||
model: "kimi-k1",
|
||||
default_model: "kimi-k2",
|
||||
models: { "kimi-k1": "K1", "kimi-k2": "K2" },
|
||||
});
|
||||
const next = setSecondaryModelOnly(raw, "kimi-k1");
|
||||
expect(section(next).model).toBe("kimi-k1");
|
||||
expect(section(next).default_model).toBe("kimi-k1");
|
||||
expect(modelsOf(next)).toEqual({ "kimi-k1": "K1", "kimi-k2": "K2" });
|
||||
});
|
||||
|
||||
it("preserves other top-level raw_other sections", () => {
|
||||
const raw = {
|
||||
experimental: { "secondary-model": true },
|
||||
secondary_model: { model: "kimi-k1" },
|
||||
};
|
||||
const next = setSecondaryModelOnly(raw, "kimi-k2") as {
|
||||
experimental?: unknown;
|
||||
};
|
||||
expect(next.experimental).toEqual({ "secondary-model": true });
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pool mutations — rebuild / upsert / remove / default
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("pool mutations", () => {
|
||||
it("setSubagentModelPool rebuilds with dual-written default (effective wins) and force", () => {
|
||||
const raw = rawWith({ model: "stale", custom: "keep" });
|
||||
const next = setSubagentModelPool(raw, {
|
||||
defaultModel: "kimi-k2",
|
||||
model: "kimi-k1", // divergent — effective default (`defaultModel`) wins
|
||||
models: { "kimi-k2": "K2" },
|
||||
force: true,
|
||||
});
|
||||
expect(section(next)).toEqual({
|
||||
default_model: "kimi-k2",
|
||||
model: "kimi-k2",
|
||||
models: { "kimi-k2": "K2" },
|
||||
force: true,
|
||||
custom: "keep",
|
||||
});
|
||||
});
|
||||
|
||||
it("setSubagentModelPool omits the models table when the pool has no entries", () => {
|
||||
const next = setSubagentModelPool(rawWith({}), {
|
||||
defaultModel: "kimi-k2",
|
||||
models: {},
|
||||
force: false,
|
||||
});
|
||||
expect(section(next)).toEqual({
|
||||
default_model: "kimi-k2",
|
||||
model: "kimi-k2",
|
||||
force: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("setSubagentModelPool is idempotent", () => {
|
||||
const pool: SubagentModelPool = {
|
||||
defaultModel: "kimi-k2",
|
||||
models: { "kimi-k2": "K2" },
|
||||
force: true,
|
||||
};
|
||||
const once = setSubagentModelPool(rawWith({ custom: "keep" }), pool);
|
||||
const twice = setSubagentModelPool(once, pool);
|
||||
expect(once).toEqual(twice);
|
||||
});
|
||||
|
||||
it("upsertSubagentPoolEntry adds and updates aliases, preserving the rest", () => {
|
||||
const raw = rawWith({
|
||||
default_model: "kimi-k1",
|
||||
models: { "kimi-k1": "K1" },
|
||||
force: false,
|
||||
});
|
||||
const added = upsertSubagentPoolEntry(raw, "kimi-k2", "K2");
|
||||
expect(modelsOf(added)).toEqual({ "kimi-k1": "K1", "kimi-k2": "K2" });
|
||||
const updated = upsertSubagentPoolEntry(added, "kimi-k2", "K2 thinking");
|
||||
expect(modelsOf(updated)).toEqual({
|
||||
"kimi-k1": "K1",
|
||||
"kimi-k2": "K2 thinking",
|
||||
});
|
||||
expect(section(updated).default_model).toBe("kimi-k1");
|
||||
});
|
||||
|
||||
it("removeSubagentPoolEntry keeps the default when removing a non-default alias", () => {
|
||||
const raw = rawWith({
|
||||
default_model: "kimi-k1",
|
||||
model: "kimi-k1",
|
||||
models: { "kimi-k1": "K1", "kimi-k2": "K2", "kimi-k3": "K3" },
|
||||
});
|
||||
const next = removeSubagentPoolEntry(raw, "kimi-k3");
|
||||
expect(modelsOf(next)).toEqual({ "kimi-k1": "K1", "kimi-k2": "K2" });
|
||||
expect(section(next).default_model).toBe("kimi-k1");
|
||||
expect(section(next).model).toBe("kimi-k1");
|
||||
});
|
||||
|
||||
it("removeSubagentPoolEntry falls the default back to the first remaining key", () => {
|
||||
const raw = rawWith({
|
||||
default_model: "kimi-k1",
|
||||
model: "kimi-k1",
|
||||
models: { "kimi-k1": "K1", "kimi-k2": "K2" },
|
||||
});
|
||||
const next = removeSubagentPoolEntry(raw, "kimi-k1");
|
||||
expect(modelsOf(next)).toEqual({ "kimi-k2": "K2" });
|
||||
expect(section(next).default_model).toBe("kimi-k2");
|
||||
expect(section(next).model).toBe("kimi-k2");
|
||||
});
|
||||
|
||||
it("removeSubagentPoolEntry collapses an emptied pool to the implicit form", () => {
|
||||
const raw = rawWith({
|
||||
default_model: "kimi-k1",
|
||||
model: "kimi-k1",
|
||||
models: { "kimi-k1": "K1" },
|
||||
});
|
||||
const next = removeSubagentPoolEntry(raw, "kimi-k1");
|
||||
const sec = section(next);
|
||||
expect(sec).not.toHaveProperty("models");
|
||||
expect(sec.default_model).toBe("kimi-k1");
|
||||
expect(sec.model).toBe("kimi-k1");
|
||||
});
|
||||
|
||||
it("setSubagentDefault syncs both keys and adds a missing alias with an empty description", () => {
|
||||
const raw = rawWith({ models: { "kimi-k2": "K2" } });
|
||||
const next = setSubagentDefault(raw, "kimi-k1");
|
||||
expect(section(next).default_model).toBe("kimi-k1");
|
||||
expect(section(next).model).toBe("kimi-k1");
|
||||
expect(modelsOf(next)).toEqual({ "kimi-k2": "K2", "kimi-k1": "" });
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// setSubagentForce — mutual exclusion with the models table
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("setSubagentForce", () => {
|
||||
it("turning force on drops the models table but keeps the default and unknown fields", () => {
|
||||
const raw = rawWith({
|
||||
default_model: "kimi-k1",
|
||||
model: "kimi-k1",
|
||||
models: { "kimi-k1": "K1", "kimi-k2": "K2" },
|
||||
custom: "keep",
|
||||
});
|
||||
const next = setSubagentForce(raw, true);
|
||||
const sec = section(next);
|
||||
expect(sec).not.toHaveProperty("models");
|
||||
expect(sec.force).toBe(true);
|
||||
expect(sec.default_model).toBe("kimi-k1");
|
||||
expect(sec.model).toBe("kimi-k1");
|
||||
expect(sec.custom).toBe("keep");
|
||||
});
|
||||
|
||||
it("turning force off leaves the models table untouched", () => {
|
||||
const raw = rawWith({
|
||||
default_model: "kimi-k1",
|
||||
models: { "kimi-k1": "K1", "kimi-k2": "K2" },
|
||||
force: true,
|
||||
});
|
||||
const next = setSubagentForce(raw, false);
|
||||
expect(section(next).force).toBe(false);
|
||||
expect(modelsOf(next)).toEqual({ "kimi-k1": "K1", "kimi-k2": "K2" });
|
||||
});
|
||||
|
||||
it("is idempotent in both directions", () => {
|
||||
const raw = rawWith({ force: false, custom: "keep" });
|
||||
expect(setSubagentForce(raw, true)).toEqual(
|
||||
setSubagentForce(setSubagentForce(raw, true), true)
|
||||
);
|
||||
expect(setSubagentForce(raw, false)).toEqual(
|
||||
setSubagentForce(setSubagentForce(raw, false), false)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// validateSubagentPool — mirror of the 6 engine rules
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("validateSubagentPool", () => {
|
||||
const aliases = ["kimi-k1", "kimi-k2", "kimi-k3"];
|
||||
|
||||
const pool = (p: Partial<SubagentModelPool>): SubagentModelPool => ({
|
||||
models: {},
|
||||
force: false,
|
||||
...p,
|
||||
});
|
||||
|
||||
it("accepts a valid pool", () => {
|
||||
expect(
|
||||
validateSubagentPool(
|
||||
pool({
|
||||
defaultModel: "kimi-k2",
|
||||
models: { "kimi-k2": "K2", "kimi-k3": "K3" },
|
||||
}),
|
||||
aliases
|
||||
)
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("accepts the implicit single-entry form when the model resolves", () => {
|
||||
expect(validateSubagentPool(pool({ model: "kimi-k2" }), aliases)).toEqual([]);
|
||||
});
|
||||
|
||||
it("accepts force with a model fallback when no pool table exists", () => {
|
||||
expect(
|
||||
validateSubagentPool(pool({ force: true, model: "kimi-k2" }), aliases)
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("flags `primary` as a reserved pool key", () => {
|
||||
const byDefault = validateSubagentPool(pool({ defaultModel: "primary" }), aliases);
|
||||
expect(byDefault).toContainEqual({
|
||||
key: "primaryReserved",
|
||||
params: { alias: "primary" },
|
||||
});
|
||||
const byEntry = validateSubagentPool(
|
||||
pool({ defaultModel: "kimi-k1", models: { primary: "" } }),
|
||||
aliases
|
||||
);
|
||||
expect(byEntry).toContainEqual({
|
||||
key: "primaryReserved",
|
||||
params: { alias: "primary" },
|
||||
});
|
||||
});
|
||||
|
||||
it("requires `default_model` when the models table exists", () => {
|
||||
// Strict mirror of the engine: a `model` fallback does not satisfy the
|
||||
// `default_model` requirement once a `models` table is present.
|
||||
expect(
|
||||
validateSubagentPool(
|
||||
pool({ model: "kimi-k2", models: { "kimi-k2": "K2" } }),
|
||||
aliases
|
||||
)
|
||||
).toEqual([{ key: "defaultRequired" }]);
|
||||
});
|
||||
|
||||
it("flags a default that is not a key of the pool table", () => {
|
||||
const errors = validateSubagentPool(
|
||||
pool({ defaultModel: "kimi-k3", models: { "kimi-k2": "K2" } }),
|
||||
aliases
|
||||
);
|
||||
expect(errors).toContainEqual({
|
||||
key: "defaultNotInPool",
|
||||
params: { alias: "kimi-k3" },
|
||||
});
|
||||
});
|
||||
|
||||
it("flags pool keys that do not resolve in the [models] registry", () => {
|
||||
const errors = validateSubagentPool(
|
||||
pool({ defaultModel: "kimi-k2", models: { "kimi-k2": "K2", ghost: "?" } }),
|
||||
aliases
|
||||
);
|
||||
expect(errors).toContainEqual({
|
||||
key: "aliasNotInModels",
|
||||
params: { alias: "ghost" },
|
||||
});
|
||||
// the implicit single-entry default is checked against the registry too
|
||||
const implicit = validateSubagentPool(pool({ model: "ghost" }), aliases);
|
||||
expect(implicit).toContainEqual({
|
||||
key: "aliasNotInModels",
|
||||
params: { alias: "ghost" },
|
||||
});
|
||||
});
|
||||
|
||||
it("flags force without any default", () => {
|
||||
expect(validateSubagentPool(pool({ force: true }), aliases)).toEqual([
|
||||
{ key: "forceRequiresDefault" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("flags force coexisting with the models table", () => {
|
||||
const errors = validateSubagentPool(
|
||||
pool({ force: true, defaultModel: "kimi-k2", models: { "kimi-k2": "K2" } }),
|
||||
aliases
|
||||
);
|
||||
expect(errors).toContainEqual({ key: "forceExcludesModels" });
|
||||
});
|
||||
});
|
||||
+293
-18
@@ -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) };
|
||||
|
||||
Reference in new issue
Block a user