@jameslovespancakes/pi-plus 1.0.14 → 1.0.16
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 +36 -4
- package/package.json +5 -5
- package/src/core/accounts/oauth-pool.ts +4 -2
- package/src/core/accounts/registry.ts +7 -2
- package/src/core/anthropic/catalog.ts +139 -0
- package/src/core/anthropic/client-identity.ts +24 -5
- package/src/core/anthropic/models.ts +108 -26
- package/src/core/config.ts +1 -1
- package/src/core/gemini/LICENSE.md +21 -0
- package/src/core/gemini/client.ts +188 -0
- package/src/core/gemini/credentials.ts +56 -0
- package/src/core/gemini/models.ts +362 -0
- package/src/core/gemini/oauth.ts +240 -0
- package/src/core/gemini/request.ts +259 -0
- package/src/core/gemini/schema.ts +142 -0
- package/src/core/gemini/stream.ts +526 -0
- package/src/core/oauth/callback-server.ts +110 -0
- package/src/core/policy/policy.ts +3 -1
- package/src/domains/models/catalog-tool.ts +1 -1
- package/src/domains/subscriptions/accounts.ts +2 -2
- package/src/domains/subscriptions/footer.ts +1 -1
- package/src/domains/subscriptions/index.ts +3 -1
- package/src/domains/subscriptions/provider.ts +66 -7
- package/src/domains/subscriptions/providers/gemini.ts +111 -0
- package/src/domains/subscriptions/providers/oauth-pool.ts +35 -9
package/README.md
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
|
|
18
18
|
## Install
|
|
19
19
|
|
|
20
|
+
Requires **pi 0.87.0 or newer**.
|
|
21
|
+
|
|
20
22
|
```sh
|
|
21
23
|
pi install npm:@jameslovespancakes/pi-plus # npm
|
|
22
24
|
pi install git:github.com/jameslovespancakes/pi-plus # git
|
|
@@ -48,13 +50,13 @@ Nothing else is required, every feature configures itself from within pi.
|
|
|
48
50
|
|
|
49
51
|
### Pool every subscription
|
|
50
52
|
|
|
51
|
-
Add multiple Claude, ChatGPT/Codex, Kimi Code, or xAI/Grok accounts.
|
|
52
|
-
keeps credentials separate, refreshes them safely, and supports
|
|
53
|
-
quota-aware routing.
|
|
53
|
+
Add multiple Claude, ChatGPT/Codex, Gemini, Kimi Code, or xAI/Grok accounts.
|
|
54
|
+
pi-plus keeps credentials separate, refreshes them safely, and supports
|
|
55
|
+
sequential or quota-aware routing.
|
|
54
56
|
|
|
55
57
|
```
|
|
56
58
|
/accounts add anthropic work
|
|
57
|
-
/accounts add
|
|
59
|
+
/accounts add gemini personal
|
|
58
60
|
/routing quota-aware
|
|
59
61
|
```
|
|
60
62
|
|
|
@@ -75,6 +77,31 @@ Account and routing commands are provider-agnostic. Sequential routing uses
|
|
|
75
77
|
account order; quota-aware routing uses reported capacity and fairly probes
|
|
76
78
|
accounts whose provider does not publish quota headers.
|
|
77
79
|
|
|
80
|
+
### Gemini on a Google account
|
|
81
|
+
|
|
82
|
+
pi keeps only the metered `google/*` API. pi-plus adds `gemini/*`, served by
|
|
83
|
+
Google's Antigravity backend and pooled like every other subscription. The
|
|
84
|
+
provider is ported from [`pi-antigravity`](https://github.com/Rahularya01/pi-antigravity).
|
|
85
|
+
`/login` → **Gemini** signs in through the browser (callback on port 51121; on
|
|
86
|
+
a headless machine, paste the callback URL when asked).
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
/login gemini
|
|
90
|
+
/accounts add gemini personal
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Models include Gemini 3.x Flash and 3.1 Pro, plus the Claude and GPT-OSS
|
|
94
|
+
models the backend also serves; each thinking level routes to the backend's own
|
|
95
|
+
runtime model. The list refreshes from your account, so newly enabled models
|
|
96
|
+
appear without an update, and `/models-refresh` forces it.
|
|
97
|
+
|
|
98
|
+
A quota-walled account is held out of routing until it resets, so the next
|
|
99
|
+
request goes to another pooled account. `PI_GEMINI_PROJECT_ID` pins a Cloud
|
|
100
|
+
project; most accounts need none.
|
|
101
|
+
|
|
102
|
+
`gemini/*` is auto-approved because the subscription has already paid for it;
|
|
103
|
+
the metered `google/*` still asks.
|
|
104
|
+
|
|
78
105
|
### Pick models on evidence
|
|
79
106
|
|
|
80
107
|
`list_models` puts the full **Artificial Analysis** benchmark set in front of the
|
|
@@ -257,10 +284,15 @@ pi-plus is a thin layer over other people's work.
|
|
|
257
284
|
| [`pi`](https://pi.dev/) | the host agent and the entire extension API | MIT |
|
|
258
285
|
| [`xxhash-wasm`](https://github.com/jungomi/xxhash-wasm) | vendored into `src/core/anthropic/vendor/` for the billing checksum | MIT |
|
|
259
286
|
| [`pi-workflow-engine`](https://github.com/timbrinded/pi-workflow-engine) | embedded workflow runtime and built-in workflows | MIT |
|
|
287
|
+
| [`pi-antigravity`](https://github.com/Rahularya01/pi-antigravity) | reference for the `gemini` provider: Antigravity OAuth, wire format, model routing and catalogue discovery | MIT |
|
|
260
288
|
|
|
261
289
|
The workflow engine keeps its upstream license in
|
|
262
290
|
[`src/domains/workflows/LICENSE.md`](src/domains/workflows/LICENSE.md).
|
|
263
291
|
|
|
292
|
+
The `gemini` provider in `src/core/gemini/` is ported from `pi-antigravity`
|
|
293
|
+
0.8.0 and adapted to pi-plus's pooled accounts and pi's own Google adapter; its
|
|
294
|
+
upstream license is kept in [`src/core/gemini/LICENSE.md`](src/core/gemini/LICENSE.md).
|
|
295
|
+
|
|
264
296
|
The Anthropic provider, OAuth, quota and routing were originally adopted from
|
|
265
297
|
[`@cortexkit/pi-anthropic-auth`](https://github.com/cortexkit/anthropic-auth)
|
|
266
298
|
(MIT) and have since been reimplemented in this repository.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jameslovespancakes/pi-plus",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "pi and more",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,10 +49,10 @@
|
|
|
49
49
|
"ws": "^8.18.3"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@earendil-works/pi-agent-core": "^0.
|
|
53
|
-
"@earendil-works/pi-ai": "^0.
|
|
54
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
55
|
-
"@earendil-works/pi-tui": "^0.
|
|
52
|
+
"@earendil-works/pi-agent-core": "^0.87.0",
|
|
53
|
+
"@earendil-works/pi-ai": "^0.87.0",
|
|
54
|
+
"@earendil-works/pi-coding-agent": "^0.87.0",
|
|
55
|
+
"@earendil-works/pi-tui": "^0.87.0",
|
|
56
56
|
"typebox": "*"
|
|
57
57
|
},
|
|
58
58
|
"scripts": {
|
|
@@ -40,7 +40,6 @@ interface OAuthPoolFile {
|
|
|
40
40
|
providers: Record<string, ProviderOAuthPool>;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
const EMPTY_FILE: OAuthPoolFile = { version: 1, providers: {} };
|
|
44
43
|
const DEFAULT_PATH = "pi-plus-oauth-accounts.json";
|
|
45
44
|
const fileCache = new Map<string, OAuthPoolFile>();
|
|
46
45
|
|
|
@@ -52,7 +51,10 @@ function loadFile(path = oauthPoolPath()): OAuthPoolFile {
|
|
|
52
51
|
const cached = fileCache.get(path);
|
|
53
52
|
if (cached) return cached;
|
|
54
53
|
|
|
55
|
-
|
|
54
|
+
// The fallback is built per call: a shared constant would be aliased into
|
|
55
|
+
// `file.providers` whenever the file is missing, and every later write would
|
|
56
|
+
// accumulate in it — outliving both the cache and the file itself.
|
|
57
|
+
const raw = readJson<Partial<OAuthPoolFile>>(path, { version: 1, providers: {} });
|
|
56
58
|
const file: OAuthPoolFile = {
|
|
57
59
|
version: 1,
|
|
58
60
|
providers: raw.providers && typeof raw.providers === "object" ? raw.providers : {},
|
|
@@ -16,9 +16,14 @@ export interface ManagedAccount {
|
|
|
16
16
|
/** Fixed account order or provider quota-aware selection. */
|
|
17
17
|
export type RoutingMode = "sequential" | "quota-aware";
|
|
18
18
|
|
|
19
|
+
/** Dismisses a dialog programmatically, e.g. a paste prompt once the browser callback wins. */
|
|
20
|
+
export interface AccountDialogOptions {
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
}
|
|
23
|
+
|
|
19
24
|
export interface AccountUi {
|
|
20
|
-
input(title: string, placeholder?: string): Promise<string | undefined>;
|
|
21
|
-
select(title: string, options: string[]): Promise<string | undefined>;
|
|
25
|
+
input(title: string, placeholder?: string, options?: AccountDialogOptions): Promise<string | undefined>;
|
|
26
|
+
select(title: string, options: string[], dialog?: AccountDialogOptions): Promise<string | undefined>;
|
|
22
27
|
confirm(title: string, message: string): Promise<boolean>;
|
|
23
28
|
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
24
29
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { agentPath, readJson, writeJson } from "../store.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Live Anthropic model list.
|
|
5
|
+
*
|
|
6
|
+
* pi's bundled catalogue is generated at build time, so a model Anthropic
|
|
7
|
+
* ships afterwards is invisible to pi until pi is upgraded — `claude-opus-5-5`
|
|
8
|
+
* was usable for days while the picker denied it existed. Claude Code does not
|
|
9
|
+
* have that problem because it asks the API.
|
|
10
|
+
*
|
|
11
|
+
* `GET /v1/models` returns identity only (`id`, `display_name`), never limits
|
|
12
|
+
* or pricing, so a discovered model still needs those filled in. They are
|
|
13
|
+
* inherited from the newest model of the same family in pi's catalogue rather
|
|
14
|
+
* than invented here — see `models.ts`.
|
|
15
|
+
*
|
|
16
|
+
* The result is cached on disk so the picker is correct on the very first
|
|
17
|
+
* render, before any network call has finished.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const CACHE_FILE = "anthropic-models.json";
|
|
21
|
+
/** Re-ask once a day; a new model is news, not an emergency. */
|
|
22
|
+
export const CATALOG_TTL_MS = 24 * 60 * 60 * 1000;
|
|
23
|
+
|
|
24
|
+
export interface LiveModel {
|
|
25
|
+
id: string;
|
|
26
|
+
displayName?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface CacheFile {
|
|
30
|
+
version: 1;
|
|
31
|
+
fetchedAt: number;
|
|
32
|
+
models: LiveModel[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const EMPTY: CacheFile = { version: 1, fetchedAt: 0, models: [] };
|
|
36
|
+
|
|
37
|
+
export function catalogPath(): string {
|
|
38
|
+
return process.env.PI_PLUS_ANTHROPIC_MODELS_FILE ?? agentPath(CACHE_FILE);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function load(): CacheFile {
|
|
42
|
+
const raw = readJson<Partial<CacheFile>>(catalogPath(), { ...EMPTY });
|
|
43
|
+
return {
|
|
44
|
+
version: 1,
|
|
45
|
+
fetchedAt: typeof raw.fetchedAt === "number" ? raw.fetchedAt : 0,
|
|
46
|
+
models: Array.isArray(raw.models)
|
|
47
|
+
? raw.models.filter((model): model is LiveModel => typeof model?.id === "string" && model.id.length > 0)
|
|
48
|
+
: [],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Last known live list. Empty before the first successful fetch. */
|
|
53
|
+
export function cachedAnthropicModels(): LiveModel[] {
|
|
54
|
+
return load().models;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function catalogIsStale(now = Date.now()): boolean {
|
|
58
|
+
return now - load().fetchedAt > CATALOG_TTL_MS;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Test seam and reset path. */
|
|
62
|
+
export function writeAnthropicCatalog(models: LiveModel[], fetchedAt = Date.now()): void {
|
|
63
|
+
writeJson(catalogPath(), { version: 1, fetchedAt, models } satisfies CacheFile, true);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface ModelsResponse {
|
|
67
|
+
data?: { id?: string; display_name?: string }[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Asks Anthropic what this credential can actually use.
|
|
72
|
+
*
|
|
73
|
+
* `auth` comes from pi's resolved provider auth, so an OAuth subscription and
|
|
74
|
+
* a plain API key both work and the token is already refreshed.
|
|
75
|
+
*/
|
|
76
|
+
export interface ResolvedAnthropicAuth {
|
|
77
|
+
apiKey?: string;
|
|
78
|
+
headers?: Record<string, string | null>;
|
|
79
|
+
baseUrl?: string;
|
|
80
|
+
/** pi reports "OAuth" for a subscription credential. */
|
|
81
|
+
source?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Only a real API key goes in `x-api-key`; everything else is a bearer token.
|
|
86
|
+
*
|
|
87
|
+
* A prefix test alone is a trap: an Anthropic *OAuth* token is `sk-ant-oat…`
|
|
88
|
+
* and an API key is `sk-ant-api…`, so checking for `sk-` routes subscription
|
|
89
|
+
* tokens into the wrong header and the endpoint answers 401.
|
|
90
|
+
*/
|
|
91
|
+
function usesApiKeyHeader(auth: ResolvedAnthropicAuth): boolean {
|
|
92
|
+
if (auth.source === "OAuth") return false;
|
|
93
|
+
return (auth.apiKey ?? "").startsWith("sk-ant-api");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function fetchAnthropicModels(
|
|
97
|
+
auth: ResolvedAnthropicAuth,
|
|
98
|
+
signal?: AbortSignal,
|
|
99
|
+
): Promise<LiveModel[]> {
|
|
100
|
+
const headers: Record<string, string> = { "anthropic-version": "2023-06-01" };
|
|
101
|
+
for (const [key, value] of Object.entries(auth.headers ?? {})) {
|
|
102
|
+
if (value !== null) headers[key] = value;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (auth.apiKey) {
|
|
106
|
+
if (usesApiKeyHeader(auth)) headers["x-api-key"] = auth.apiKey;
|
|
107
|
+
else headers.Authorization = `Bearer ${auth.apiKey}`;
|
|
108
|
+
}
|
|
109
|
+
// Exactly this one beta. The model list rejects the request when the
|
|
110
|
+
// Claude Code betas meant for /v1/messages are carried over.
|
|
111
|
+
headers["anthropic-beta"] = "oauth-2025-04-20";
|
|
112
|
+
|
|
113
|
+
const base = (auth.baseUrl || "https://api.anthropic.com").replace(/\/+$/, "");
|
|
114
|
+
const response = await fetch(`${base}/v1/models?limit=200`, { headers, signal });
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
throw new Error(`Anthropic model list failed: ${response.status} ${response.statusText}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const body = (await response.json()) as ModelsResponse;
|
|
120
|
+
return (body.data ?? [])
|
|
121
|
+
.filter((model) => typeof model?.id === "string" && model.id.length > 0)
|
|
122
|
+
.map((model) => ({ id: model.id!, ...(model.display_name && { displayName: model.display_name }) }));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Refreshes the cache. Returns the ids that were not already known, so a
|
|
127
|
+
* caller can decide whether re-registering the provider is worth it.
|
|
128
|
+
*/
|
|
129
|
+
export async function refreshAnthropicCatalog(
|
|
130
|
+
auth: ResolvedAnthropicAuth,
|
|
131
|
+
signal?: AbortSignal,
|
|
132
|
+
): Promise<string[]> {
|
|
133
|
+
const before = new Set(cachedAnthropicModels().map((model) => model.id));
|
|
134
|
+
const models = await fetchAnthropicModels(auth, signal);
|
|
135
|
+
if (models.length === 0) return [];
|
|
136
|
+
|
|
137
|
+
writeAnthropicCatalog(models);
|
|
138
|
+
return models.map((model) => model.id).filter((id) => !before.has(id));
|
|
139
|
+
}
|
|
@@ -8,7 +8,7 @@ import { xxhash64 } from "./xxhash64.ts";
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
/** Pinned to the Claude Code release being imitated. */
|
|
11
|
-
export const CLAUDE_CODE_VERSION = "2.1.
|
|
11
|
+
export const CLAUDE_CODE_VERSION = "2.1.280";
|
|
12
12
|
|
|
13
13
|
/** Claude Code checksum constants. They can change between CLI releases. */
|
|
14
14
|
const CCH_SEED = 0x4d659218e32a3268n;
|
|
@@ -163,12 +163,31 @@ export function selectBetas(body: unknown, extra: string[] = []): string {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
/** Headers presenting this client as Claude Code. */
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
/**
|
|
167
|
+
* Betas this identity adds, on top of whatever pi computed for the model.
|
|
168
|
+
*
|
|
169
|
+
* These must be *merged*, never substituted. pi enables model-specific betas
|
|
170
|
+
* that authorise fields it also emits — `mid-conversation-output-config-…`
|
|
171
|
+
* covers the `output_config` system messages it inserts for adaptive-effort
|
|
172
|
+
* models. Replacing the list leaves those messages in the body with nothing
|
|
173
|
+
* permitting them, and Anthropic rejects the request:
|
|
174
|
+
*
|
|
175
|
+
* messages.1.output_config: Extra inputs are not permitted
|
|
176
|
+
*
|
|
177
|
+
* which is a 400 on every Opus 5 request and reads like a pi bug.
|
|
178
|
+
*/
|
|
179
|
+
export function identityBetas(body?: unknown, existing: readonly string[] = []): string[] {
|
|
180
|
+
return [...new Set([...existing.map((beta) => beta.trim()).filter(Boolean), ...selectBetas(body).split(",")])];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Deliberately omits `anthropic-beta`. pi copies that header verbatim into the
|
|
185
|
+
* request body's `betas`, so setting it here would discard pi's own list; the
|
|
186
|
+
* merge happens per request through {@link identityBetas} instead.
|
|
187
|
+
*/
|
|
188
|
+
export function clientIdentityHeaders(): Record<string, string> {
|
|
168
189
|
return {
|
|
169
190
|
"user-agent": USER_AGENT,
|
|
170
|
-
// Pi copies this header into the request body's betas field.
|
|
171
|
-
"anthropic-beta": selectBetas(body, incoming),
|
|
172
191
|
"anthropic-version": "2023-06-01",
|
|
173
192
|
"anthropic-dangerous-direct-browser-access": "true",
|
|
174
193
|
"x-app": "cli",
|
|
@@ -1,9 +1,23 @@
|
|
|
1
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import { ANTHROPIC_MODELS as PI_ANTHROPIC_MODELS } from "@earendil-works/pi-ai/providers/anthropic.models";
|
|
3
|
+
import { cachedAnthropicModels, type LiveModel } from "./catalog.ts";
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
6
|
* Anthropic model catalogue.
|
|
3
7
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
8
|
+
* Three sources, in increasing authority:
|
|
9
|
+
*
|
|
10
|
+
* 1. pi's bundled catalogue — complete metadata, but generated at build time
|
|
11
|
+
* and therefore always a little behind Anthropic.
|
|
12
|
+
* 2. the live `/v1/models` list cached by `catalog.ts` — authoritative about
|
|
13
|
+
* which models exist, silent about their limits.
|
|
14
|
+
* 3. local corrections and additions, for the handful of things pi has
|
|
15
|
+
* wrong or has never carried.
|
|
16
|
+
*
|
|
17
|
+
* The merge has to be a superset of pi's list.
|
|
18
|
+
* `registerProvider("anthropic", { models })` *substitutes* the catalogue
|
|
19
|
+
* rather than extending it, so anything omitted here vanishes from the picker
|
|
20
|
+
* with no error at all.
|
|
7
21
|
*/
|
|
8
22
|
|
|
9
23
|
export const FABLE_CONTEXT_WINDOW = 1_000_000;
|
|
@@ -31,39 +45,107 @@ const fable = (id: string, name: string, pricing = FABLE_PRICING): ModelSpec =>
|
|
|
31
45
|
contextWindow: FABLE_CONTEXT_WINDOW, maxTokens: FABLE_MAX_OUTPUT,
|
|
32
46
|
});
|
|
33
47
|
|
|
34
|
-
|
|
48
|
+
/** Models neither pi nor the live list carries. */
|
|
49
|
+
const ADDITIONS: ModelSpec[] = [
|
|
35
50
|
fable("claude-fable-5", "Claude Fable 5"),
|
|
36
51
|
fable("claude-mythos-5", "Claude Mythos 5"),
|
|
37
52
|
fable("claude-fable-5-1", "Claude Fable 5.1", FABLE_5_1_PRICING),
|
|
38
53
|
fable("claude-mythos-5-1", "Claude Mythos 5.1", FABLE_5_1_PRICING),
|
|
39
|
-
|
|
40
|
-
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Applied on top of pi's entry for the same id. Only the listed fields change;
|
|
58
|
+
* everything else pi knows (thinking levels, input limits, cache tiers) stays.
|
|
59
|
+
*/
|
|
60
|
+
const CORRECTIONS: Record<string, Partial<ModelSpec>> = {
|
|
61
|
+
"claude-opus-5": {
|
|
41
62
|
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
42
63
|
contextWindow: 1_000_000, maxTokens: 128_000,
|
|
43
64
|
},
|
|
44
|
-
{
|
|
45
|
-
id: "claude-opus-4-8", name: "Claude Opus 4.8", reasoning: true, input: textImage,
|
|
65
|
+
"claude-opus-4-8": {
|
|
46
66
|
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
47
67
|
contextWindow: 1_000_000, maxTokens: 128_000,
|
|
48
68
|
},
|
|
49
|
-
{
|
|
50
|
-
id: "claude-opus-4-5", name: "Claude Opus 4.5", reasoning: true, input: textImage,
|
|
51
|
-
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
52
|
-
contextWindow: 200_000, maxTokens: 64_000,
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
id: "claude-sonnet-5", name: "Claude Sonnet 5", reasoning: true, input: textImage,
|
|
69
|
+
"claude-sonnet-5": {
|
|
56
70
|
cost: { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
|
|
57
71
|
contextWindow: 1_000_000, maxTokens: 128_000,
|
|
58
72
|
},
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
]
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
type AnthropicModel = Model<"anthropic-messages">;
|
|
76
|
+
|
|
77
|
+
/** `claude-opus-5-5` -> `opus`; undated ids only, so aliases do not win below. */
|
|
78
|
+
function familyOf(id: string): string | undefined {
|
|
79
|
+
return /^claude-([a-z]+)-\d/.exec(id)?.[1];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** `claude-opus-5-5` -> [5, 5]. A dated alias sorts below its undated form. */
|
|
83
|
+
function versionOf(id: string): number[] {
|
|
84
|
+
const tail = /^claude-[a-z]+-(.+)$/.exec(id)?.[1] ?? "";
|
|
85
|
+
// A date suffix (20251101) is a release stamp, not a version component.
|
|
86
|
+
return tail.split("-").filter((part) => /^\d+$/.test(part) && part.length < 5).map(Number);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isNewer(candidate: string, incumbent: string): boolean {
|
|
90
|
+
const left = versionOf(candidate);
|
|
91
|
+
const right = versionOf(incumbent);
|
|
92
|
+
for (let i = 0; i < Math.max(left.length, right.length); i++) {
|
|
93
|
+
const a = left[i] ?? -1;
|
|
94
|
+
const b = right[i] ?? -1;
|
|
95
|
+
if (a !== b) return a > b;
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Title-case a display name when Anthropic did not send one. */
|
|
101
|
+
function nameFor(model: LiveModel): string {
|
|
102
|
+
if (model.displayName) return model.displayName;
|
|
103
|
+
const family = familyOf(model.id) ?? "";
|
|
104
|
+
const version = versionOf(model.id).join(".");
|
|
105
|
+
const pretty = family ? family[0].toUpperCase() + family.slice(1) : model.id;
|
|
106
|
+
return version ? `Claude ${pretty} ${version}` : `Claude ${pretty}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const base = Object.values(PI_ANTHROPIC_MODELS as unknown as Record<string, AnthropicModel>);
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Newest model per family, used as the template for a model the live list
|
|
113
|
+
* reports but pi has never seen. Inheriting beats guessing: a new Opus gets
|
|
114
|
+
* the current Opus's context window, output cap, thinking levels and pricing,
|
|
115
|
+
* and those track pi upgrades automatically.
|
|
116
|
+
*/
|
|
117
|
+
function templates(models: AnthropicModel[]): Map<string, AnthropicModel> {
|
|
118
|
+
const newest = new Map<string, AnthropicModel>();
|
|
119
|
+
for (const model of models) {
|
|
120
|
+
const family = familyOf(model.id);
|
|
121
|
+
if (!family) continue;
|
|
122
|
+
const incumbent = newest.get(family);
|
|
123
|
+
if (!incumbent || isNewer(model.id, incumbent.id)) newest.set(family, model);
|
|
124
|
+
}
|
|
125
|
+
return newest;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** pi's catalogue, corrected, plus anything the live list knows about that pi does not. */
|
|
129
|
+
export function buildAnthropicModels(live: LiveModel[] = cachedAnthropicModels()): ModelSpec[] {
|
|
130
|
+
const corrected = base.map((model) => ({ ...model, ...CORRECTIONS[model.id] })) as AnthropicModel[];
|
|
131
|
+
const known = new Set(corrected.map((model) => model.id));
|
|
132
|
+
const byFamily = templates(corrected);
|
|
133
|
+
|
|
134
|
+
const discovered: AnthropicModel[] = [];
|
|
135
|
+
for (const model of live) {
|
|
136
|
+
if (known.has(model.id)) continue;
|
|
137
|
+
const template = byFamily.get(familyOf(model.id) ?? "");
|
|
138
|
+
if (!template) continue; // An unrecognised family has nothing safe to inherit.
|
|
139
|
+
known.add(model.id);
|
|
140
|
+
discovered.push({ ...template, id: model.id, name: nameFor(model) });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return [
|
|
144
|
+
...corrected,
|
|
145
|
+
...discovered,
|
|
146
|
+
...ADDITIONS.filter((model) => !known.has(model.id)),
|
|
147
|
+
] as unknown as ModelSpec[];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Snapshot taken at load; `refreshAnthropicModels()` re-registers on change. */
|
|
151
|
+
export const ANTHROPIC_MODELS: ModelSpec[] = buildAnthropicModels();
|
package/src/core/config.ts
CHANGED
|
@@ -28,7 +28,7 @@ export interface PiPlusConfig {
|
|
|
28
28
|
const DEFAULTS: PiPlusConfig = {
|
|
29
29
|
env: {},
|
|
30
30
|
policy: {
|
|
31
|
-
autoApprove: ["anthropic/*", "openai-codex/*", "kimi-coding/*", "xai/*"],
|
|
31
|
+
autoApprove: ["anthropic/*", "openai-codex/*", "gemini/*", "kimi-coding/*", "xai/*"],
|
|
32
32
|
requireApproval: ["openrouter/*", "google/*", "openai/*"],
|
|
33
33
|
deny: [],
|
|
34
34
|
},
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rahul Arya
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|