pi-multikey 1.11.1 → 1.13.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/config.ts +24 -7
- package/identity.ts +5 -4
- package/package.json +1 -1
- package/presets.ts +39 -2
- package/probe.ts +33 -3
package/config.ts
CHANGED
|
@@ -396,17 +396,34 @@ export function maskKey(key: string): string {
|
|
|
396
396
|
|
|
397
397
|
/** OpenCode Zen free tier endpoint that mimics the official OpenCode client. */
|
|
398
398
|
const OPENCODE_ZEN_BASE_URL = "https://opencode.ai/zen/v1";
|
|
399
|
+
/**
|
|
400
|
+
* Where the v2 client actually sends Zen traffic: the console /api/v2/config
|
|
401
|
+
* prescribes this as the opencode provider's `settings.baseURL` (verified
|
|
402
|
+
* live 2026-09-21 — the client's session http.request hook shows POSTs to
|
|
403
|
+
* this host, never zen/v1). Same free lineup, but served against workspace
|
|
404
|
+
* quota instead of the anonymous per-IP quota that gates zen/v1.
|
|
405
|
+
*/
|
|
406
|
+
const OPENCODE_INFERENCE_BASE_URL = "https://opencode.ai/inference/openai/v1";
|
|
399
407
|
// The Zen free tier gates on the client version parsed from User-Agent
|
|
400
408
|
// (HTTP 426 "OpenCode 1.17.0 or newer is required" when too old — seen live
|
|
401
|
-
// 2026-09-17 with 0.1.50
|
|
402
|
-
//
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
409
|
+
// 2026-09-17 with 0.1.50; no such gate exists in the v2 branch console code,
|
|
410
|
+
// so this is production-server-side only). Track the official v2 client:
|
|
411
|
+
// packages/core/src/session/model-request.ts sends App.useragent(app), i.e.
|
|
412
|
+
// `opencode/<channel>/<version>/<name>`, with name defaulting to "cli"
|
|
413
|
+
// (OPENCODE_CLIENT ?? OPENCODE_ARTIFACT) — see packages/cli/src/version.ts
|
|
414
|
+
// and packages/cli/src/server-process.ts. Channel "latest" observed live on a
|
|
415
|
+
// working install (captured via session http.request hook: the client sends
|
|
416
|
+
// `opencode/latest/2.0.11/cli`). Bump alongside official releases.
|
|
417
|
+
const OPENCODE_ZEN_USER_AGENT = "opencode/latest/2.0.11/cli";
|
|
418
|
+
const OPENCODE_ZEN_CLIENT = "cli";
|
|
419
|
+
|
|
420
|
+
/** True when a baseUrl points at OpenCode Zen (either the legacy zen/v1 path
|
|
421
|
+
* or the inference gateway the v2 client actually uses; case-insensitive,
|
|
422
|
+
* trailing slash ok). */
|
|
407
423
|
export function isOpenCodeZenEndpoint(baseUrl: string | undefined): boolean {
|
|
408
424
|
if (!baseUrl) return false;
|
|
409
|
-
|
|
425
|
+
const normalized = baseUrl.replace(/\/+$/, "").toLowerCase();
|
|
426
|
+
return normalized === OPENCODE_ZEN_BASE_URL || normalized === OPENCODE_INFERENCE_BASE_URL;
|
|
410
427
|
}
|
|
411
428
|
|
|
412
429
|
/**
|
package/identity.ts
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
* Client identity headers for endpoints that gate on them.
|
|
3
3
|
*
|
|
4
4
|
* OpenCode Zen — opencode.ai/zen identifies the calling client with four headers,
|
|
5
|
-
* set in packages/
|
|
5
|
+
* set in packages/core/src/session/model-request.ts (SessionModelRequest.prepare)
|
|
6
6
|
* whenever the provider id starts with "opencode":
|
|
7
7
|
*
|
|
8
|
-
* x-opencode-client
|
|
9
|
-
*
|
|
10
|
-
* User-Agent opencode/<
|
|
8
|
+
* x-opencode-client app.name (official prod default "cli" via OPENCODE_CLIENT ??
|
|
9
|
+
* OPENCODE_ARTIFACT; we send "cli")
|
|
10
|
+
* User-Agent App.useragent(app), i.e. opencode/<channel>/<version>/<name>
|
|
11
|
+
* (pinned in config.ts, currently opencode/latest/2.0.11/cli:
|
|
11
12
|
* the free tier 426-rejects versions < 1.17.0)
|
|
12
13
|
* x-opencode-session input.sessionID ("ses_" + Identifier.create(descending))
|
|
13
14
|
* x-opencode-request input.user.id, the id of the user message being answered
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-multikey",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"description": "One pi provider backed by many API keys: automatic 429 rotation, per-request key leases for concurrent subagents, and a /multikey management TUI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
package/presets.ts
CHANGED
|
@@ -57,7 +57,7 @@ export const PRESETS: Preset[] = [
|
|
|
57
57
|
{
|
|
58
58
|
id: "b-ai",
|
|
59
59
|
name: "B.AI",
|
|
60
|
-
description: "api.b.ai — Hunyuan Hy3, MiMo V2.5, Qwen3.8
|
|
60
|
+
description: "api.b.ai — Hunyuan Hy3, MiMo V2.5, Qwen3.8, DeepSeek V4.1 Flash, GLM 5.3 Flash (5 models)",
|
|
61
61
|
defaultPoolId: "bai",
|
|
62
62
|
baseUrl: "https://api.b.ai/v1",
|
|
63
63
|
api: "openai-completions",
|
|
@@ -96,6 +96,38 @@ export const PRESETS: Preset[] = [
|
|
|
96
96
|
maxTokens: 131_072,
|
|
97
97
|
thinkingLevelMap: levels({ off: "none", low: "low", medium: "medium", xhigh: "xhigh" }),
|
|
98
98
|
},
|
|
99
|
+
{
|
|
100
|
+
// b.ai /v1/models lists `deepseek-v4.1-flash` (bare ids only, no
|
|
101
|
+
// limit fields — verified live 2026-09-21). Sizes from catalog
|
|
102
|
+
// consensus (models.dev opencode/greenpt rows + the same model
|
|
103
|
+
// behind cline-free/deepseek-v4.1-flash, verified live 2026-09-17):
|
|
104
|
+
// ctx 1M, out 384K. Text+image input (models.dev opencode row).
|
|
105
|
+
// Thinking tiers UNVERIFIED on b.ai: conservative off/high only —
|
|
106
|
+
// b.ai rejects minimal/xhigh/max with HTTP 400 (seen on mimo-v2.5).
|
|
107
|
+
// Widen the map after probing with a paid key.
|
|
108
|
+
id: "deepseek-v4.1-flash",
|
|
109
|
+
name: "DeepSeek V4.1 Flash",
|
|
110
|
+
reasoning: true,
|
|
111
|
+
input: ["text", "image"],
|
|
112
|
+
contextWindow: 1_000_000,
|
|
113
|
+
maxTokens: 384_000,
|
|
114
|
+
thinkingLevelMap: levels({ off: "none", high: "high" }),
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
// b.ai /v1/models lists `glm-5.3-flash` (bare ids only — verified
|
|
118
|
+
// live 2026-09-21). Sizes from catalog consensus (models.dev
|
|
119
|
+
// zhipuai/zai rows): ctx 1M, out 128K. Upstream inputs
|
|
120
|
+
// text/image/video/pdf (pi tracks text + image, like mimo-v2.5).
|
|
121
|
+
// Thinking tiers UNVERIFIED on b.ai: conservative off/high only
|
|
122
|
+
// (see deepseek-v4.1-flash above). Widen after probing.
|
|
123
|
+
id: "glm-5.3-flash",
|
|
124
|
+
name: "GLM 5.3 Flash",
|
|
125
|
+
reasoning: true,
|
|
126
|
+
input: ["text", "image"],
|
|
127
|
+
contextWindow: 1_000_000,
|
|
128
|
+
maxTokens: 131_072,
|
|
129
|
+
thinkingLevelMap: levels({ off: "none", high: "high" }),
|
|
130
|
+
},
|
|
99
131
|
],
|
|
100
132
|
},
|
|
101
133
|
{
|
|
@@ -103,7 +135,12 @@ export const PRESETS: Preset[] = [
|
|
|
103
135
|
name: "OpenCode Zen",
|
|
104
136
|
description: "opencode.ai/zen free tier — Big Pickle, MiMo V2.5, Ling 3.0 Fin, Nemotron 3 Ultra/Lightning, Muse Spark 1.3 (6 free models)",
|
|
105
137
|
defaultPoolId: "zen",
|
|
106
|
-
|
|
138
|
+
// Inference gateway, not zen/v1: the v2 client's console /api/v2/config
|
|
139
|
+
// prescribes this as the opencode provider baseURL, and the free lineup
|
|
140
|
+
// is served here against workspace quota. zen/v1 only has the anonymous
|
|
141
|
+
// per-IP quota, which datacenter egress IPs exhaust almost immediately
|
|
142
|
+
// (HTTP 429 FreeUsageLimitError on every request). Verified live 2026-09-21.
|
|
143
|
+
baseUrl: "https://opencode.ai/inference/openai/v1",
|
|
107
144
|
api: "openai-completions",
|
|
108
145
|
keyHint: "https://opencode.ai/auth → sign in → workspace Keys page (one entry per key; multiple keys share the load)",
|
|
109
146
|
models: [
|
package/probe.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { endpointHeaders, endpointIdentityHeaders } from "./config.ts";
|
|
1
|
+
import { endpointHeaders, endpointIdentityHeaders, isOpenCodeZenEndpoint } from "./config.ts";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Endpoint probing: auto-detect the auth header style and fetch the model list.
|
|
@@ -140,14 +140,39 @@ export function parseModelsResponse(body: unknown): RemoteModel[] {
|
|
|
140
140
|
return models;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Minimal tool stubs that satisfy OpenCode Zen's free-tier client check.
|
|
145
|
+
*
|
|
146
|
+
* Verified live 2026-09-21 (see identity.ts notes): the free tier answers
|
|
147
|
+
* 403 FreeTierError ("can only be used from within OpenCode") to bare,
|
|
148
|
+
* non-streaming pings — even with a good key — but accepts the same request
|
|
149
|
+
* once it looks agentic: `stream: true` plus tool definitions carrying
|
|
150
|
+
* opencode's tool names. Schemas are irrelevant (empty is fine); a single
|
|
151
|
+
* tool is not. These stubs are never executed — the probe only reads status.
|
|
152
|
+
*/
|
|
153
|
+
const ZEN_PROBE_TOOLS = ["read", "shell", "edit", "write"].map((name) => ({
|
|
154
|
+
type: "function" as const,
|
|
155
|
+
function: {
|
|
156
|
+
name,
|
|
157
|
+
description: name,
|
|
158
|
+
parameters: { type: "object" as const, properties: {} },
|
|
159
|
+
},
|
|
160
|
+
}));
|
|
161
|
+
|
|
143
162
|
/**
|
|
144
163
|
* Verify a key with a minimal chat completion (a few tokens at most). Returns
|
|
145
164
|
* "ok" when auth was accepted (2xx, or 4xx that clearly got past auth like a
|
|
146
|
-
* bad-model/params 400/404), "rejected" on 401/403, "error"
|
|
165
|
+
* bad-model/params 400/404 or a quota 429), "rejected" on 401/403, "error"
|
|
166
|
+
* on network trouble.
|
|
147
167
|
* `onLog` receives the server's rejection body so the TUI can show why.
|
|
168
|
+
*
|
|
169
|
+
* On OpenCode Zen the ping carries the agentic shape (streaming + stub tools,
|
|
170
|
+
* see ZEN_PROBE_TOOLS): without it the free tier 403-rejects even good keys,
|
|
171
|
+
* which would misreport a working key as rejected.
|
|
148
172
|
*/
|
|
149
173
|
async function chatProbe(baseUrl: string, style: AuthStyle, key: string, modelId: string, onLog?: (line: string) => void): Promise<"ok" | "rejected" | "error"> {
|
|
150
174
|
try {
|
|
175
|
+
const zenShaped = isOpenCodeZenEndpoint(baseUrl);
|
|
151
176
|
const response = await fetch(`${trimSlash(baseUrl)}/chat/completions`, {
|
|
152
177
|
method: "POST",
|
|
153
178
|
headers: {
|
|
@@ -156,7 +181,12 @@ async function chatProbe(baseUrl: string, style: AuthStyle, key: string, modelId
|
|
|
156
181
|
...endpointIdentityHeaders(baseUrl),
|
|
157
182
|
...authHeaders(style, key),
|
|
158
183
|
},
|
|
159
|
-
body: JSON.stringify({
|
|
184
|
+
body: JSON.stringify({
|
|
185
|
+
model: modelId,
|
|
186
|
+
max_tokens: 4,
|
|
187
|
+
messages: [{ role: "user", content: "ping" }],
|
|
188
|
+
...(zenShaped ? { tools: ZEN_PROBE_TOOLS, stream: true } : {}),
|
|
189
|
+
}),
|
|
160
190
|
signal: AbortSignal.timeout(CHAT_TIMEOUT_MS),
|
|
161
191
|
});
|
|
162
192
|
if (response.status === 401 || response.status === 403) {
|