Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions reference/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ billion-context/
│ ├── kiro.ts # Kiro CLI: model-gateway hosts, built-in developer/system→user roles, kiro:conversation_id identity
│ ├── launcher.ts # `bili <client>` launcher (auto-proxies both schemes)
│ ├── client-config.ts # Read-only discovery of each client's own upstream config
│ ├── toml-integer.ts # Shared strict TOML integer reader for model token limits
│ ├── plugin.ts # Cooperative plugin protocol (x-bili-plugin lane, legacy marker #920)
│ ├── plugin-install.ts # `bili plugin install` driving host channels
│ ├── dsh-channel.ts # dsh profile bundle management (refreshDshProfileCopy, #1196)
Expand Down
15 changes: 9 additions & 6 deletions src/client-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import os from "node:os";
import path from "node:path";
import { parse as parseJsonc, type ParseError } from "jsonc-parser";
import { vertexEnabled } from "./vertex.js";
import { nonNegativeTomlInteger } from "./toml-integer.js";

export interface ClaudeSettings {
anthropicBaseUrl?: string;
Expand Down Expand Up @@ -720,7 +721,7 @@ export function parseCodexToml(text: string): CodexConfig {
continue;
}
const strMatch = /^([A-Za-z0-9_.-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/.exec(line);
const numMatch = /^([A-Za-z0-9_.-]+)\s*=\s*([0-9]+)\b/.exec(line);
const numMatch = /^([A-Za-z0-9_.-]+)\s*=\s*([^\s#]+)\s*(?:#.*)?$/.exec(line);
if (strMatch) {
const key = strMatch[1];
const val = strMatch[2] !== undefined ? strMatch[2] : strMatch[3];
Expand All @@ -733,9 +734,11 @@ export function parseCodexToml(text: string): CodexConfig {
result.providers[curProvider].baseUrl = val;
}
} else if (numMatch && table === "") {
if (numMatch[1] === "model_context_window") codexContextWindow = Number(numMatch[2]);
else if (numMatch[1] === "model_auto_compact_token_limit") codexAutoCompactLimit = Number(numMatch[2]);
else if (numMatch[1] === "model_max_output_tokens") codexMaxOutput = Number(numMatch[2]);
const numVal = nonNegativeTomlInteger(numMatch[2]);
if (numVal === undefined) continue;
if (numMatch[1] === "model_context_window") codexContextWindow = numVal;
else if (numMatch[1] === "model_auto_compact_token_limit") codexAutoCompactLimit = numVal;
else if (numMatch[1] === "model_max_output_tokens") codexMaxOutput = numVal;
}
}
if (codexModel) result.model = codexModel;
Expand Down Expand Up @@ -1079,11 +1082,11 @@ export function parseKimiToml(text: string): KimiConfig {
continue;
}
const strMatch = /^([A-Za-z0-9_.-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/.exec(line);
const numMatch = /^([A-Za-z0-9_.-]+)\s*=\s*([0-9]+)\b/.exec(line);
const numMatch = /^([A-Za-z0-9_.-]+)\s*=\s*([^\s#]+)\s*(?:#.*)?$/.exec(line);
if (!strMatch && !numMatch) continue;
const key = (strMatch ?? numMatch)![1];
const strVal = strMatch ? (strMatch[2] !== undefined ? strMatch[2] : strMatch[3]) : undefined;
const numVal = numMatch ? Number(numMatch[2]) : undefined;
const numVal = numMatch ? nonNegativeTomlInteger(numMatch[2]) : undefined;
if (parts.length === 0) {
if (key === "default_model" && strVal !== undefined) result.defaultModel = strVal;
} else if (parts[0] === "providers") {
Expand Down
8 changes: 6 additions & 2 deletions src/kimi/toml-edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// own writeback is line-surgical per domain — our block survives login/logout/
// catalog refresh as long as it uses its own provider name + model alias
// (the refresh orchestrator preserves user-owned entries).
import { nonNegativeTomlInteger } from "../toml-integer.js";

export const KIMI_PROVIDER = "bili";
export const KIMI_ALIAS = "bili-kimi";
Expand Down Expand Up @@ -55,8 +56,11 @@ function keyValue(line: string): { key: string; strVal?: string; numVal?: number
if (!l || l.startsWith("#") || l.startsWith("[")) return undefined;
const strMatch = /^([A-Za-z0-9_.-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/.exec(l);
if (strMatch) return { key: strMatch[1], strVal: strMatch[2] !== undefined ? strMatch[2] : strMatch[3] };
const numMatch = /^([A-Za-z0-9_.-]+)\s*=\s*([0-9]+)\b/.exec(l);
if (numMatch) return { key: numMatch[1], numVal: Number(numMatch[2]) };
const numMatch = /^([A-Za-z0-9_.-]+)\s*=\s*([^\s#]+)\s*(?:#.*)?$/.exec(l);
if (numMatch) {
const numVal = nonNegativeTomlInteger(numMatch[2]);
if (numVal !== undefined) return { key: numMatch[1], numVal };
}
// Inline tables / arrays ({ x-bili-plugin = "kimi" }, ["a"]) have no scalar
// value — key-only, so callers can still locate/replace the line.
const tblMatch = /^([A-Za-z0-9_.-]+)\s*=\s*[{[]/.exec(l);
Expand Down
8 changes: 8 additions & 0 deletions src/toml-integer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** Parse the non-negative TOML integer subset used for model token limits. */
export function nonNegativeTomlInteger(value: string): number | undefined {
// Reject numeric prefixes of floats/dates. Underscores only separate digits
// of the selected base, and model limits must remain exactly representable.
if (!/^(?:\+?(?:0|[1-9](?:_?[0-9])*)|0x[0-9A-Fa-f](?:_?[0-9A-Fa-f])*|0o[0-7](?:_?[0-7])*|0b[01](?:_?[01])*)$/.test(value)) return undefined;
const parsed = Number(value.replace(/_/g, ""));
return Number.isSafeInteger(parsed) ? parsed : undefined;
}
68 changes: 68 additions & 0 deletions tests/client-config-toml-integers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import test from "node:test";
import assert from "node:assert/strict";
import { parseCodexToml, parseKimiToml } from "../src/client-config.ts";

const valid: ReadonlyArray<readonly [string, number]> = [
["128_000", 128000],
["+262_144", 262144],
["0x1_f400", 128000],
["0o372_000", 128000],
["0b1_1111_0100_0000_0000", 128000],
["65536", 65536],
];

for (const [literal, expected] of valid) {
test(`Codex TOML model limits retain the integer ${literal}`, () => {
const config = parseCodexToml([
'model = "custom-model"',
`model_context_window = ${literal} # context`,
`model_auto_compact_token_limit = ${literal}`,
`model_max_output_tokens = ${literal}`,
].join("\n"));
assert.equal(config.contextWindow, expected);
assert.equal(config.autoCompactLimit, expected);
assert.equal(config.maxOutput, expected);
assert.deepEqual(config.modelWindows, [{ id: "custom-model", contextWindow: expected, maxOutput: expected }]);
});

test(`Kimi launcher model and override limits retain the integer ${literal}`, () => {
const config = parseKimiToml([
"[models.base]",
'model = "base-model"',
`max_context_size = ${literal} # context`,
"[models.overridden]",
'model = "overridden-model"',
"max_context_size = 1234",
"[models.overridden.overrides]",
`max_context_size = ${literal}`,
].join("\n"));
assert.deepEqual(config.models, [
{ id: "base-model", contextWindow: expected },
{ id: "overridden-model", contextWindow: expected },
]);
});
}

for (const literal of ["1.5", "2026-01-01", "01", "1 trailing", "9007199254740993", "1_", "1__0", "0x_10", "+0x10", "-1"]) {
test(`client TOML readers reject the non-limit ${literal} without using its prefix`, () => {
const codex = parseCodexToml([
'model = "custom-model"',
`model_context_window = ${literal}`,
`model_auto_compact_token_limit = ${literal}`,
`model_max_output_tokens = ${literal}`,
].join("\n"));
assert.equal(codex.contextWindow, undefined);
assert.equal(codex.autoCompactLimit, undefined);
assert.equal(codex.maxOutput, undefined);
assert.equal(codex.modelWindows, undefined);
const kimi = parseKimiToml([
"[models.invalid]",
`max_context_size = ${literal}`,
"[models.valid]",
"max_context_size = 65536",
"[models.valid.overrides]",
`max_context_size = ${literal}`,
].join("\n"));
assert.deepEqual(kimi.models, [{ id: "valid", contextWindow: 65536 }]);
});
}
36 changes: 36 additions & 0 deletions tests/kimi-native.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,42 @@ test("scanKimiModelWindows keys windows by model id across all sections (#2954)"
assert.deepEqual(scanKimiModelWindows(SAMPLE_CONFIG), {}, "no windows declared → empty map, nothing to feed");
});

for (const [literal, expected] of [
["128_000", 128000],
["+131072", 131072],
["0x2_0000", 131072],
["0o400_000", 131072],
["0b10_0000_0000_0000_0000", 131072],
["65536 # context limit", 65536],
] as const) {
test(`Kimi preserves TOML integer limit ${literal} through routing and unroute`, () => {
const config = THINKING_CONFIG.replace("max_context_size = 65536", `max_context_size = ${literal}`)
.replace("max_output_size = 8192", "max_output_size = 8_192");
assert.deepEqual(scanKimiModelWindows(config), { "deepseek-r1": expected });
const route = okOf(resolveKimiRoute(config, {}));
assert.equal(route.contextWindow, expected);
assert.equal(route.maxOutput, 8192);
const applied = applyKimiManagedConfig(config, stateOf(route));
const reread = okOf(resolveKimiRoute(applied, {}));
assert.equal(reread.contextWindow, expected);
assert.equal(reread.maxOutput, 8192);
assert.equal(applyKimiManagedConfig(applied, stateOf(reread)), applied);
assert.ok(unrouteKimiConfig(applied).includes(`max_context_size = ${literal}`), "the user's literal survives uninstall");
});
}

for (const literal of ["1.5", "1e6", "2026-01-01", "128__000", "128_", "_128", "01", "0x_1", "1 trailing", "9007199254740993"]) {
test(`Kimi does not mistake ${literal} for an integer limit`, () => {
const config = THINKING_CONFIG.replace("max_context_size = 65536", `max_context_size = ${literal}`)
.replace("max_output_size = 8192", `max_output_size = ${literal}`);
assert.deepEqual(scanKimiModelWindows(config), {});
const route = okOf(resolveKimiRoute(config, {}));
assert.equal(route.contextWindow, undefined);
assert.equal(route.maxOutput, undefined);
assert.ok(!aliasSection(applyKimiManagedConfig(config, stateOf(route))).includes("max_"));
});
}

test("kimiConfigModelWindows hands both kimi proxy paths ONE windows source (#3436/#3557)", () => {
const home = path.join(tmpdir(), `bili-kimi-win-${process.pid}-${Date.now()}`);
mkdirSync(home, { recursive: true });
Expand Down
Loading