@xynogen/pix-9router 0.4.3 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,213 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import type { ModelsDevModel, RouterModel } from "./data.ts";
3
- import {
4
- getContextWindow,
5
- getInputTypes,
6
- getMaxTokens,
7
- getModelName,
8
- getReasoning,
9
- } from "./provider.ts";
10
-
11
- // ── getInputTypes ────────────────────────────────────────────────────────────
12
-
13
- describe("getInputTypes", () => {
14
- it("uses devModel modalities when present", () => {
15
- const model: RouterModel = { id: "some-model" };
16
- const dev: ModelsDevModel = {
17
- id: "some-model",
18
- modalities: { input: ["text", "image"] },
19
- };
20
- expect(getInputTypes(model, dev)).toEqual(["text", "image"]);
21
- });
22
-
23
- it("filters out non text/image modalities from devModel", () => {
24
- const model: RouterModel = { id: "some-model" };
25
- const dev: ModelsDevModel = {
26
- id: "some-model",
27
- modalities: { input: ["text", "audio"] },
28
- };
29
- expect(getInputTypes(model, dev)).toEqual(["text"]);
30
- });
31
-
32
- it("falls back to pattern match for claude", () => {
33
- expect(getInputTypes({ id: "claude-sonnet-4-5" })).toEqual(["text", "image"]);
34
- });
35
-
36
- it("falls back to pattern match for gpt-4o", () => {
37
- expect(getInputTypes({ id: "gpt-4o" })).toEqual(["text", "image"]);
38
- });
39
-
40
- it("falls back to pattern match for kimi-k2", () => {
41
- expect(getInputTypes({ id: "kimi-k2-instruct" })).toEqual(["text", "image"]);
42
- });
43
-
44
- it("returns text-only for unknown model with no devModel", () => {
45
- expect(getInputTypes({ id: "some-random-llm" })).toEqual(["text"]);
46
- });
47
-
48
- it("uses devModel over pattern when both match", () => {
49
- const model: RouterModel = { id: "claude-opus-4" };
50
- const dev: ModelsDevModel = {
51
- id: "claude-opus-4",
52
- modalities: { input: ["text"] }, // explicitly text-only despite claude pattern
53
- };
54
- expect(getInputTypes(model, dev)).toEqual(["text"]);
55
- });
56
- });
57
-
58
- // ── getModelName ─────────────────────────────────────────────────────────────
59
-
60
- describe("getModelName", () => {
61
- it("prefers router model.name", () => {
62
- expect(getModelName({ id: "x", name: "Router Name" }, { id: "x", name: "Dev Name" })).toBe(
63
- "Router Name",
64
- );
65
- });
66
-
67
- it("falls back to devModel.name", () => {
68
- expect(getModelName({ id: "x" }, { id: "x", name: "Dev Name" })).toBe("Dev Name");
69
- });
70
-
71
- it("falls back to model.id", () => {
72
- expect(getModelName({ id: "some-id" })).toBe("some-id");
73
- });
74
-
75
- it("returns 'unknown' when all empty", () => {
76
- expect(getModelName({})).toBe("unknown");
77
- });
78
- });
79
-
80
- // ── getContextWindow ──────────────────────────────────────────────────────────
81
-
82
- describe("getContextWindow", () => {
83
- it("prefers model.context_window", () => {
84
- expect(
85
- getContextWindow(
86
- { id: "x", context_window: 32_000 },
87
- { id: "x", limit: { context: 100_000 } },
88
- ),
89
- ).toBe(32_000);
90
- });
91
-
92
- it("falls back to model.contextWindow", () => {
93
- expect(getContextWindow({ id: "x", contextWindow: 64_000 })).toBe(64_000);
94
- });
95
-
96
- it("falls back to capabilities.contextWindow (9router v1 nested shape)", () => {
97
- expect(getContextWindow({ id: "x", capabilities: { contextWindow: 1_000_000 } })).toBe(
98
- 1_000_000,
99
- );
100
- });
101
-
102
- it("top-level fields beat capabilities.contextWindow", () => {
103
- expect(
104
- getContextWindow({
105
- id: "x",
106
- contextWindow: 64_000,
107
- capabilities: { contextWindow: 1_000_000 },
108
- }),
109
- ).toBe(64_000);
110
- });
111
-
112
- it("capabilities.contextWindow beats devModel.limit.context", () => {
113
- expect(
114
- getContextWindow(
115
- { id: "x", capabilities: { contextWindow: 1_000_000 } },
116
- { id: "x", limit: { context: 200_000 } },
117
- ),
118
- ).toBe(1_000_000);
119
- });
120
-
121
- it("falls back to devModel.limit.context", () => {
122
- expect(getContextWindow({ id: "x" }, { id: "x", limit: { context: 200_000 } })).toBe(200_000);
123
- });
124
-
125
- it("falls back to DEFAULT_CONTEXT_WINDOW", () => {
126
- expect(getContextWindow({ id: "x" })).toBe(128_000);
127
- });
128
- });
129
-
130
- // ── getMaxTokens ──────────────────────────────────────────────────────────────
131
-
132
- describe("getMaxTokens", () => {
133
- it("prefers model.max_tokens", () => {
134
- expect(
135
- getMaxTokens({ id: "x", max_tokens: 4_096 }, { id: "x", limit: { output: 32_000 } }),
136
- ).toBe(4_096);
137
- });
138
-
139
- it("falls back to model.maxTokens", () => {
140
- expect(getMaxTokens({ id: "x", maxTokens: 8_192 })).toBe(8_192);
141
- });
142
-
143
- it("falls back to capabilities.maxOutput (9router v1 nested shape)", () => {
144
- expect(getMaxTokens({ id: "x", capabilities: { maxOutput: 64_000 } })).toBe(64_000);
145
- });
146
-
147
- it("top-level fields beat capabilities.maxOutput", () => {
148
- expect(getMaxTokens({ id: "x", maxTokens: 8_192, capabilities: { maxOutput: 64_000 } })).toBe(
149
- 8_192,
150
- );
151
- });
152
-
153
- it("capabilities.maxOutput beats devModel.limit.output", () => {
154
- expect(
155
- getMaxTokens(
156
- { id: "x", capabilities: { maxOutput: 64_000 } },
157
- { id: "x", limit: { output: 16_000 } },
158
- ),
159
- ).toBe(64_000);
160
- });
161
-
162
- it("falls back to devModel.limit.output", () => {
163
- expect(getMaxTokens({ id: "x" }, { id: "x", limit: { output: 16_000 } })).toBe(16_000);
164
- });
165
-
166
- it("falls back to DEFAULT_MAX_TOKENS", () => {
167
- expect(getMaxTokens({ id: "x" })).toBe(16_384);
168
- });
169
-
170
- it("handles the PR #9 regression case (qwen3-coder-flash nested capabilities)", () => {
171
- const model: RouterModel = {
172
- id: "alims-intl/qwen3-coder-flash-2025-07-28",
173
- capabilities: { contextWindow: 1_000_000, maxOutput: 64_000 },
174
- };
175
- expect(getContextWindow(model)).toBe(1_000_000);
176
- expect(getMaxTokens(model)).toBe(64_000);
177
- });
178
- });
179
-
180
- // ── getReasoning ──────────────────────────────────────────────────────────────
181
-
182
- describe("getReasoning", () => {
183
- it("uses devModel.reasoning when boolean true", () => {
184
- expect(getReasoning({ id: "x" }, { id: "x", reasoning: true })).toBe(true);
185
- });
186
-
187
- it("uses devModel.reasoning when boolean false", () => {
188
- // even if pattern would match, devModel wins
189
- expect(getReasoning({ id: "claude-opus-4" }, { id: "claude-opus-4", reasoning: false })).toBe(
190
- false,
191
- );
192
- });
193
-
194
- it("falls back to pattern — opus", () => {
195
- expect(getReasoning({ id: "claude-opus-4" })).toBe(true);
196
- });
197
-
198
- it("falls back to pattern — sonnet", () => {
199
- expect(getReasoning({ id: "claude-sonnet-4-5" })).toBe(true);
200
- });
201
-
202
- it("falls back to pattern — thinking", () => {
203
- expect(getReasoning({ id: "deepseek-thinking" })).toBe(true);
204
- });
205
-
206
- it("falls back to pattern — reasoner", () => {
207
- expect(getReasoning({ id: "o1-reasoner" })).toBe(true);
208
- });
209
-
210
- it("returns false for plain model with no devModel", () => {
211
- expect(getReasoning({ id: "llama-3-8b-instruct" })).toBe(false);
212
- });
213
- });
@@ -1,218 +0,0 @@
1
- import { expect, test } from "bun:test";
2
- import { COLLAPSED_TOOL_GLYPH } from "@xynogen/pix-pretty/utils";
3
- import { makeRenderCall, makeRenderResult } from "./render.js";
4
-
5
- const theme = {
6
- fg: (t: string, s: string) => `[${t}]${s}`,
7
- bold: (s: string) => `*${s}*`,
8
- } as never;
9
-
10
- function stubComponent() {
11
- let captured = "";
12
- const component = {
13
- setText: (s: string) => {
14
- captured = s;
15
- },
16
- render: (_width?: number) => captured.split("\n"),
17
- invalidate: () => {},
18
- };
19
- return { component, get: () => captured };
20
- }
21
-
22
- function context(stub: ReturnType<typeof stubComponent>, overrides: Record<string, unknown> = {}) {
23
- return {
24
- lastComponent: stub.component as never,
25
- isError: false,
26
- state: {},
27
- expanded: false,
28
- invalidate: () => {},
29
- ...overrides,
30
- } as never;
31
- }
32
-
33
- const config = {
34
- tool: "fetch",
35
- target: (_details: { _type: "fetchResult"; chars: number }) => "https://example.com",
36
- meta: (details: { _type: "fetchResult"; chars: number }) => `${details.chars} chars`,
37
- };
38
-
39
- test("renderCall shows title + dim arg", () => {
40
- const stub = stubComponent();
41
- const rc = makeRenderCall<{ url: string }>("fetch", (a) => a.url);
42
- rc({ url: "https://x.com" }, theme, context(stub));
43
- // Only the tool name is accent/toolTitle-colored; the arg is secondary.
44
- expect(stub.get()).toBe("[toolTitle]*fetch* [dim]https://x.com");
45
- });
46
-
47
- test("renderCall hides only an effectively collapsed call row", () => {
48
- const stub = stubComponent();
49
- const rc = makeRenderCall<{ url: string }>("fetch", (a) => a.url);
50
- rc({ url: "https://x.com" }, theme, context(stub, { state: { collapsed: true } }));
51
- expect(stub.get()).toBe("");
52
-
53
- rc(
54
- { url: "https://x.com" },
55
- theme,
56
- context(stub, { state: { collapsed: true }, expanded: true }),
57
- );
58
- expect(stub.get()).toBe("[toolTitle]*fetch* [dim]https://x.com");
59
- });
60
-
61
- test("renderResult renders structured compact details after collapse", () => {
62
- const stub = stubComponent();
63
- const rr = makeRenderResult(config);
64
- rr(
65
- {
66
- content: [{ type: "text", text: "full body" }],
67
- details: { _type: "fetchResult", chars: 9 },
68
- } as never,
69
- { expanded: false, isPartial: false },
70
- theme,
71
- context(stub, { state: { collapsed: true } }),
72
- );
73
- expect(stub.get()).toContain("[success]✓");
74
- expect(stub.get()).toContain("https://example.com");
75
- expect(stub.get()).toContain("9 chars");
76
- });
77
-
78
- test("partial updates keep detail visible and do not schedule collapse", () => {
79
- const stub = stubComponent();
80
- const state: Record<string, unknown> = {};
81
- const rr = makeRenderResult(config);
82
- rr(
83
- {
84
- content: [{ type: "text", text: "streaming body" }],
85
- details: { _type: "fetchResult", chars: 14 },
86
- } as never,
87
- { expanded: false, isPartial: true },
88
- theme,
89
- context(stub, { state }),
90
- );
91
- expect(stub.get()).toContain("streaming body");
92
- expect(stub.component.render(20)).toEqual([" [dim]streaming body"]);
93
- expect(state.timer).toBeUndefined();
94
- });
95
-
96
- test("expanded mode restores full joined text", () => {
97
- const stub = stubComponent();
98
- const rr = makeRenderResult(config);
99
- const component = rr(
100
- {
101
- content: [
102
- { type: "text", text: "first" },
103
- { type: "image", data: "ignored", mimeType: "image/png" },
104
- { type: "text", text: "second" },
105
- ],
106
- details: { _type: "fetchResult", chars: 12 },
107
- } as never,
108
- { expanded: true, isPartial: false },
109
- theme,
110
- context(stub, { state: { collapsed: true }, expanded: true }),
111
- );
112
- expect(stub.get()).toBe(" [dim]first\n [dim]second");
113
- expect(component.render(20)).toEqual([
114
- "[success]────────────────────",
115
- " [dim]first",
116
- " [dim]second",
117
- "[success]────────────────────",
118
- ]);
119
- });
120
-
121
- test("structured errors get red open frames but collapsed summaries stay unframed", () => {
122
- const rr = makeRenderResult({ ...config, status: () => "error" as const });
123
- const open = stubComponent();
124
- const openComponent = rr(
125
- {
126
- content: [{ type: "text", text: "failed body" }],
127
- details: { _type: "fetchResult", chars: 11 },
128
- } as never,
129
- { expanded: false, isPartial: false },
130
- theme,
131
- context(open),
132
- );
133
- expect(openComponent.render(20)[0]).toBe("[error]────────────────────");
134
-
135
- const collapsed = stubComponent();
136
- const collapsedComponent = rr(
137
- {
138
- content: [{ type: "text", text: "failed body" }],
139
- details: { _type: "fetchResult", chars: 11 },
140
- } as never,
141
- { expanded: false, isPartial: false },
142
- theme,
143
- context(collapsed, { state: { collapsed: true } }),
144
- );
145
- expect(collapsedComponent.render(20).join("\n")).not.toContain("────");
146
- });
147
-
148
- test("terminal error body gets an error frame", () => {
149
- const stub = stubComponent();
150
- const component = makeRenderResult(config)(
151
- { content: [{ type: "text", text: "boom" }] } as never,
152
- { expanded: true, isPartial: false },
153
- theme,
154
- context(stub, { isError: true, expanded: true }),
155
- );
156
- expect(component.render(20)).toEqual([
157
- "[error]────────────────────",
158
- " [error]boom",
159
- "[error]────────────────────",
160
- ]);
161
- });
162
-
163
- test("renderResult caps the normal preview", () => {
164
- const stub = stubComponent();
165
- const rr = makeRenderResult(config);
166
- const body = Array.from({ length: 37 }, (_, i) => `L${i}`).join("\n");
167
- rr(
168
- {
169
- content: [{ type: "text", text: body }],
170
- details: { _type: "fetchResult", chars: body.length },
171
- } as never,
172
- { expanded: false, isPartial: false },
173
- theme,
174
- context(stub, { state: { timer: 1 } }),
175
- );
176
- expect(stub.get()).toContain("… 5 more lines");
177
- });
178
-
179
- test("warning and error callbacks select shared status glyphs", () => {
180
- for (const [status, glyph] of [
181
- ["warning", COLLAPSED_TOOL_GLYPH.warning],
182
- ["error", COLLAPSED_TOOL_GLYPH.error],
183
- ] as const) {
184
- const stub = stubComponent();
185
- const rr = makeRenderResult({ ...config, status: () => status });
186
- rr(
187
- {
188
- content: [{ type: "text", text: "body" }],
189
- details: { _type: "fetchResult", chars: 4 },
190
- } as never,
191
- { expanded: false, isPartial: false },
192
- theme,
193
- context(stub, { state: { collapsed: true } }),
194
- );
195
- expect(stub.get()).toContain(`[${status}]${glyph}`);
196
- }
197
- });
198
-
199
- test("renderResult renders empty and errors without compacting", () => {
200
- const rr = makeRenderResult(config);
201
- const empty = stubComponent();
202
- rr(
203
- { content: [{ type: "text", text: " " }] } as never,
204
- { expanded: false, isPartial: false },
205
- theme,
206
- context(empty),
207
- );
208
- expect(empty.get()).toBe(" [muted](empty)");
209
-
210
- const failed = stubComponent();
211
- rr(
212
- { content: [{ type: "text", text: "boom" }] } as never,
213
- { expanded: false, isPartial: false },
214
- theme,
215
- context(failed, { isError: true, state: { collapsed: true } }),
216
- );
217
- expect(failed.get()).toBe(" [error]boom");
218
- });
@@ -1,60 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import registerFetch from "./fetch.ts";
3
- import registerSearch from "./search.ts";
4
-
5
- function captureParameters(register: (pi: never) => void): {
6
- properties: Record<string, { type?: string; enum?: string[]; description?: string }>;
7
- required?: string[];
8
- } {
9
- let parameters: unknown;
10
- register({
11
- registerTool(tool: { parameters: unknown }) {
12
- parameters = tool.parameters;
13
- },
14
- } as never);
15
- if (!parameters) throw new Error("tool parameters not captured");
16
- return parameters as {
17
- properties: Record<string, { type?: string; enum?: string[]; description?: string }>;
18
- required?: string[];
19
- };
20
- }
21
-
22
- describe("enum-like tool parameters", () => {
23
- test("search_type exposes values and their semantics", () => {
24
- const searchType = captureParameters(registerSearch).properties.search_type;
25
- if (!searchType) throw new Error("search_type schema not found");
26
-
27
- expect(searchType.type).toBe("string");
28
- expect(searchType.enum).toEqual(["web", "news"]);
29
- expect(searchType.description).toContain('exactly "web"');
30
- expect(searchType.description).toContain('"news"');
31
- });
32
-
33
- test("fetch format exposes values and their semantics", () => {
34
- const format = captureParameters(registerFetch).properties.format;
35
- if (!format) throw new Error("format schema not found");
36
-
37
- expect(format.type).toBe("string");
38
- expect(format.enum).toEqual(["markdown", "text", "html"]);
39
- expect(format.description).toContain('exactly "markdown"');
40
- expect(format.description).toContain('"html"');
41
- });
42
- });
43
-
44
- describe("fetch parameter requiredness", () => {
45
- test("only url is required; format is optional and defaults to markdown", () => {
46
- const params = captureParameters(registerFetch);
47
-
48
- // url must be required, format must NOT be — this is the exact validator
49
- // bug the optionality fix addresses (executeFetch already defaults format
50
- // to "markdown"), so guard against a regression back to a required format.
51
- expect(params.required).toEqual(["url"]);
52
- expect(params.required).not.toContain("format");
53
-
54
- // format stays a fully described enum in properties even while optional.
55
- const format = params.properties.format;
56
- if (!format) throw new Error("format schema not found");
57
- expect(format.enum).toEqual(["markdown", "text", "html"]);
58
- expect(format.description?.toLowerCase()).toContain("default");
59
- });
60
- });