pi-cliproxyapi-native 0.1.7 → 0.1.9
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 +38 -7
- package/extensions/index.ts +27 -18
- package/package.json +4 -4
- package/src/catalog.ts +11 -6
- package/src/claude-quota.ts +4 -1
- package/src/compaction.ts +78 -0
- package/src/media-defaults.ts +2 -2
- package/src/provider.ts +21 -10
package/README.md
CHANGED
|
@@ -6,15 +6,15 @@ Chat uses Pi's native API adapters and model metadata, with availability from yo
|
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
The extension targets Pi 1.1.0 and CLIProxyAPI 8.0.22, with Node.js 22.19 or newer. Run CLIProxyAPI with your upstream accounts configured before connecting Pi. The extension uses Pi's host-provided packages, adds no runtime dependencies, and needs no compilation.
|
|
10
10
|
|
|
11
11
|
If you installed `pi-cliproxy-native`, remove it before installing the renamed package. Both register the same `cliproxyapi` provider.
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
|
-
pi install npm:pi-cliproxyapi-native@0.1.
|
|
14
|
+
pi install npm:pi-cliproxyapi-native@0.1.9
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
For Git installation, use `pi install git:github.com/hawkff/pi-cliproxyapi-native@v0.1.
|
|
17
|
+
For Git installation, use `pi install git:github.com/hawkff/pi-cliproxyapi-native@v0.1.9`. For a local checkout, use `pi install /path/to/pi-cliproxyapi-native`.
|
|
18
18
|
|
|
19
19
|
The default proxy address is `http://localhost:8317`. For another address, set the [connection](#connection) before logging in. Restart Pi, then run:
|
|
20
20
|
|
|
@@ -135,9 +135,9 @@ Your proxy must advertise the exact ID for a model to be available.
|
|
|
135
135
|
| Google models | `gemini-2.5-flash-image`, `gemini-3.1-flash-image`, `gemini-3-pro-image`, `gemini-3.1-flash-lite-image` |
|
|
136
136
|
| xAI models | `grok-imagine-image`, `grok-imagine-image-quality`, `grok-imagine-image-2.0`, `grok-imagine-video`, `grok-imagine-video-1.5`, `grok-imagine-video-1.5-preview` |
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
Supported media IDs can also use advertised `api/` and `oauth/` credential routes. Gemini image models also support `vertex/` and `antigravity/` backend routes. Those backend prefixes do not enable OpenAI or xAI media execution; the picker shows those entries as disabled. Other prefixes, nested prefixes, media aliases, unknown media IDs, Google Veo, and editing endpoints are unsupported.
|
|
139
139
|
|
|
140
|
-
The extension disables these retired Imagen IDs, including their
|
|
140
|
+
The extension disables these retired Imagen IDs, including their supported prefixed forms:
|
|
141
141
|
|
|
142
142
|
- `imagen-3.0-generate-002`
|
|
143
143
|
- `imagen-3.0-fast-generate-001`
|
|
@@ -199,8 +199,18 @@ Invalid management credentials, rejected account access, invalid usage data, or
|
|
|
199
199
|
|
|
200
200
|
Management requests stay on the configured proxy and reject redirects. Account access tokens stay in CLIProxyAPI. The extension reads account metadata and proxies usage GETs; it does not reset quotas, change credentials, or clear proxy cooldowns. Management keys grant broader access than chat keys, so keep them private. Live lookup supports up to 100 active Claude subscription accounts.
|
|
201
201
|
|
|
202
|
+
Explicit `api/` routes skip live Claude subscription lookup, even with a management key. `oauth/` routes keep the lookup for matching OAuth accounts. Retries keep the selected route's full model ID.
|
|
203
|
+
|
|
202
204
|
Other chat routes retain the response-based fallback: `Retry-After` on HTTP 429 or a recognized quota HTTP 403, or `reset_seconds` in a `model_cooldown` error. Google's native adapter exposes only the structured error body. Those waits share the same seven-day deadline but cannot inspect live weekly usage. Image and video generation still do not retry.
|
|
203
205
|
|
|
206
|
+
### Compaction
|
|
207
|
+
|
|
208
|
+
For automatic compaction and `/compact`, the extension limits medium or higher thinking to low without changing your chat thinking level. It uses Pi's default thinking budgets for these requests. If a summary hits its output cap, it retries once with more output room, up to the model's output limit. Pi's native adapters also limit output to the remaining context space. An empty or thinking-only response gets one retry.
|
|
209
|
+
|
|
210
|
+
Pi still selects which messages to summarize and retains recent history, previous summaries, custom instructions, and file tracking. Only the final response reaches the compactor, with usage from both attempts. A second incomplete response fails compaction rather than saving a partial checkpoint. Cancellation, tool calls, and provider failures do not trigger these output retries; Pi keeps its configured transient-error retries and the extension keeps its quota handling.
|
|
211
|
+
|
|
212
|
+
Authentication failures, exhausted quotas, and inputs that exceed the model's context window still need correction. Run `/compact` after resolving the cause. The extension does not truncate history to force a summary or switch models. CLIProxyAPI's Responses compaction endpoint is separate from Pi's text-summary workflow.
|
|
213
|
+
|
|
204
214
|
### Aliases and model limits
|
|
205
215
|
|
|
206
216
|
The chat catalog skips unknown IDs rather than guessing their capabilities. Recognized catalog owners resolve duplicate metadata IDs; ambiguous cross-family matches stay out.
|
|
@@ -217,9 +227,9 @@ To describe a proxy alias, add a canonical metadata reference to `~/.pi/agent/pi
|
|
|
217
227
|
|
|
218
228
|
Replace `<canonical-model-id>` with an ID from Pi's Anthropic catalog. References can also use `openai/`, `openai-codex/`, or `google/`. Aliases select metadata; requests retain the original proxy ID.
|
|
219
229
|
|
|
220
|
-
|
|
230
|
+
The recognized prefixes are `vertex/`, `antigravity/`, `api/`, and `oauth/`. Each can reuse its unprefixed ID's alias if that reference resolves to one metadata entry. An alias for the full prefixed ID takes precedence. Other prefixes need an explicit chat alias. Arbitrary aliases may lack ID-specific adapter behavior; canonical Gemini IDs and the recognized prefixes retain native thinking and tool-turn handling.
|
|
221
231
|
|
|
222
|
-
For Antigravity IDs `claude-opus-5-5-high` and `claude-sonnet-5-5-high`, the extension falls back to `anthropic/claude-opus-5-5` and `anthropic/claude-sonnet-5-5` metadata. This applies to bare IDs and recognized
|
|
232
|
+
For Antigravity IDs `claude-opus-5-5-high` and `claude-sonnet-5-5-high`, the extension falls back to `anthropic/claude-opus-5-5` and `anthropic/claude-sonnet-5-5` metadata. This applies to bare IDs and the recognized route prefixes. Explicit aliases take precedence, followed by exact native metadata. The fallback requires the corresponding canonical entry in Pi and names the model like a configured alias, for example `claude-opus-5-5-high (Claude Opus 5.5)`; requests keep the advertised `-high` ID.
|
|
223
233
|
|
|
224
234
|
Use Pi's `models.json` `modelOverrides` for limits, prices, or compatibility changes. Use its `models` array for IDs with no built-in metadata. Updating Pi supplies newer metadata. Catalog prices are estimates, not the proxy's bill; check upstream limits before increasing them.
|
|
225
235
|
|
|
@@ -236,6 +246,27 @@ These examples select media defaults. Chat routes belong in Pi's built-in `/mode
|
|
|
236
246
|
|
|
237
247
|
Set the CLIProxyAPI auth record's top-level `prefix` to `vertex` or `antigravity` to advertise those routes. With `routing.force-model-prefix: false` in CLIProxyAPI 8, the proxy retains bare IDs too. Legacy configurations use the top-level `force-model-prefix` setting. This extension does not change proxy configuration or restart it.
|
|
238
248
|
|
|
249
|
+
## OAuth and API-key routes
|
|
250
|
+
|
|
251
|
+
CLIProxyAPI can advertise separate model IDs for OAuth and API-key credentials. The extension labels `oauth/<model>` as **OAuth** and `api/<model>` as **API key**. It matches the unprefixed model's metadata and sends the full advertised ID on requests. It does not retry an explicit route as a bare ID or another credential route.
|
|
252
|
+
|
|
253
|
+
Configure the prefixes in CLIProxyAPI's v8 layout:
|
|
254
|
+
|
|
255
|
+
- For an API-key group, set `api-keys.<provider>[].prefix` to `api`. A key-level `prefix` can override the group value.
|
|
256
|
+
- For an OAuth credential, set the auth record's top-level `prefix` to `oauth`.
|
|
257
|
+
|
|
258
|
+
If a legacy file stores client keys in the root `api-keys` list, migrate them to `access.api-keys` before adding provider groups. A successful v8 management write performs this migration.
|
|
259
|
+
|
|
260
|
+
CLIProxyAPI also supports per-group and per-key model aliases, exclusions, priorities, and proxy overrides. See its [configuration fields](https://github.com/router-for-me/cliproxyapidocs/blob/main/docs/en/configuration/options.md).
|
|
261
|
+
|
|
262
|
+
The [v8 management API](https://github.com/router-for-me/cliproxyapidocs/blob/main/docs/en/management/apiv8.md) manages API-key groups through `/v8/management/config/api-keys/<provider>`. These lists must be replaced as a whole; numeric paths cannot select an entry. OAuth fields use `PATCH /v8/management/credentials/fields`. The extension does not call these endpoints or need a management key for model discovery.
|
|
263
|
+
|
|
264
|
+
After setting the prefixes, run `/cliproxyapi-refresh`. Choose `cliproxyapi/oauth/<model>` or `cliproxyapi/api/<model>` in Pi's `/model` picker. Media routes appear in `/cli:model`.
|
|
265
|
+
|
|
266
|
+
The extension trusts your prefix configuration; it does not inspect credential types. Keep each prefix limited to its matching credential type. Unprefixed IDs remain **Automatic (proxy routing)** and may use either pool. Set `routing.force-model-prefix: true` in CLIProxyAPI to exclude prefixed credentials from unprefixed requests.
|
|
267
|
+
|
|
268
|
+
Chat and media selections remain independent. The automatic OpenAI image default is still unprefixed; select a prefixed media model to choose its credential route.
|
|
269
|
+
|
|
239
270
|
## Thinking and child sessions
|
|
240
271
|
|
|
241
272
|
Use a reference such as `cliproxyapi/<model-id>:high` in Pi or pi-subagents. Pi separates the thinking suffix from the request ID. Supported levels come from model metadata; `xhigh` and `max` require explicit support. Pi clamps unsupported levels, and models that require thinking cannot use `off`. Keep thinking suffixes out of alias references.
|
package/extensions/index.ts
CHANGED
|
@@ -22,6 +22,10 @@ export default async function (pi: ExtensionAPI) {
|
|
|
22
22
|
throw new Error("pi-cliproxyapi.json must contain valid JSON.");
|
|
23
23
|
}
|
|
24
24
|
const config = parseConfig(raw, process.env.CLIPROXYAPI_BASE_URL);
|
|
25
|
+
const compactionSignals = new WeakSet<AbortSignal>();
|
|
26
|
+
pi.on("session_before_compact", (event) => {
|
|
27
|
+
compactionSignals.add(event.signal);
|
|
28
|
+
});
|
|
25
29
|
let context: ExtensionContext | undefined;
|
|
26
30
|
const waits = new Map<symbol, number>();
|
|
27
31
|
pi.on("session_start", (_event, ctx) => {
|
|
@@ -33,25 +37,30 @@ export default async function (pi: ExtensionAPI) {
|
|
|
33
37
|
waits.clear();
|
|
34
38
|
});
|
|
35
39
|
pi.registerProvider(
|
|
36
|
-
createCliproxyProvider(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (!
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
createCliproxyProvider(
|
|
41
|
+
config,
|
|
42
|
+
undefined,
|
|
43
|
+
(id, seconds) => {
|
|
44
|
+
if (!context) return;
|
|
45
|
+
if (seconds === undefined) waits.delete(id);
|
|
46
|
+
else {
|
|
47
|
+
if (!waits.has(id)) {
|
|
48
|
+
const message = `CLIProxyAPI quota limit. Retrying at ${new Date(Date.now() + seconds * 1000).toLocaleString()}. Cancel the current request to stop waiting.`;
|
|
49
|
+
if (context.mode === "print" || context.mode === "json") process.stderr.write(`${message}\n`);
|
|
50
|
+
else context.ui.notify(message, "warning");
|
|
51
|
+
}
|
|
52
|
+
waits.set(id, seconds);
|
|
44
53
|
}
|
|
45
|
-
waits.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
const remaining = Math.min(...waits.values());
|
|
55
|
+
context.ui.setStatus(
|
|
56
|
+
"cliproxyapi-quota",
|
|
57
|
+
waits.size
|
|
58
|
+
? `CLIProxyAPI quota: retry in ${Math.floor(remaining / 3600)}h ${Math.floor(remaining / 60) % 60}m ${remaining % 60}s`
|
|
59
|
+
: undefined,
|
|
60
|
+
);
|
|
61
|
+
},
|
|
62
|
+
compactionSignals,
|
|
63
|
+
),
|
|
55
64
|
);
|
|
56
65
|
registerMediaTools(pi, config);
|
|
57
66
|
registerModelPicker(pi, config);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cliproxyapi-native",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "CLIProxyAPI chat discovery and image/video generation with Pi's native adapters",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@biomejs/biome": "^2.5.13",
|
|
42
|
-
"@earendil-works/pi-ai": "^1.0
|
|
43
|
-
"@earendil-works/pi-coding-agent": "^1.0
|
|
44
|
-
"@earendil-works/pi-tui": "^1.0
|
|
42
|
+
"@earendil-works/pi-ai": "^1.1.0",
|
|
43
|
+
"@earendil-works/pi-coding-agent": "^1.1.0",
|
|
44
|
+
"@earendil-works/pi-tui": "^1.1.0",
|
|
45
45
|
"@types/node": "^24.13.4",
|
|
46
46
|
"typescript": "5.9.3"
|
|
47
47
|
},
|
package/src/catalog.ts
CHANGED
|
@@ -36,15 +36,20 @@ export function parseCatalog(value: unknown) {
|
|
|
36
36
|
|
|
37
37
|
// Resolve metadata and capabilities without changing registry or request IDs.
|
|
38
38
|
export function modelRoute(id: string) {
|
|
39
|
-
const match = /^(vertex|antigravity)\/(.+)$/.exec(id);
|
|
39
|
+
const match = /^(vertex|antigravity|oauth|api)\/(.+)$/.exec(id);
|
|
40
|
+
const prefix = match?.[1];
|
|
40
41
|
return {
|
|
41
42
|
metadataId: match?.[2] ?? id,
|
|
42
|
-
|
|
43
|
+
prefix,
|
|
44
|
+
backend: prefix === "vertex" ? "Vertex" : prefix === "antigravity" ? "Antigravity" : undefined,
|
|
45
|
+
authType: prefix === "api" ? "api-key" : prefix === "oauth" ? "oauth" : undefined,
|
|
43
46
|
};
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
export function backendLabel(id: string) {
|
|
47
|
-
|
|
50
|
+
const { backend, authType } = modelRoute(id);
|
|
51
|
+
if (authType) return authType === "api-key" ? "API key" : "OAuth";
|
|
52
|
+
return backend ?? (id.includes("/") ? "Unknown backend" : "Automatic (proxy routing)");
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
export function routedName(id: string, name = id) {
|
|
@@ -188,11 +193,11 @@ export function mapCatalog(value: unknown, config: Config, known: readonly Model
|
|
|
188
193
|
skipped.add(entry.id);
|
|
189
194
|
continue;
|
|
190
195
|
}
|
|
191
|
-
const { metadataId,
|
|
192
|
-
const routed = !entry.id.includes("/") ||
|
|
196
|
+
const { metadataId, prefix } = modelRoute(entry.id);
|
|
197
|
+
const routed = !entry.id.includes("/") || prefix;
|
|
193
198
|
const alias = Object.hasOwn(config.aliases, entry.id)
|
|
194
199
|
? config.aliases[entry.id]
|
|
195
|
-
:
|
|
200
|
+
: prefix && Object.hasOwn(config.aliases, metadataId)
|
|
196
201
|
? config.aliases[metadataId]
|
|
197
202
|
: routed && !byId.has(metadataId)
|
|
198
203
|
? metadataFallbacks.get(metadataId)
|
package/src/claude-quota.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
2
|
+
import { modelRoute } from "./catalog.ts";
|
|
2
3
|
import { type Config, isRecord } from "./config.ts";
|
|
3
4
|
|
|
4
5
|
export type QuotaWaitHandler = (id: symbol, remainingSeconds: number | undefined) => void;
|
|
@@ -150,7 +151,9 @@ export async function fetchClaudeQuota(
|
|
|
150
151
|
// ponytail: probe at most 100 accounts; batch larger pools before raising this limit.
|
|
151
152
|
if (accounts.length > 100)
|
|
152
153
|
throw new Error("Live quota lookup supports at most 100 active Claude accounts.");
|
|
153
|
-
const
|
|
154
|
+
const { metadataId, prefix } = modelRoute(modelId);
|
|
155
|
+
const reference =
|
|
156
|
+
config.aliases[modelId] ?? (prefix ? config.aliases[metadataId] : undefined) ?? metadataId;
|
|
154
157
|
const results = await Promise.allSettled(
|
|
155
158
|
accounts.map(async (account: unknown) => {
|
|
156
159
|
if (
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { contentText, lazyStream, type ProviderStreams } from "@earendil-works/pi-ai";
|
|
2
|
+
|
|
3
|
+
export function withCompactionRecovery(
|
|
4
|
+
stream: ProviderStreams["streamSimple"],
|
|
5
|
+
signals: WeakSet<AbortSignal>,
|
|
6
|
+
): ProviderStreams["streamSimple"] {
|
|
7
|
+
return (model, context, options) => {
|
|
8
|
+
if (!options?.signal || !signals.has(options.signal)) return stream(model, context, options);
|
|
9
|
+
const signal = options.signal;
|
|
10
|
+
return lazyStream(model, async () => ({
|
|
11
|
+
async *[Symbol.asyncIterator]() {
|
|
12
|
+
const limit = model.maxTokens > 0 ? model.maxTokens : 32768;
|
|
13
|
+
let maxTokens = Math.max(1, Math.min(options.maxTokens ?? 16384, limit));
|
|
14
|
+
const reasoning =
|
|
15
|
+
options.reasoning && !["minimal", "low"].includes(options.reasoning) ? "low" : options.reasoning;
|
|
16
|
+
const run = () =>
|
|
17
|
+
stream(model, context, {
|
|
18
|
+
...options,
|
|
19
|
+
maxTokens,
|
|
20
|
+
reasoning,
|
|
21
|
+
// Chat-specific thinking budgets must not consume the summary's output allowance.
|
|
22
|
+
thinkingBudgets: undefined,
|
|
23
|
+
}).result();
|
|
24
|
+
let response = await run();
|
|
25
|
+
const retryTokens = Math.min(Math.max(maxTokens * 2, 16384), limit);
|
|
26
|
+
const empty = response.stopReason === "stop" && !contentText(response.content).trim();
|
|
27
|
+
if (
|
|
28
|
+
!signal.aborted &&
|
|
29
|
+
!response.content.some((block) => block.type === "toolCall") &&
|
|
30
|
+
((response.stopReason === "length" && retryTokens > maxTokens) || empty)
|
|
31
|
+
) {
|
|
32
|
+
maxTokens = retryTokens;
|
|
33
|
+
const previous = response.usage;
|
|
34
|
+
response = await run();
|
|
35
|
+
response = { ...response, usage: structuredClone(response.usage) };
|
|
36
|
+
for (const key of [
|
|
37
|
+
"input",
|
|
38
|
+
"output",
|
|
39
|
+
"cacheRead",
|
|
40
|
+
"cacheWrite",
|
|
41
|
+
"cacheWrite1h",
|
|
42
|
+
"reasoning",
|
|
43
|
+
"totalTokens",
|
|
44
|
+
] as const) {
|
|
45
|
+
if (previous[key] !== undefined) response.usage[key] = (response.usage[key] ?? 0) + previous[key];
|
|
46
|
+
}
|
|
47
|
+
for (const key of ["input", "output", "cacheRead", "cacheWrite", "total"] as const)
|
|
48
|
+
response.usage.cost[key] += previous.cost[key];
|
|
49
|
+
}
|
|
50
|
+
if (signal.aborted) {
|
|
51
|
+
yield {
|
|
52
|
+
type: "error",
|
|
53
|
+
reason: "aborted",
|
|
54
|
+
error: { ...response, stopReason: "aborted", errorMessage: "Compaction was aborted." },
|
|
55
|
+
};
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (
|
|
59
|
+
response.content.some((block) => block.type === "toolCall") ||
|
|
60
|
+
!["stop", "length", "error"].includes(response.stopReason) ||
|
|
61
|
+
(response.stopReason === "stop" && !contentText(response.content).trim())
|
|
62
|
+
) {
|
|
63
|
+
response = {
|
|
64
|
+
...response,
|
|
65
|
+
stopReason: "error",
|
|
66
|
+
errorMessage: "CLIProxyAPI compaction did not return a complete text summary.",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (response.stopReason === "stop" || response.stopReason === "length") {
|
|
70
|
+
yield { type: "start", partial: { ...response, content: [], stopReason: "pending" } };
|
|
71
|
+
yield { type: "done", reason: response.stopReason, message: response };
|
|
72
|
+
} else {
|
|
73
|
+
yield { type: "error", reason: "error", error: response };
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
}));
|
|
77
|
+
};
|
|
78
|
+
}
|
package/src/media-defaults.ts
CHANGED
|
@@ -34,12 +34,12 @@ export function readMediaDefaults(config: Config, ctx: DefaultsContext) {
|
|
|
34
34
|
export function automaticImageDefault(config: Config, ctx: DefaultsContext) {
|
|
35
35
|
const model = ctx.model;
|
|
36
36
|
if (!model || mediaPurpose(model.id)) return undefined;
|
|
37
|
-
const { metadataId,
|
|
37
|
+
const { metadataId, prefix } = modelRoute(model.id);
|
|
38
38
|
const alias =
|
|
39
39
|
model.provider === PROVIDER_ID
|
|
40
40
|
? Object.hasOwn(config.aliases, model.id)
|
|
41
41
|
? config.aliases[model.id]
|
|
42
|
-
:
|
|
42
|
+
: prefix && Object.hasOwn(config.aliases, metadataId)
|
|
43
43
|
? config.aliases[metadataId]
|
|
44
44
|
: undefined
|
|
45
45
|
: undefined;
|
package/src/provider.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import { getBuiltinModels } from "@earendil-works/pi-ai/providers/all";
|
|
21
21
|
import { type CpaApi, endpoint, mapCatalog, modelRoute } from "./catalog.ts";
|
|
22
22
|
import { type QuotaWaitHandler, waitForClaudeQuota } from "./claude-quota.ts";
|
|
23
|
+
import { withCompactionRecovery } from "./compaction.ts";
|
|
23
24
|
import { type Config, isRecord, PROVIDER_ID } from "./config.ts";
|
|
24
25
|
|
|
25
26
|
export type { QuotaWaitHandler } from "./claude-quota.ts";
|
|
@@ -176,17 +177,23 @@ async function waitForQuota(resetAt: number, signal?: AbortSignal, onWait?: Quot
|
|
|
176
177
|
}
|
|
177
178
|
}
|
|
178
179
|
|
|
179
|
-
function route(
|
|
180
|
+
function route(
|
|
181
|
+
streams: ProviderStreams,
|
|
182
|
+
config: Config,
|
|
183
|
+
compactionSignals: WeakSet<AbortSignal>,
|
|
184
|
+
onWait?: QuotaWaitHandler,
|
|
185
|
+
): ProviderStreams {
|
|
180
186
|
const wrap =
|
|
181
187
|
(stream: ProviderStreams["streamSimple"]): ProviderStreams["streamSimple"] =>
|
|
182
188
|
(selected, context, options) => {
|
|
183
189
|
// A resumed selection must not send the current key to an old endpoint.
|
|
184
190
|
const model = { ...selected, baseUrl: endpoint(config.baseUrl, selected.api) };
|
|
185
|
-
const { metadataId, backend } = modelRoute(model.id);
|
|
186
|
-
const google =
|
|
191
|
+
const { metadataId, prefix, backend, authType } = modelRoute(model.id);
|
|
192
|
+
const google = prefix && model.api === "google-generative-ai";
|
|
187
193
|
const managementKey =
|
|
188
194
|
options?.env?.CLIPROXYAPI_MANAGEMENT_KEY ?? process.env.CLIPROXYAPI_MANAGEMENT_KEY ?? "";
|
|
189
|
-
const liveClaude =
|
|
195
|
+
const liveClaude =
|
|
196
|
+
model.api === "anthropic-messages" && !backend && authType !== "api-key" && managementKey !== "";
|
|
190
197
|
const adapterModel = google ? { ...model, id: metadataId } : model;
|
|
191
198
|
// Google needs canonical IDs for tool turns. Swap identities to keep bare history cross-route.
|
|
192
199
|
const adapterContext = google
|
|
@@ -330,17 +337,21 @@ function route(streams: ProviderStreams, config: Config, onWait?: QuotaWaitHandl
|
|
|
330
337
|
},
|
|
331
338
|
}));
|
|
332
339
|
};
|
|
333
|
-
return {
|
|
340
|
+
return {
|
|
341
|
+
stream: wrap(streams.stream),
|
|
342
|
+
streamSimple: withCompactionRecovery(wrap(streams.streamSimple), compactionSignals),
|
|
343
|
+
};
|
|
334
344
|
}
|
|
335
345
|
|
|
336
346
|
export function createCliproxyProvider(
|
|
337
347
|
config: Config,
|
|
338
348
|
known: readonly Model<Api>[] = builtinCatalog(),
|
|
339
349
|
onQuotaWait?: QuotaWaitHandler,
|
|
350
|
+
compactionSignals = new WeakSet<AbortSignal>(),
|
|
340
351
|
) {
|
|
341
352
|
const standardAuth = envApiKeyAuth("CLIProxyAPI API key", ["CLIPROXYAPI_API_KEY"]);
|
|
342
353
|
const scope = createHash("sha256")
|
|
343
|
-
.update(JSON.stringify([
|
|
354
|
+
.update(JSON.stringify([5, { baseUrl: config.baseUrl, aliases: config.aliases }]))
|
|
344
355
|
.digest("hex");
|
|
345
356
|
const provider = createProvider<CpaApi>({
|
|
346
357
|
id: PROVIDER_ID,
|
|
@@ -372,10 +383,10 @@ export function createCliproxyProvider(
|
|
|
372
383
|
return (await discover(config, context.credential.key, context.signal, known)).models;
|
|
373
384
|
},
|
|
374
385
|
api: {
|
|
375
|
-
"anthropic-messages": route(anthropicMessagesApi(), config, onQuotaWait),
|
|
376
|
-
"openai-responses": route(openAIResponsesApi(), config, onQuotaWait),
|
|
377
|
-
"openai-completions": route(openAICompletionsApi(), config, onQuotaWait),
|
|
378
|
-
"google-generative-ai": route(googleGenerativeAIApi(), config, onQuotaWait),
|
|
386
|
+
"anthropic-messages": route(anthropicMessagesApi(), config, compactionSignals, onQuotaWait),
|
|
387
|
+
"openai-responses": route(openAIResponsesApi(), config, compactionSignals, onQuotaWait),
|
|
388
|
+
"openai-completions": route(openAICompletionsApi(), config, compactionSignals, onQuotaWait),
|
|
389
|
+
"google-generative-ai": route(googleGenerativeAIApi(), config, compactionSignals, onQuotaWait),
|
|
379
390
|
},
|
|
380
391
|
});
|
|
381
392
|
const refresh = provider.refreshModels;
|