pi-multikey 1.4.0 → 1.5.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/README.md CHANGED
@@ -50,6 +50,8 @@ Built-in presets decouple "model settings" from "keys". The data comes from b.ai
50
50
 
51
51
  Endpoint `https://opencode.ai/zen/v1`; keys from [opencode.ai/auth](https://opencode.ai/auth) → workspace Keys. Context / max-output are the **Zen free-tier serving limits** (consistent across models.dev `opencode` provider + pi's built-in opencode catalog); the raw models are bigger — MiMo V2.5 = 1M ctx, Hy3 = 262K ctx. `muse-spark-1.2-contributor-free` uses the OpenAI **Responses** API endpoint; the other seven use chat completions.
52
52
 
53
+ Requests to this endpoint automatically carry the `User-Agent` header that opencode.ai expects (`opencode/1.15.0 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.13`) — appended whenever a pool's baseUrl is `https://opencode.ai/zen/v1`, on both probes and live requests.
54
+
53
55
  | Model | ctx / max-out | Modalities | Supported thinking levels |
54
56
  |---|---|---|---|
55
57
  | big-pickle | 200K / 32K | text | always-on (no thinkingLevelMap, like pi's catalog) |
package/README.zh.md CHANGED
@@ -56,6 +56,8 @@ DeepSeek / Tencent / 小米官方文档,并对每个 thinking 档位做过实
56
56
  原始模型更大——MiMo V2.5 = 1M ctx,Hy3 = 262K ctx。
57
57
  `muse-spark-1.2-contributor-free` 使用 OpenAI **Responses** API;其余七个使用 chat completions。
58
58
 
59
+ 对该端点的请求会自动携带 opencode.ai 期望的 `User-Agent` 头(`opencode/1.15.0 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.13`)——只要 pool 的 baseUrl 是 `https://opencode.ai/zen/v1`,探测和真实请求都会附加。
60
+
59
61
  | 模型 | ctx / max-out | 模态 | 生效 thinking 档位 |
60
62
  |---|---|---|---|
61
63
  | big-pickle | 200K / 32K | text | 始终思考(无 thinkingLevelMap,与 pi 内置目录一致) |
package/config.ts CHANGED
@@ -339,3 +339,24 @@ export function maskKey(key: string): string {
339
339
  if (key.length <= 10) return "…";
340
340
  return `${key.slice(0, 6)}…${key.slice(-4)}`;
341
341
  }
342
+
343
+ // ── Endpoint-required headers ────────────────────────────────────────────────
344
+
345
+ /** OpenCode Zen free tier endpoint that requires a specific User-Agent header. */
346
+ const OPENCODE_ZEN_BASE_URL = "https://opencode.ai/zen/v1";
347
+ const OPENCODE_ZEN_USER_AGENT =
348
+ "opencode/1.15.0 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.13";
349
+
350
+ /**
351
+ * Headers that must be sent to known endpoints.
352
+ *
353
+ * Currently: OpenCode Zen's free tier endpoint requires this exact User-Agent;
354
+ * anything else returns an empty object.
355
+ *
356
+ * The comparison is case-insensitive and tolerates a trailing slash.
357
+ */
358
+ export function endpointHeaders(baseUrl: string): Record<string, string> {
359
+ const normalized = baseUrl.replace(/\/+$/, "").toLowerCase();
360
+ if (normalized === OPENCODE_ZEN_BASE_URL) return { "User-Agent": OPENCODE_ZEN_USER_AGENT };
361
+ return {};
362
+ }
package/index.ts CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
13
13
  import { getApiProvider, type Api } from "@earendil-works/pi-ai";
14
- import { configPath, loadConfig, saveConfig, toProviderModels, type KeypoolConfig, type PoolConfig } from "./config.ts";
14
+ import { configPath, endpointHeaders, loadConfig, saveConfig, toProviderModels, type KeypoolConfig, type PoolConfig } from "./config.ts";
15
15
  import { KeyPool } from "./pool.ts";
16
16
  import { createRotatingStreamSimple } from "./stream.ts";
17
17
  import { runManager, type ManagerHooks } from "./manage.ts";
@@ -69,7 +69,7 @@ export default function multikey(pi: ExtensionAPI) {
69
69
  // Real keys are injected per-request by the rotating stream function.
70
70
  apiKey: "multikey-managed",
71
71
  api,
72
- headers: pool.headers,
72
+ headers: { ...pool.headers, ...endpointHeaders(pool.baseUrl) },
73
73
  models: toProviderModels(pool),
74
74
  streamSimple: createRotatingStreamSimple(keyPool, api, notify),
75
75
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-multikey",
3
- "version": "1.4.0",
3
+ "version": "1.5.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/probe.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { endpointHeaders } from "./config.ts";
2
+
1
3
  /**
2
4
  * Endpoint probing: auto-detect the auth header style and fetch the model list.
3
5
  *
@@ -57,11 +59,11 @@ function authHeaders(style: AuthStyle, key: string): Record<string, string> {
57
59
  return style === "bearer" ? { Authorization: `Bearer ${key}` } : { "x-api-key": key };
58
60
  }
59
61
 
60
- async function fetchJson(url: string, headers: Record<string, string>, timeoutMs: number): Promise<{ status: number; body?: unknown }> {
62
+ async function fetchJson(url: string, headers: Record<string, string>, timeoutMs: number, baseUrl?: string): Promise<{ status: number; body?: unknown }> {
61
63
  try {
62
64
  const response = await fetch(url, {
63
65
  method: "GET",
64
- headers: { Accept: "application/json", ...headers },
66
+ headers: { Accept: "application/json", ...endpointHeaders(baseUrl ?? url), ...headers },
65
67
  signal: AbortSignal.timeout(timeoutMs),
66
68
  });
67
69
  let body: unknown;
@@ -147,7 +149,7 @@ async function chatProbe(baseUrl: string, style: AuthStyle, key: string, modelId
147
149
  try {
148
150
  const response = await fetch(`${trimSlash(baseUrl)}/chat/completions`, {
149
151
  method: "POST",
150
- headers: { "Content-Type": "application/json", ...authHeaders(style, key) },
152
+ headers: { "Content-Type": "application/json", ...endpointHeaders(baseUrl), ...authHeaders(style, key) },
151
153
  body: JSON.stringify({ model: modelId, max_tokens: 4, messages: [{ role: "user", content: "ping" }] }),
152
154
  signal: AbortSignal.timeout(CHAT_TIMEOUT_MS),
153
155
  });
@@ -186,7 +188,7 @@ export async function probeEndpoint(
186
188
  let result: { status: number; body?: unknown };
187
189
  try {
188
190
  emit(`GET ${url} (${style === "bearer" ? "Authorization: Bearer" : "x-api-key"})…`);
189
- result = await fetchJson(url, authHeaders(style, key), MODELS_TIMEOUT_MS);
191
+ result = await fetchJson(url, authHeaders(style, key), MODELS_TIMEOUT_MS, baseUrl);
190
192
  } catch (error) {
191
193
  emit(` network error: ${error instanceof Error ? error.message : String(error)}`);
192
194
  continue;