pi-multikey 1.11.1 → 1.12.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 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). Track the official client's InstallationVersion:
402
- // packages/opencode/src/session/llm/request.ts sends `opencode/${Version}`.
403
- const OPENCODE_ZEN_USER_AGENT = "opencode/1.18.31";
404
- const OPENCODE_ZEN_CLIENT = "tui";
405
-
406
- /** True when a baseUrl points at OpenCode Zen (case-insensitive, trailing slash ok). */
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
- return baseUrl.replace(/\/+$/, "").toLowerCase() === OPENCODE_ZEN_BASE_URL;
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/opencode/src/session/llm/request.ts (LLMRequestPrep.prepare)
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 flags.client (official default "cli"; we send "tui",
9
- * verified accepted live 2026-09-17)
10
- * User-Agent opencode/<InstallationVersion> (pinned in config.ts:
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.11.1",
3
+ "version": "1.12.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
@@ -103,7 +103,12 @@ export const PRESETS: Preset[] = [
103
103
  name: "OpenCode Zen",
104
104
  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
105
  defaultPoolId: "zen",
106
- baseUrl: "https://opencode.ai/zen/v1",
106
+ // Inference gateway, not zen/v1: the v2 client's console /api/v2/config
107
+ // prescribes this as the opencode provider baseURL, and the free lineup
108
+ // is served here against workspace quota. zen/v1 only has the anonymous
109
+ // per-IP quota, which datacenter egress IPs exhaust almost immediately
110
+ // (HTTP 429 FreeUsageLimitError on every request). Verified live 2026-09-21.
111
+ baseUrl: "https://opencode.ai/inference/openai/v1",
107
112
  api: "openai-completions",
108
113
  keyHint: "https://opencode.ai/auth → sign in → workspace Keys page (one entry per key; multiple keys share the load)",
109
114
  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" on network trouble.
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({ model: modelId, max_tokens: 4, messages: [{ role: "user", content: "ping" }] }),
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) {