@xynogen/pix-9router 0.3.21 → 0.4.0
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/provider.ts +70 -27
- package/src/render.test.ts +5 -6
- package/src/render.ts +4 -4
package/package.json
CHANGED
package/src/provider.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* ROUTER_API_KEY — bearer token (required for live model list)
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import type { RefreshModelsContext } from "@earendil-works/pi-ai";
|
|
12
13
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
14
|
import type { ModelsDevModel, RouterModel } from "./data.js";
|
|
14
15
|
import { fetchModelsDevIndex, lookupInIndex, routerBaseUrl, routerModels } from "./data.js";
|
|
@@ -26,6 +27,33 @@ const ZERO_COST = {
|
|
|
26
27
|
// Fallback pattern-based detection if models.dev lookup fails
|
|
27
28
|
const IMAGE_CAPABLE_PATTERNS = [/claude/i, /gpt-5/i, /gpt-4/i, /kimi-k2/i, /hy3/i];
|
|
28
29
|
|
|
30
|
+
interface RouterModelsResponse {
|
|
31
|
+
data?: RouterModel[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const COMPAT = {
|
|
35
|
+
supportsDeveloperRole: false,
|
|
36
|
+
supportsUsageInStreaming: false,
|
|
37
|
+
maxTokensField: "max_tokens",
|
|
38
|
+
} as const;
|
|
39
|
+
|
|
40
|
+
function toModelConfig(devIndex: Map<string, ModelsDevModel>) {
|
|
41
|
+
return (model: RouterModel) => {
|
|
42
|
+
const id = model.id ?? "";
|
|
43
|
+
const devModel = lookupInIndex(id, devIndex);
|
|
44
|
+
return {
|
|
45
|
+
id,
|
|
46
|
+
name: getModelName(model, devModel),
|
|
47
|
+
reasoning: getReasoning(model, devModel),
|
|
48
|
+
input: getInputTypes(model, devModel),
|
|
49
|
+
cost: ZERO_COST,
|
|
50
|
+
contextWindow: getContextWindow(model, devModel),
|
|
51
|
+
maxTokens: getMaxTokens(model, devModel),
|
|
52
|
+
compat: COMPAT,
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
29
57
|
export function getInputTypes(model: RouterModel, devModel?: ModelsDevModel): ("text" | "image")[] {
|
|
30
58
|
if (devModel?.modalities?.input) {
|
|
31
59
|
const inputs = devModel.modalities.input.filter(
|
|
@@ -82,39 +110,54 @@ export default async function registerProvider(pi: ExtensionAPI): Promise<void>
|
|
|
82
110
|
return;
|
|
83
111
|
}
|
|
84
112
|
|
|
85
|
-
|
|
113
|
+
// Upstream moved OpenAI `compat` settings from the provider level to the
|
|
114
|
+
// per-model level (ProviderModelConfig.compat). Applied in toModelConfig.
|
|
115
|
+
|
|
116
|
+
// Register from disk cache only — startup must not block on network.
|
|
117
|
+
const cached = routerModels.getCached();
|
|
118
|
+
pi.registerProvider("9router", providerConfig(apiKey, cached, new Map()));
|
|
119
|
+
|
|
120
|
+
// Background refresh; re-registering after startup applies immediately.
|
|
121
|
+
void Promise.all([
|
|
86
122
|
routerModels.get(),
|
|
87
123
|
fetchModelsDevIndex().catch(() => new Map<string, ModelsDevModel>()),
|
|
88
|
-
])
|
|
124
|
+
])
|
|
125
|
+
.then(([models, devIndex]) =>
|
|
126
|
+
pi.registerProvider("9router", providerConfig(apiKey, models, devIndex)),
|
|
127
|
+
)
|
|
128
|
+
.catch(() => {});
|
|
129
|
+
}
|
|
89
130
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
maxTokensField: "max_tokens",
|
|
97
|
-
} as const;
|
|
98
|
-
|
|
99
|
-
pi.registerProvider("9router", {
|
|
131
|
+
function providerConfig(
|
|
132
|
+
apiKey: string,
|
|
133
|
+
models: RouterModel[],
|
|
134
|
+
devIndex: Map<string, ModelsDevModel>,
|
|
135
|
+
) {
|
|
136
|
+
return {
|
|
100
137
|
name: "9Router",
|
|
101
138
|
baseUrl: routerBaseUrl(),
|
|
102
139
|
apiKey: "$ROUTER_API_KEY",
|
|
103
140
|
api: "openai-completions",
|
|
104
141
|
headers: { "User-Agent": "pi-coding-agent" },
|
|
105
|
-
models: models.map((
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
142
|
+
models: models.map(toModelConfig(devIndex)),
|
|
143
|
+
// Live fetch on /model refresh — bypasses the disk cache.
|
|
144
|
+
// Pi calls refreshModels once per provider at every startup with
|
|
145
|
+
// allowNetwork:false (awaited, no timeout) — never touch the network
|
|
146
|
+
// there; serve the registered snapshot instead.
|
|
147
|
+
async refreshModels({ signal, allowNetwork }: RefreshModelsContext) {
|
|
148
|
+
if (!allowNetwork) return models.map(toModelConfig(devIndex));
|
|
149
|
+
const res = await fetch(`${routerBaseUrl()}/models`, {
|
|
150
|
+
signal: AbortSignal.any([AbortSignal.timeout(8_000), ...(signal ? [signal] : [])]),
|
|
151
|
+
headers: {
|
|
152
|
+
Authorization: `Bearer ${apiKey}`,
|
|
153
|
+
"User-Agent": "pi-coding-agent",
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
if (!res.ok) throw new Error(`9router /models: ${res.status}`);
|
|
157
|
+
const raw = (await res.json()) as RouterModelsResponse;
|
|
158
|
+
const list = (raw.data ?? []).filter((m) => Boolean(m.id));
|
|
159
|
+
const fresh = await fetchModelsDevIndex().catch(() => new Map<string, ModelsDevModel>());
|
|
160
|
+
return list.map(toModelConfig(fresh));
|
|
161
|
+
},
|
|
162
|
+
};
|
|
120
163
|
}
|
package/src/render.test.ts
CHANGED
|
@@ -36,13 +36,12 @@ const config = {
|
|
|
36
36
|
meta: (details: { _type: "fetchResult"; chars: number }) => `${details.chars} chars`,
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
test("renderCall shows title +
|
|
39
|
+
test("renderCall shows title + dim arg", () => {
|
|
40
40
|
const stub = stubComponent();
|
|
41
41
|
const rc = makeRenderCall<{ url: string }>("fetch", (a) => a.url);
|
|
42
42
|
rc({ url: "https://x.com" }, theme, context(stub));
|
|
43
|
-
// Only the tool name is accent/toolTitle-colored; the arg is
|
|
44
|
-
|
|
45
|
-
expect(stub.get()).toBe("[toolTitle]*fetch* [muted]https://x.com");
|
|
43
|
+
// Only the tool name is accent/toolTitle-colored; the arg is secondary.
|
|
44
|
+
expect(stub.get()).toBe("[toolTitle]*fetch* [dim]https://x.com");
|
|
46
45
|
});
|
|
47
46
|
|
|
48
47
|
test("renderCall hides only an effectively collapsed call row", () => {
|
|
@@ -56,7 +55,7 @@ test("renderCall hides only an effectively collapsed call row", () => {
|
|
|
56
55
|
theme,
|
|
57
56
|
context(stub, { state: { collapsed: true }, expanded: true }),
|
|
58
57
|
);
|
|
59
|
-
expect(stub.get()).toBe("[toolTitle]*fetch* [
|
|
58
|
+
expect(stub.get()).toBe("[toolTitle]*fetch* [dim]https://x.com");
|
|
60
59
|
});
|
|
61
60
|
|
|
62
61
|
test("renderResult renders structured compact details after collapse", () => {
|
|
@@ -157,7 +156,7 @@ test("renderResult renders empty and errors without compacting", () => {
|
|
|
157
156
|
theme,
|
|
158
157
|
context(empty),
|
|
159
158
|
);
|
|
160
|
-
expect(empty.get()).toBe(" [
|
|
159
|
+
expect(empty.get()).toBe(" [muted](empty)");
|
|
161
160
|
|
|
162
161
|
const failed = stubComponent();
|
|
163
162
|
rr(
|
package/src/render.ts
CHANGED
|
@@ -81,12 +81,12 @@ function dimBody(body: string, theme: Theme, expanded: boolean): string {
|
|
|
81
81
|
const out = shown.map((line) => ` ${theme.fg("dim", line)}`);
|
|
82
82
|
const remaining = lines.length - maxShow;
|
|
83
83
|
if (remaining > 0) {
|
|
84
|
-
out.push(` ${theme.fg("
|
|
84
|
+
out.push(` ${theme.fg("muted", `… ${remaining} more lines`)}`);
|
|
85
85
|
}
|
|
86
86
|
return out.join("\n");
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
/** Build a `renderCall` that shows `<title> <
|
|
89
|
+
/** Build a `renderCall` that shows `<title> <secondary arg>`. */
|
|
90
90
|
export function makeRenderCall<TArgs>(title: string, pickArg: (args: TArgs) => string) {
|
|
91
91
|
return (args: TArgs, theme: Theme, ctx: RenderCtx): Component => {
|
|
92
92
|
const text = getText(ctx.lastComponent);
|
|
@@ -97,7 +97,7 @@ export function makeRenderCall<TArgs>(title: string, pickArg: (args: TArgs) => s
|
|
|
97
97
|
)
|
|
98
98
|
return text;
|
|
99
99
|
const arg = pickArg(args);
|
|
100
|
-
text.setText(`${theme.fg("toolTitle", theme.bold(title))} ${theme.fg("
|
|
100
|
+
text.setText(`${theme.fg("toolTitle", theme.bold(title))} ${theme.fg("dim", arg)}`);
|
|
101
101
|
return text;
|
|
102
102
|
};
|
|
103
103
|
}
|
|
@@ -118,7 +118,7 @@ export function makeRenderResult<TDetails>(config?: CompactRendererConfig<TDetai
|
|
|
118
118
|
return text;
|
|
119
119
|
}
|
|
120
120
|
if (!body.trim()) {
|
|
121
|
-
text.setText(` ${theme.fg("
|
|
121
|
+
text.setText(` ${theme.fg("muted", "(empty)")}`);
|
|
122
122
|
return text;
|
|
123
123
|
}
|
|
124
124
|
|