pi-cliproxyapi-native 0.1.4 → 0.1.6
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 +8 -4
- package/package.json +4 -4
- package/src/catalog.ts +13 -6
- package/src/media.ts +19 -10
- package/src/picker.ts +28 -24
- package/src/provider.ts +6 -2
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
|
-
This release targets Pi 0.
|
|
9
|
+
This release targets Pi 1.0.4 and CLIProxyAPI 8.0.18, 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.6
|
|
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.6`. 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
|
|
|
@@ -194,6 +194,8 @@ Replace `<canonical-model-id>` with an ID from Pi's Anthropic catalog. Reference
|
|
|
194
194
|
|
|
195
195
|
A `vertex/` or `antigravity/` route can reuse its unprefixed ID's alias if that reference resolves to one metadata entry. An alias for the full prefixed ID takes precedence. Custom prefixes need an explicit chat alias. Arbitrary aliases may lack ID-specific adapter behavior; canonical Gemini IDs and the two recognized prefixes retain native thinking and tool-turn handling.
|
|
196
196
|
|
|
197
|
+
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 `antigravity/` or `vertex/` routes. 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.
|
|
198
|
+
|
|
197
199
|
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.
|
|
198
200
|
|
|
199
201
|
## Backend routes
|
|
@@ -213,6 +215,8 @@ Set the CLIProxyAPI auth record's top-level `prefix` to `vertex` or `antigravity
|
|
|
213
215
|
|
|
214
216
|
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.
|
|
215
217
|
|
|
218
|
+
The `-high` in Antigravity model IDs belongs to the request ID. Pi's `:high` suffix sets thinking effort. For example, `cliproxyapi/antigravity/claude-opus-5-5-high:high` sends `antigravity/claude-opus-5-5-high` with high effort.
|
|
219
|
+
|
|
216
220
|
Child loading depends on the [pi-subagents extension settings](https://github.com/nicobailon/pi-subagents/blob/main/docs/agents.md#tool-and-extension-selection). Local foreground children can inherit providers from the parent. Background children load extensions through discovery or an allowlist. A saved catalog alone does not register this provider.
|
|
217
221
|
|
|
218
222
|
For native roles that need explicit loading, add this extension to `subagentOnlyExtensions`. Merge this example into `~/.pi/agent/settings.json`, replacing the path and role name while preserving existing entries:
|
|
@@ -237,7 +241,7 @@ Claude uses the legacy fine-grained tool-streaming header in place of eager tool
|
|
|
237
241
|
|
|
238
242
|
OpenAI images POST `model`, `prompt`, and `n=1` to `/v1/images/generations`, without `response_format`. xAI uses the same route with `response_format: "b64_json"`. Gemini uses `/v1beta/models/{id}:generateContent`. Media generation sends the prompt and generation options without chat history, system instructions, or tools.
|
|
239
243
|
|
|
240
|
-
OpenAI image POSTs use a dedicated HTTP connection
|
|
244
|
+
OpenAI image POSTs use a dedicated HTTP connection and preserve Node's configured proxy routing. They disable socket timeouts so the ten-minute deadline controls the request, request identity encoding, and reject compressed responses.
|
|
241
245
|
|
|
242
246
|
The implementation follows CLIProxyAPI v7.3.1's [OpenAI image handler](https://github.com/router-for-me/CLIProxyAPI/blob/v7.3.1/sdk/api/handlers/openai/openai_images_handlers.go) and [Codex image executor](https://github.com/router-for-me/CLIProxyAPI/blob/v7.3.1/internal/runtime/executor/codex_openai_images.go). The xAI and Google routes follow v7.2.158's [video handler](https://github.com/router-for-me/CLIProxyAPI/blob/v7.2.158/sdk/api/handlers/openai/openai_videos_handlers.go), [Gemini handler](https://github.com/router-for-me/CLIProxyAPI/blob/v7.2.158/sdk/api/handlers/gemini/gemini_handlers.go), and [Vertex executor](https://github.com/router-for-me/CLIProxyAPI/blob/v7.2.158/internal/runtime/executor/gemini_vertex_executor.go). See [xAI's video documentation](https://docs.x.ai/developers/model-capabilities/video/generation) for duration and status semantics.
|
|
243
247
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cliproxyapi-native",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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": "^0.
|
|
43
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
44
|
-
"@earendil-works/pi-tui": "^0.
|
|
42
|
+
"@earendil-works/pi-ai": "^1.0.4",
|
|
43
|
+
"@earendil-works/pi-coding-agent": "^1.0.4",
|
|
44
|
+
"@earendil-works/pi-tui": "^1.0.4",
|
|
45
45
|
"@types/node": "^24.13.4",
|
|
46
46
|
"typescript": "5.9.3"
|
|
47
47
|
},
|
package/src/catalog.ts
CHANGED
|
@@ -171,9 +171,17 @@ const owners: Readonly<Record<string, string>> = {
|
|
|
171
171
|
"gemini-cli": "google",
|
|
172
172
|
};
|
|
173
173
|
|
|
174
|
+
// Built-in aliases for advertised IDs without native metadata; exact metadata still wins.
|
|
175
|
+
const metadataFallbacks = new Map([
|
|
176
|
+
["claude-opus-5-5-high", "anthropic/claude-opus-5-5"],
|
|
177
|
+
["claude-sonnet-5-5-high", "anthropic/claude-sonnet-5-5"],
|
|
178
|
+
]);
|
|
179
|
+
|
|
174
180
|
export function mapCatalog(value: unknown, config: Config, known: readonly Model<Api>[]) {
|
|
175
181
|
const models = new Map<string, Model<CpaApi>>();
|
|
176
182
|
const skipped = new Set<string>();
|
|
183
|
+
const byId = Map.groupBy(known, (model) => model.id);
|
|
184
|
+
const byReference = Map.groupBy(known, (model) => `${model.provider}/${model.id}`);
|
|
177
185
|
for (const entry of parseCatalog(value)) {
|
|
178
186
|
if (entry.hidden || models.has(entry.id)) continue;
|
|
179
187
|
if (mediaPurpose(entry.id)) {
|
|
@@ -181,16 +189,15 @@ export function mapCatalog(value: unknown, config: Config, known: readonly Model
|
|
|
181
189
|
continue;
|
|
182
190
|
}
|
|
183
191
|
const { metadataId, backend } = modelRoute(entry.id);
|
|
192
|
+
const routed = !entry.id.includes("/") || backend;
|
|
184
193
|
const alias = Object.hasOwn(config.aliases, entry.id)
|
|
185
194
|
? config.aliases[entry.id]
|
|
186
195
|
: backend && Object.hasOwn(config.aliases, metadataId)
|
|
187
196
|
? config.aliases[metadataId]
|
|
188
|
-
:
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
: (!entry.id.includes("/") || backend) && model.id === metadataId,
|
|
193
|
-
);
|
|
197
|
+
: routed && !byId.has(metadataId)
|
|
198
|
+
? metadataFallbacks.get(metadataId)
|
|
199
|
+
: undefined;
|
|
200
|
+
const candidates = (alias ? byReference.get(alias) : routed ? byId.get(metadataId) : undefined) ?? [];
|
|
194
201
|
const families = new Set(
|
|
195
202
|
candidates.map((model) => (model.provider === "openai-codex" ? "openai" : model.provider)),
|
|
196
203
|
);
|
package/src/media.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import * as http from "node:http";
|
|
3
|
+
import * as https from "node:https";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { addAbortSignal, Readable } from "node:stream";
|
|
6
6
|
import { type ImageContent, type Static, type TextContent, Type } from "@earendil-works/pi-ai";
|
|
@@ -8,7 +8,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
8
8
|
import { mapMediaCatalog, mediaCapability, mediaControls } from "./catalog.ts";
|
|
9
9
|
import { type Config, isRecord, PROVIDER_ID } from "./config.ts";
|
|
10
10
|
import { type DefaultsContext, effectiveMediaDefaults, resolveMediaModel } from "./media-defaults.ts";
|
|
11
|
-
import { fetchCatalog, validateKey } from "./provider.ts";
|
|
11
|
+
import { deadline, fetchCatalog, validateKey } from "./provider.ts";
|
|
12
12
|
|
|
13
13
|
type MediaContext = Pick<ExtensionContext, "cwd" | "model"> &
|
|
14
14
|
DefaultsContext & {
|
|
@@ -102,10 +102,6 @@ function mediaOutputOptions(
|
|
|
102
102
|
return options;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
function deadline(signal: AbortSignal | undefined, milliseconds: number) {
|
|
106
|
-
return AbortSignal.any([...(signal ? [signal] : []), AbortSignal.timeout(milliseconds)]);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
105
|
export async function mediaKey(ctx: Pick<MediaContext, "modelRegistry">, signal: AbortSignal) {
|
|
110
106
|
signal.throwIfAborted();
|
|
111
107
|
const cancelled = Promise.withResolvers<never>();
|
|
@@ -133,12 +129,25 @@ function openaiImageResponse(config: Config, key: string, body: object, signal:
|
|
|
133
129
|
return new Promise<{ ok: boolean; status: number; body: ReturnType<typeof Readable.toWeb> | null }>(
|
|
134
130
|
(resolve, reject) => {
|
|
135
131
|
const url = new URL(`${config.baseUrl}/v1/images/generations`);
|
|
136
|
-
|
|
137
|
-
const
|
|
132
|
+
const transport = url.protocol === "https:" ? https : http;
|
|
133
|
+
const options = "options" in transport.globalAgent ? transport.globalAgent.options : undefined;
|
|
134
|
+
// Preserve configured proxies without inheriting the global agent's socket timeout.
|
|
135
|
+
const agent = new transport.Agent({
|
|
136
|
+
...(isRecord(options) ? options : {}),
|
|
137
|
+
keepAlive: false,
|
|
138
|
+
timeout: 0,
|
|
139
|
+
});
|
|
140
|
+
const connect = agent.createConnection.bind(agent);
|
|
141
|
+
agent.createConnection = (connectionOptions, callback) => {
|
|
142
|
+
// Node assigns HTTPS proxy sockets to the request only after CONNECT completes.
|
|
143
|
+
const socket = connect(connectionOptions, callback);
|
|
144
|
+
return socket ? addAbortSignal(signal, socket) : socket;
|
|
145
|
+
};
|
|
146
|
+
const request = transport.request(
|
|
138
147
|
url,
|
|
139
148
|
{
|
|
140
149
|
method: "POST",
|
|
141
|
-
agent
|
|
150
|
+
agent,
|
|
142
151
|
timeout: 0,
|
|
143
152
|
signal,
|
|
144
153
|
headers: {
|
package/src/picker.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
type MediaDefaults,
|
|
17
17
|
readMediaDefaults,
|
|
18
18
|
} from "./media-defaults.ts";
|
|
19
|
-
import { fetchCatalog } from "./provider.ts";
|
|
19
|
+
import { deadline, fetchCatalog } from "./provider.ts";
|
|
20
20
|
|
|
21
21
|
export function pickerCatalog(value: unknown) {
|
|
22
22
|
const rows = new Map<
|
|
@@ -58,6 +58,10 @@ export function searchPickerItems(items: readonly PickerItem[], query: string) {
|
|
|
58
58
|
);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
function clearedPurpose(choice: string) {
|
|
62
|
+
return choice === "clear image" ? "image" : choice === "clear video" ? "video" : undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
61
65
|
function defaultsLabel(defaults: MediaDefaults, automatic?: string) {
|
|
62
66
|
const label = (id?: string | null) => (id === null ? "invalid / disabled" : id ? routedName(id) : "none");
|
|
63
67
|
return `Image: ${label(defaults.image)}${automatic ? " (automatic default)" : ""} | Video: ${label(defaults.video)}`;
|
|
@@ -207,6 +211,8 @@ export class ModelPicker extends Container implements Focusable {
|
|
|
207
211
|
}
|
|
208
212
|
|
|
209
213
|
export function registerModelPicker(pi: ExtensionAPI, config: Config) {
|
|
214
|
+
let commandSequence = 0;
|
|
215
|
+
const savedSequence = { image: 0, video: 0 };
|
|
210
216
|
pi.registerCommand("cli:model", {
|
|
211
217
|
description: "Search CLIProxyAPI image/video models and select a session media default",
|
|
212
218
|
getArgumentCompletions: (prefix) =>
|
|
@@ -214,6 +220,7 @@ export function registerModelPicker(pi: ExtensionAPI, config: Config) {
|
|
|
214
220
|
.filter((value) => value.startsWith(prefix))
|
|
215
221
|
.map((value) => ({ value, label: value })),
|
|
216
222
|
async handler(args, ctx) {
|
|
223
|
+
const sequence = ++commandSequence;
|
|
217
224
|
const report = (content: string, error = false) => {
|
|
218
225
|
if (ctx.hasUI) ctx.ui.notify(content, error ? "error" : "info");
|
|
219
226
|
else if (ctx.mode === "print") console.error(content);
|
|
@@ -225,39 +232,41 @@ export function registerModelPicker(pi: ExtensionAPI, config: Config) {
|
|
|
225
232
|
};
|
|
226
233
|
try {
|
|
227
234
|
const input = args.trim();
|
|
228
|
-
const clear = input === "clear image" ? "image" : input === "clear video" ? "video" : undefined;
|
|
229
235
|
const save = (choice: string) => {
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
236
|
+
const clearing = clearedPurpose(choice);
|
|
237
|
+
const capability = clearing ? undefined : mediaCapability(choice);
|
|
238
|
+
const purpose = clearing ?? capability?.purpose;
|
|
239
|
+
if (!purpose || capability?.disabledReason)
|
|
240
|
+
throw new Error("Unsupported CLIProxyAPI media selection.");
|
|
241
|
+
ctx.signal?.throwIfAborted();
|
|
242
|
+
if (sequence < savedSequence[purpose]) {
|
|
243
|
+
report(`CLIProxyAPI ${purpose} choice superseded by a newer command; no change saved.`);
|
|
244
|
+
return;
|
|
238
245
|
}
|
|
246
|
+
const defaults = readMediaDefaults(config, ctx);
|
|
247
|
+
if (clearing) delete defaults[purpose];
|
|
248
|
+
else defaults[purpose] = choice;
|
|
239
249
|
pi.appendEntry(MEDIA_DEFAULTS_ENTRY, { version: 1, endpoint: config.baseUrl, defaults });
|
|
250
|
+
savedSequence[purpose] = sequence;
|
|
240
251
|
const effective = effectiveMediaDefaults(config, ctx);
|
|
241
252
|
report(defaultsLabel(effective.defaults, effective.automatic));
|
|
242
253
|
};
|
|
243
|
-
if (
|
|
244
|
-
save(
|
|
254
|
+
if (clearedPurpose(input)) {
|
|
255
|
+
save(input);
|
|
245
256
|
return;
|
|
246
257
|
}
|
|
247
258
|
if (input.startsWith("clear")) throw new Error("Usage: /cli:model clear image|video");
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
: undefined;
|
|
259
|
+
const explicit = input.startsWith("select ") ? input.slice(7).trim() : undefined;
|
|
260
|
+
const disabledReason = explicit === undefined ? undefined : mediaCapability(explicit)?.disabledReason;
|
|
251
261
|
if (disabledReason) {
|
|
252
262
|
report(disabledReason, true);
|
|
253
263
|
return;
|
|
254
264
|
}
|
|
255
|
-
const signal =
|
|
265
|
+
const signal = deadline(ctx.signal, 15000);
|
|
256
266
|
const key = await mediaKey(ctx, signal);
|
|
257
267
|
const rows = pickerCatalog(await fetchCatalog(config, key, signal));
|
|
258
268
|
const { defaults, automatic } = effectiveMediaDefaults(config, ctx);
|
|
259
269
|
const items = pickerItems(rows, defaults);
|
|
260
|
-
const explicit = input.startsWith("select ") ? input.slice(7).trim() : undefined;
|
|
261
270
|
const query = input === "list" ? "" : input.startsWith("search ") ? input.slice(7) : input;
|
|
262
271
|
let choice = explicit;
|
|
263
272
|
if (choice === undefined) {
|
|
@@ -291,14 +300,9 @@ export function registerModelPicker(pi: ExtensionAPI, config: Config) {
|
|
|
291
300
|
}
|
|
292
301
|
}
|
|
293
302
|
if (choice === undefined) return;
|
|
294
|
-
if (choice
|
|
295
|
-
save(choice);
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
const row = rows.find((row) => row.id === choice);
|
|
299
|
-
if (!row?.supported)
|
|
303
|
+
if (!clearedPurpose(choice) && !rows.find((row) => row.id === choice)?.supported)
|
|
300
304
|
throw new Error("Model unavailable or unsupported. Use /cli:model to list supported IDs.");
|
|
301
|
-
save(
|
|
305
|
+
save(choice);
|
|
302
306
|
} catch {
|
|
303
307
|
report(
|
|
304
308
|
"CLIProxyAPI model selection failed: unavailable/unsupported model, invalid command, authentication, or connection error. Check /login cliproxyapi. Use /cli:model list, select <exact ID>, or clear image|video.",
|
package/src/provider.ts
CHANGED
|
@@ -36,9 +36,13 @@ export function validateKey(key: string) {
|
|
|
36
36
|
return key;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export function deadline(signal: AbortSignal | undefined, milliseconds: number) {
|
|
40
|
+
return AbortSignal.any([...(signal ? [signal] : []), AbortSignal.timeout(milliseconds)]);
|
|
41
|
+
}
|
|
42
|
+
|
|
39
43
|
export async function fetchCatalog(config: Config, key: string, signal: AbortSignal) {
|
|
40
44
|
validateKey(key);
|
|
41
|
-
const boundedSignal =
|
|
45
|
+
const boundedSignal = deadline(signal, 10000);
|
|
42
46
|
let response: Response;
|
|
43
47
|
try {
|
|
44
48
|
response = await fetch(`${config.baseUrl}/v1/models`, {
|
|
@@ -150,7 +154,7 @@ function route(streams: ProviderStreams, baseUrl: string): ProviderStreams {
|
|
|
150
154
|
? (event) => options.onProviderStreamEvent?.(event, model)
|
|
151
155
|
: undefined,
|
|
152
156
|
});
|
|
153
|
-
if (!
|
|
157
|
+
if (!google) return run();
|
|
154
158
|
return lazyStream(model, async () => ({
|
|
155
159
|
async *[Symbol.asyncIterator]() {
|
|
156
160
|
for await (const event of run()) {
|