fix(model): added bracket-affix stripping and string-keyed resolution cache
- Replaced WeakMap model cache with provider/id string keys for stable reuse. - Returned official model ids directly when matched, before heuristics. - Collapsed non-message token path to system prompt and tool schema totals.
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
getBracketStrippedModelIdCandidates,
|
||||
getLongestModelLikeIdSegment,
|
||||
getModelLikeIdSegments,
|
||||
stripBracketedModelIdAffixes,
|
||||
} from "../src/config/model-id-affixes";
|
||||
|
||||
describe("getModelLikeIdSegments", () => {
|
||||
test("keeps only family-prefixed segments that carry a digit, deduped", () => {
|
||||
expect(getModelLikeIdSegments("openrouter/anthropic/claude-3.5-sonnet")).toEqual(["claude-3.5-sonnet"]);
|
||||
// `random-text` lacks a family prefix; `claude` (no digit) is dropped.
|
||||
expect(getModelLikeIdSegments("random-text claude gemini-2")).toEqual(["gemini-2"]);
|
||||
});
|
||||
|
||||
test("orders longest first with lexicographic tie-break", () => {
|
||||
expect(getModelLikeIdSegments("claude-3 claude-3-5-haiku claude-2")).toEqual([
|
||||
"claude-3-5-haiku",
|
||||
"claude-2",
|
||||
"claude-3",
|
||||
]);
|
||||
});
|
||||
|
||||
test("normalizes whitespace and case before matching", () => {
|
||||
expect(getModelLikeIdSegments(" GLM-4.5-Air GEMINI-2 ")).toEqual(["glm-4.5-air", "gemini-2"]);
|
||||
});
|
||||
|
||||
test("returns empty for ids with no model-like segment", () => {
|
||||
expect(getModelLikeIdSegments("")).toEqual([]);
|
||||
expect(getModelLikeIdSegments("just some words")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getLongestModelLikeIdSegment", () => {
|
||||
test("matches getModelLikeIdSegments[0]", () => {
|
||||
const id = "[Kiro] claude-3 claude-3-5-sonnet";
|
||||
expect(getLongestModelLikeIdSegment(id)).toBe(getModelLikeIdSegments(id)[0]);
|
||||
expect(getLongestModelLikeIdSegment(id)).toBe("claude-3-5-sonnet");
|
||||
});
|
||||
|
||||
test("is undefined when nothing matches", () => {
|
||||
expect(getLongestModelLikeIdSegment("vendor/unknown-tag")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBracketStrippedModelIdCandidates", () => {
|
||||
test("no brackets yields no candidates", () => {
|
||||
expect(getBracketStrippedModelIdCandidates("claude-opus-4-8")).toEqual([]);
|
||||
});
|
||||
|
||||
test("strips leading reseller tag", () => {
|
||||
expect(getBracketStrippedModelIdCandidates("[Kiro] claude-opus-4-8")).toEqual(["claude-opus-4-8"]);
|
||||
});
|
||||
|
||||
test("strips both ends first, then each side, in preference order", () => {
|
||||
expect(getBracketStrippedModelIdCandidates("[gcli转] gemini-3.1-pro-preview [假流]")).toEqual([
|
||||
"gemini-3.1-pro-preview",
|
||||
"gemini-3.1-pro-preview [假流]",
|
||||
"[gcli转] gemini-3.1-pro-preview",
|
||||
]);
|
||||
});
|
||||
|
||||
test("supports full-width brackets", () => {
|
||||
expect(stripBracketedModelIdAffixes("【供应商】 deepseek-v3 【限时】")).toBe("deepseek-v3");
|
||||
});
|
||||
});
|
||||
@@ -15,8 +15,10 @@
|
||||
* (messages.length shrinks) resets the cache.
|
||||
*/
|
||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||
import { countTokens } from "@oh-my-pi/pi-natives";
|
||||
import { resetSettingsForTest, Settings } from "../src/config/settings";
|
||||
import { StatusLineComponent } from "../src/modes/components/status-line";
|
||||
import { computeNonMessageTokens, estimateToolSchemaTokens } from "../src/modes/utils/context-usage";
|
||||
import { initTheme } from "../src/modes/theme/theme";
|
||||
import type { AgentSession } from "../src/session/agent-session";
|
||||
|
||||
@@ -122,6 +124,38 @@ describe("StatusLineComponent incremental context breakdown cache", () => {
|
||||
expect(v3.usedTokens).toBeGreaterThan(v2.usedTokens);
|
||||
});
|
||||
|
||||
it("non-message token shortcut matches previous category sum semantics", () => {
|
||||
const session = makeSession({
|
||||
messages: [],
|
||||
systemPrompt: [
|
||||
"You are an assistant.\n\n<skills>\n- code: Write code\n- review: Review code\n</skills>",
|
||||
"Loaded context file",
|
||||
"Runtime note",
|
||||
],
|
||||
tools: [
|
||||
{
|
||||
name: "bash",
|
||||
description: "Run shell commands",
|
||||
parameters: { type: "object", properties: { command: { type: "string" } } },
|
||||
},
|
||||
],
|
||||
skills: [
|
||||
{ name: "code", description: "Write code" },
|
||||
{ name: "review", description: "Review code" },
|
||||
],
|
||||
});
|
||||
|
||||
const skillsTokens = countTokens(["code", "Write code", "review", "Review code"]);
|
||||
const previousCategorySum =
|
||||
Math.max(0, countTokens(session.systemPrompt?.[0] ?? "") - skillsTokens) +
|
||||
countTokens((session.systemPrompt ?? []).slice(1)) +
|
||||
estimateToolSchemaTokens(session.agent?.state?.tools ?? []) +
|
||||
skillsTokens;
|
||||
|
||||
expect(new StatusLineComponent(session).getCachedContextBreakdown().usedTokens).toBe(previousCategorySum);
|
||||
expect(computeNonMessageTokens(session)).toBe(previousCategorySum);
|
||||
});
|
||||
|
||||
it("zero messages: produces only non-message tokens, no crash", () => {
|
||||
const session = makeSession({ messages: [] });
|
||||
const comp = new StatusLineComponent(session);
|
||||
|
||||
Reference in New Issue
Block a user