pi-multikey 1.12.0 → 1.14.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 +2 -2
- package/index.ts +61 -21
- package/package.json +1 -1
- package/presets.ts +63 -2
- package/stream.ts +11 -2
package/config.ts
CHANGED
|
@@ -360,8 +360,8 @@ function normalizePool(pool: PoolConfig): PoolConfig {
|
|
|
360
360
|
}
|
|
361
361
|
|
|
362
362
|
/** Convert a pool config into pi ProviderModelConfig-style definitions (provider-level compat merged in). */
|
|
363
|
-
export function toProviderModels(pool: PoolConfig): ProviderModelConfig[] {
|
|
364
|
-
return pool.models.map(
|
|
363
|
+
export function toProviderModels(pool: PoolConfig, models?: PoolModelConfig[]): ProviderModelConfig[] {
|
|
364
|
+
return (models ?? pool.models).map(
|
|
365
365
|
(m): ProviderModelConfig => ({
|
|
366
366
|
id: m.id,
|
|
367
367
|
name: m.name ?? m.id,
|
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, endpointHeaders, loadConfig, saveConfig, toProviderModels, type KeypoolConfig, type PoolConfig } from "./config.ts";
|
|
14
|
+
import { configPath, endpointHeaders, loadConfig, saveConfig, toProviderModels, type KeypoolConfig, type PoolConfig, type PoolModelConfig } from "./config.ts";
|
|
15
15
|
import { resetConversation } from "./identity.ts";
|
|
16
16
|
import { KeyPool } from "./pool.ts";
|
|
17
17
|
import { createRotatingStreamSimple } from "./stream.ts";
|
|
@@ -36,6 +36,8 @@ export default function multikey(pi: ExtensionAPI) {
|
|
|
36
36
|
|
|
37
37
|
const pools = new Map<string, KeyPool>();
|
|
38
38
|
for (const pool of config.pools) pools.set(pool.id, new KeyPool(pool));
|
|
39
|
+
/** pi provider ids registered per pool (pool.id plus one suffixed id per extra transport). */
|
|
40
|
+
const providerIds = new Map<string, string[]>();
|
|
39
41
|
|
|
40
42
|
let ui: ExtensionContext["ui"] | undefined;
|
|
41
43
|
const notify = (message: string) => {
|
|
@@ -47,33 +49,68 @@ export default function multikey(pi: ExtensionAPI) {
|
|
|
47
49
|
};
|
|
48
50
|
|
|
49
51
|
/**
|
|
50
|
-
* Register a pool as
|
|
52
|
+
* Register a pool as pi provider(s). Returns undefined on success, or a
|
|
51
53
|
* human-readable reason why the pool was skipped (unknown api, incomplete).
|
|
54
|
+
*
|
|
55
|
+
* One pi provider per transport: pi dispatches to a provider's custom
|
|
56
|
+
* streamSimple only when model.api matches the registered api
|
|
57
|
+
* (provider-composer.js), so models on another transport would bypass key
|
|
58
|
+
* rotation and per-request identity headers entirely — on OpenCode Zen
|
|
59
|
+
* that draws 403 FreeTierError (verified live 2026-09-21: muse-spark went
|
|
60
|
+
* out with the dummy key and a uuid session). Extra transports register
|
|
61
|
+
* under "<pool.id>.<api>" sharing the same KeyPool, so keys are entered
|
|
62
|
+
* once and rotation/cooldowns span both providers.
|
|
52
63
|
*/
|
|
53
64
|
function registerPool(pool: PoolConfig): string | undefined {
|
|
54
65
|
if (pool.keys.length === 0 || pool.models.length === 0) {
|
|
55
66
|
return pool.keys.length === 0 ? "no API keys" : "no models";
|
|
56
67
|
}
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
68
|
+
const defaultApi = pool.api ?? "openai-completions";
|
|
69
|
+
const groups = new Map<string, PoolModelConfig[]>();
|
|
70
|
+
for (const m of pool.models) {
|
|
71
|
+
const api = m.api ?? defaultApi;
|
|
72
|
+
const list = groups.get(api);
|
|
73
|
+
if (list) list.push(m);
|
|
74
|
+
else groups.set(api, [m]);
|
|
75
|
+
}
|
|
76
|
+
for (const api of groups.keys()) {
|
|
77
|
+
if (!getApiProvider(api as Api)) {
|
|
78
|
+
// Never throw at startup over a bad api value; report it instead so
|
|
79
|
+
// the user gets a fix hint (and /multikey marks the pool broken).
|
|
80
|
+
return `unknown api type "${api}" (edit the pool and pick a valid API type)`;
|
|
81
|
+
}
|
|
62
82
|
}
|
|
63
83
|
const keyPool = pools.get(pool.id) ?? new KeyPool(pool);
|
|
64
84
|
keyPool.updateConfig(pool);
|
|
65
85
|
pools.set(pool.id, keyPool);
|
|
66
86
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
87
|
+
const wanted: string[] = [];
|
|
88
|
+
for (const [api, models] of groups) {
|
|
89
|
+
const providerId = api === defaultApi ? pool.id : `${pool.id}.${api}`;
|
|
90
|
+
wanted.push(providerId);
|
|
91
|
+
pi.registerProvider(providerId, {
|
|
92
|
+
name: api === defaultApi ? (pool.name ?? pool.id) : `${pool.name ?? pool.id} (${api})`,
|
|
93
|
+
baseUrl: pool.baseUrl,
|
|
94
|
+
// Real keys are injected per-request by the rotating stream function.
|
|
95
|
+
apiKey: "multikey-managed",
|
|
96
|
+
api,
|
|
97
|
+
headers: { ...pool.headers, ...endpointHeaders(pool.baseUrl) },
|
|
98
|
+
models: toProviderModels(pool, models),
|
|
99
|
+
streamSimple: createRotatingStreamSimple(keyPool, api, notify, () => saveConfig(config)),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
// Drop split providers left over from a previous registration whose
|
|
103
|
+
// models have since moved transports (or back to a single provider).
|
|
104
|
+
for (const stale of providerIds.get(pool.id) ?? []) {
|
|
105
|
+
if (!wanted.includes(stale)) {
|
|
106
|
+
try {
|
|
107
|
+
pi.unregisterProvider(stale);
|
|
108
|
+
} catch {
|
|
109
|
+
// Already gone.
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
providerIds.set(pool.id, wanted);
|
|
77
114
|
return undefined;
|
|
78
115
|
}
|
|
79
116
|
|
|
@@ -98,11 +135,14 @@ export default function multikey(pi: ExtensionAPI) {
|
|
|
98
135
|
config.pools = config.pools.filter((p) => p.id !== poolId);
|
|
99
136
|
pools.delete(poolId);
|
|
100
137
|
saveConfig(config);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
138
|
+
for (const providerId of [poolId, ...(providerIds.get(poolId) ?? [])]) {
|
|
139
|
+
try {
|
|
140
|
+
pi.unregisterProvider(providerId);
|
|
141
|
+
} catch {
|
|
142
|
+
// Not registered yet.
|
|
143
|
+
}
|
|
105
144
|
}
|
|
145
|
+
providerIds.delete(poolId);
|
|
106
146
|
}
|
|
107
147
|
|
|
108
148
|
function reloadFromDisk() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-multikey",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.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,12 +96,44 @@ 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
|
{
|
|
102
134
|
id: "opencode-zen",
|
|
103
135
|
name: "OpenCode Zen",
|
|
104
|
-
description: "opencode.ai/zen free tier — Big Pickle, MiMo V2.5, Ling 3.0 Fin, Nemotron 3 Ultra/Lightning, Muse Spark 1.3 (
|
|
136
|
+
description: "opencode.ai/zen free tier — Big Pickle, MiMo V2.6 Flash, MiMo V2.5, Ling 3.0 Fin, Nemotron 3 Ultra/Lightning, Muse Spark 1.3 (7 free models)",
|
|
105
137
|
defaultPoolId: "zen",
|
|
106
138
|
// Inference gateway, not zen/v1: the v2 client's console /api/v2/config
|
|
107
139
|
// prescribes this as the opencode provider baseURL, and the free lineup
|
|
@@ -123,10 +155,39 @@ export const PRESETS: Preset[] = [
|
|
|
123
155
|
maxTokens: 32_000,
|
|
124
156
|
compat: ZEN_CHAT_COMPAT,
|
|
125
157
|
},
|
|
158
|
+
{
|
|
159
|
+
// Xiaomi MiMo-V2.6-Flash, released to the Zen free tier 2026-09-22 and
|
|
160
|
+
// the successor of mimo-v2.5-free (docs opencode.ai/docs/zen lists both,
|
|
161
|
+
// in that order). Limits are authoritative from console /api/v2/config
|
|
162
|
+
// `providers.opencode.models` and mirrored exactly by models.dev:
|
|
163
|
+
// ctx 200,000 / out 32,000, tools on, cost 0. Upstream inputs
|
|
164
|
+
// text/image/audio/video/pdf (pi tracks text + image, like mimo-v2.5).
|
|
165
|
+
// Reasoning is always-on with no effort control: models.dev reports
|
|
166
|
+
// `reasoning: true`, `reasoning_options: []`, `interleaved.field =
|
|
167
|
+
// reasoning_content`; verified live 2026-09-22 that reasoning_content
|
|
168
|
+
// streams separately and usage reports reasoning_tokens — so, like the
|
|
169
|
+
// rest of the free lineup, no thinkingLevelMap.
|
|
170
|
+
// Verified live 2026-09-22 (agentic probe shape, see probe.ts): streaming
|
|
171
|
+
// chat with the read/shell/edit/write quartet → 200, content "PONG",
|
|
172
|
+
// finish stop, usage {prompt 191, completion 13, reasoning 9}; the same
|
|
173
|
+
// request without tools/stream → 403 FreeTierError, confirming the gate
|
|
174
|
+
// still applies to this model id.
|
|
175
|
+
id: "mimo-v2.6-flash-free",
|
|
176
|
+
name: "MiMo V2.6 Flash Free",
|
|
177
|
+
reasoning: true,
|
|
178
|
+
input: ["text", "image"],
|
|
179
|
+
contextWindow: 200_000,
|
|
180
|
+
maxTokens: 127_000,
|
|
181
|
+
compat: ZEN_CHAT_COMPAT,
|
|
182
|
+
},
|
|
126
183
|
{
|
|
127
184
|
// Xiaomi MiMo V2.5 omni; raw model is 1M ctx but the Zen FREE tier serves 200K/32K.
|
|
128
185
|
// Repo metadata: inputs text/image/audio/video (pi tracks text + image),
|
|
129
186
|
// reasoning via separate reasoning_content stream, no reasoning_options.
|
|
187
|
+
// Still listed in opencode.ai/docs/zen's free lineup and still answers 200
|
|
188
|
+
// (verified live 2026-09-22), but it disappeared from the console
|
|
189
|
+
// /api/v2/config model map that day when mimo-v2.6-flash-free landed —
|
|
190
|
+
// kept because it demonstrably works; drop it when it 404s.
|
|
130
191
|
id: "mimo-v2.5-free",
|
|
131
192
|
name: "MiMo V2.5 Free",
|
|
132
193
|
reasoning: true,
|
package/stream.ts
CHANGED
|
@@ -54,8 +54,8 @@ interface CapturedResponse {
|
|
|
54
54
|
export type Notifier = (message: string) => void;
|
|
55
55
|
|
|
56
56
|
export function createRotatingStreamSimple(pool: KeyPool, apiName: string, notify: Notifier, onConfigDirty?: () => void) {
|
|
57
|
-
const
|
|
58
|
-
if (!
|
|
57
|
+
const fallback = getApiProvider(apiName as Api);
|
|
58
|
+
if (!fallback) throw new Error(`multikey: no API provider registered for api: ${apiName}`);
|
|
59
59
|
// Auth style: "api-key" providers want the key in x-api-key (some reject
|
|
60
60
|
// Authorization entirely); bearer is the pi-ai default and needs no help.
|
|
61
61
|
const authStyle = pool.config.auth ?? "bearer";
|
|
@@ -108,6 +108,15 @@ export function createRotatingStreamSimple(pool: KeyPool, apiName: string, notif
|
|
|
108
108
|
|
|
109
109
|
try {
|
|
110
110
|
const captured: CapturedResponse = { status: 0 };
|
|
111
|
+
// Transport follows the MODEL, not the pool: pi dispatches to a
|
|
112
|
+
// provider's custom streamSimple only when model.api matches the
|
|
113
|
+
// registered api (provider-composer.js), so a pool mixing
|
|
114
|
+
// transports (e.g. oc.zen's openai-responses muse-spark entry)
|
|
115
|
+
// must resolve the impl per request. Anything without a match
|
|
116
|
+
// falls back to the pool-level api. Verified live 2026-09-21:
|
|
117
|
+
// without this, muse-spark bypassed rotation (dummy key, no
|
|
118
|
+
// ses_/msg_ identity headers) and drew 403 FreeTierError.
|
|
119
|
+
const impl = getApiProvider(model.api) ?? fallback;
|
|
111
120
|
// Identity headers (session / request) are per-request, so they are
|
|
112
121
|
// merged here rather than baked into the provider registration.
|
|
113
122
|
// Providers apply options.headers last, so these win over the static
|