pi-cliproxyapi-native 0.1.7 → 0.1.8
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 +11 -3
- package/extensions/index.ts +27 -18
- package/package.json +4 -4
- package/src/compaction.ts +78 -0
- package/src/provider.ts +16 -6
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.8
|
|
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.8`. 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
|
|
|
@@ -201,6 +201,14 @@ Management requests stay on the configured proxy and reject redirects. Account a
|
|
|
201
201
|
|
|
202
202
|
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
203
|
|
|
204
|
+
### Compaction
|
|
205
|
+
|
|
206
|
+
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.
|
|
207
|
+
|
|
208
|
+
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.
|
|
209
|
+
|
|
210
|
+
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.
|
|
211
|
+
|
|
204
212
|
### Aliases and model limits
|
|
205
213
|
|
|
206
214
|
The chat catalog skips unknown IDs rather than guessing their capabilities. Recognized catalog owners resolve duplicate metadata IDs; ambiguous cross-family matches stay out.
|
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.8",
|
|
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
|
},
|
|
@@ -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/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,7 +177,12 @@ 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) => {
|
|
@@ -330,13 +336,17 @@ function route(streams: ProviderStreams, config: Config, onWait?: QuotaWaitHandl
|
|
|
330
336
|
},
|
|
331
337
|
}));
|
|
332
338
|
};
|
|
333
|
-
return {
|
|
339
|
+
return {
|
|
340
|
+
stream: wrap(streams.stream),
|
|
341
|
+
streamSimple: withCompactionRecovery(wrap(streams.streamSimple), compactionSignals),
|
|
342
|
+
};
|
|
334
343
|
}
|
|
335
344
|
|
|
336
345
|
export function createCliproxyProvider(
|
|
337
346
|
config: Config,
|
|
338
347
|
known: readonly Model<Api>[] = builtinCatalog(),
|
|
339
348
|
onQuotaWait?: QuotaWaitHandler,
|
|
349
|
+
compactionSignals = new WeakSet<AbortSignal>(),
|
|
340
350
|
) {
|
|
341
351
|
const standardAuth = envApiKeyAuth("CLIProxyAPI API key", ["CLIPROXYAPI_API_KEY"]);
|
|
342
352
|
const scope = createHash("sha256")
|
|
@@ -372,10 +382,10 @@ export function createCliproxyProvider(
|
|
|
372
382
|
return (await discover(config, context.credential.key, context.signal, known)).models;
|
|
373
383
|
},
|
|
374
384
|
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),
|
|
385
|
+
"anthropic-messages": route(anthropicMessagesApi(), config, compactionSignals, onQuotaWait),
|
|
386
|
+
"openai-responses": route(openAIResponsesApi(), config, compactionSignals, onQuotaWait),
|
|
387
|
+
"openai-completions": route(openAICompletionsApi(), config, compactionSignals, onQuotaWait),
|
|
388
|
+
"google-generative-ai": route(googleGenerativeAIApi(), config, compactionSignals, onQuotaWait),
|
|
379
389
|
},
|
|
380
390
|
});
|
|
381
391
|
const refresh = provider.refreshModels;
|