@xynogen/pix-9router 0.3.6 → 0.3.7
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.
- package/package.json +1 -1
- package/src/data.ts +6 -0
- package/src/provider.test.ts +62 -45
- package/src/provider.ts +13 -6
package/package.json
CHANGED
package/src/data.ts
CHANGED
|
@@ -41,6 +41,12 @@ export interface RouterModel {
|
|
|
41
41
|
max_tokens?: number;
|
|
42
42
|
maxTokens?: number;
|
|
43
43
|
owned_by?: string;
|
|
44
|
+
/** 9router v1 API nests these inside capabilities */
|
|
45
|
+
capabilities?: {
|
|
46
|
+
contextWindow?: number;
|
|
47
|
+
maxOutput?: number;
|
|
48
|
+
[key: string]: unknown;
|
|
49
|
+
};
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
interface RouterModelsResponse {
|
package/src/provider.test.ts
CHANGED
|
@@ -1,48 +1,12 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import type { ModelsDevModel, RouterModel } from "./data.ts";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const IMAGE_CAPABLE_PATTERNS = [/claude/i, /gpt-5/i, /gpt-4/i, /kimi-k2/i, /hy3/i];
|
|
12
|
-
|
|
13
|
-
function getInputTypes(model: RouterModel, devModel?: ModelsDevModel): ("text" | "image")[] {
|
|
14
|
-
if (devModel?.modalities?.input) {
|
|
15
|
-
const inputs = devModel.modalities.input.filter(
|
|
16
|
-
(i): i is "text" | "image" => i === "text" || i === "image",
|
|
17
|
-
);
|
|
18
|
-
if (inputs.length > 0) return inputs;
|
|
19
|
-
}
|
|
20
|
-
const id = model.id ?? "";
|
|
21
|
-
if (IMAGE_CAPABLE_PATTERNS.some((p) => p.test(id))) return ["text", "image"];
|
|
22
|
-
return ["text"];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function getModelName(model: RouterModel, devModel?: ModelsDevModel): string {
|
|
26
|
-
return model.name || devModel?.name || model.id || "unknown";
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function getContextWindow(model: RouterModel, devModel?: ModelsDevModel): number {
|
|
30
|
-
return (
|
|
31
|
-
model.context_window ||
|
|
32
|
-
model.contextWindow ||
|
|
33
|
-
devModel?.limit?.context ||
|
|
34
|
-
DEFAULT_CONTEXT_WINDOW
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function getMaxTokens(model: RouterModel, devModel?: ModelsDevModel): number {
|
|
39
|
-
return model.max_tokens || model.maxTokens || devModel?.limit?.output || DEFAULT_MAX_TOKENS;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function getReasoning(model: RouterModel, devModel?: ModelsDevModel): boolean {
|
|
43
|
-
if (typeof devModel?.reasoning === "boolean") return devModel.reasoning;
|
|
44
|
-
return /reasoner|thinking|xhigh|high|max|pro|codex|opus|sonnet/i.test(model.id ?? "");
|
|
45
|
-
}
|
|
3
|
+
import {
|
|
4
|
+
getContextWindow,
|
|
5
|
+
getInputTypes,
|
|
6
|
+
getMaxTokens,
|
|
7
|
+
getModelName,
|
|
8
|
+
getReasoning,
|
|
9
|
+
} from "./provider.ts";
|
|
46
10
|
|
|
47
11
|
// ── getInputTypes ────────────────────────────────────────────────────────────
|
|
48
12
|
|
|
@@ -129,12 +93,37 @@ describe("getContextWindow", () => {
|
|
|
129
93
|
expect(getContextWindow({ id: "x", contextWindow: 64_000 })).toBe(64_000);
|
|
130
94
|
});
|
|
131
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
|
+
|
|
132
121
|
it("falls back to devModel.limit.context", () => {
|
|
133
122
|
expect(getContextWindow({ id: "x" }, { id: "x", limit: { context: 200_000 } })).toBe(200_000);
|
|
134
123
|
});
|
|
135
124
|
|
|
136
125
|
it("falls back to DEFAULT_CONTEXT_WINDOW", () => {
|
|
137
|
-
expect(getContextWindow({ id: "x" })).toBe(
|
|
126
|
+
expect(getContextWindow({ id: "x" })).toBe(128_000);
|
|
138
127
|
});
|
|
139
128
|
});
|
|
140
129
|
|
|
@@ -151,12 +140,40 @@ describe("getMaxTokens", () => {
|
|
|
151
140
|
expect(getMaxTokens({ id: "x", maxTokens: 8_192 })).toBe(8_192);
|
|
152
141
|
});
|
|
153
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
|
+
|
|
154
162
|
it("falls back to devModel.limit.output", () => {
|
|
155
163
|
expect(getMaxTokens({ id: "x" }, { id: "x", limit: { output: 16_000 } })).toBe(16_000);
|
|
156
164
|
});
|
|
157
165
|
|
|
158
166
|
it("falls back to DEFAULT_MAX_TOKENS", () => {
|
|
159
|
-
expect(getMaxTokens({ id: "x" })).toBe(
|
|
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);
|
|
160
177
|
});
|
|
161
178
|
});
|
|
162
179
|
|
package/src/provider.ts
CHANGED
|
@@ -26,7 +26,7 @@ const ZERO_COST = {
|
|
|
26
26
|
// Fallback pattern-based detection if models.dev lookup fails
|
|
27
27
|
const IMAGE_CAPABLE_PATTERNS = [/claude/i, /gpt-5/i, /gpt-4/i, /kimi-k2/i, /hy3/i];
|
|
28
28
|
|
|
29
|
-
function getInputTypes(model: RouterModel, devModel?: ModelsDevModel): ("text" | "image")[] {
|
|
29
|
+
export function getInputTypes(model: RouterModel, devModel?: ModelsDevModel): ("text" | "image")[] {
|
|
30
30
|
if (devModel?.modalities?.input) {
|
|
31
31
|
const inputs = devModel.modalities.input.filter(
|
|
32
32
|
(i): i is "text" | "image" => i === "text" || i === "image",
|
|
@@ -38,24 +38,31 @@ function getInputTypes(model: RouterModel, devModel?: ModelsDevModel): ("text" |
|
|
|
38
38
|
return ["text"];
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
function getModelName(model: RouterModel, devModel?: ModelsDevModel): string {
|
|
41
|
+
export function getModelName(model: RouterModel, devModel?: ModelsDevModel): string {
|
|
42
42
|
return model.name || devModel?.name || model.id || "unknown";
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
function getContextWindow(model: RouterModel, devModel?: ModelsDevModel): number {
|
|
45
|
+
export function getContextWindow(model: RouterModel, devModel?: ModelsDevModel): number {
|
|
46
46
|
return (
|
|
47
47
|
model.context_window ||
|
|
48
48
|
model.contextWindow ||
|
|
49
|
+
model.capabilities?.contextWindow ||
|
|
49
50
|
devModel?.limit?.context ||
|
|
50
51
|
DEFAULT_CONTEXT_WINDOW
|
|
51
52
|
);
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
function getMaxTokens(model: RouterModel, devModel?: ModelsDevModel): number {
|
|
55
|
-
return
|
|
55
|
+
export function getMaxTokens(model: RouterModel, devModel?: ModelsDevModel): number {
|
|
56
|
+
return (
|
|
57
|
+
model.max_tokens ||
|
|
58
|
+
model.maxTokens ||
|
|
59
|
+
model.capabilities?.maxOutput ||
|
|
60
|
+
devModel?.limit?.output ||
|
|
61
|
+
DEFAULT_MAX_TOKENS
|
|
62
|
+
);
|
|
56
63
|
}
|
|
57
64
|
|
|
58
|
-
function getReasoning(model: RouterModel, devModel?: ModelsDevModel): boolean {
|
|
65
|
+
export function getReasoning(model: RouterModel, devModel?: ModelsDevModel): boolean {
|
|
59
66
|
if (typeof devModel?.reasoning === "boolean") return devModel.reasoning;
|
|
60
67
|
return /reasoner|thinking|xhigh|high|max|pro|codex|opus|sonnet/i.test(model.id ?? "");
|
|
61
68
|
}
|