pi-cliproxyapi-native 0.3.0 → 0.3.2
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 +86 -39
- package/extensions/index.ts +35 -26
- package/package.json +5 -5
- package/src/catalog.ts +122 -13
- package/src/config.ts +11 -0
- package/src/media.ts +687 -136
- package/src/picker.ts +15 -3
- package/src/provider.ts +38 -59
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Pi CLIProxyAPI Native
|
|
2
2
|
|
|
3
|
-
Use [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI) for chat, image generation, and video generation in [Pi](https://pi.dev).
|
|
3
|
+
Use [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI) for chat, image generation and editing, and video generation in [Pi](https://pi.dev).
|
|
4
4
|
|
|
5
5
|
Chat uses Pi's native API adapters and model metadata, with availability from your proxy. Image and video tools have separate model selections, so you can generate media without switching your chat model.
|
|
6
6
|
|
|
@@ -12,6 +12,9 @@ Chat uses Pi's native API adapters and model metadata, with availability from yo
|
|
|
12
12
|
- [Choose a media model](#choose-a-media-model)
|
|
13
13
|
- [Generate media](#generate-media)
|
|
14
14
|
- [Resolution and aspect ratio](#resolution-and-aspect-ratio)
|
|
15
|
+
- [OpenAI image controls and editing](#openai-image-controls-and-editing)
|
|
16
|
+
- [Image usage and cost](#image-usage-and-cost)
|
|
17
|
+
- [Pi image models](#pi-image-models)
|
|
15
18
|
- [Supported media models](#supported-media-models)
|
|
16
19
|
- [Files, status, and limits](#files-status-and-limits)
|
|
17
20
|
- [Chat models](#chat-models)
|
|
@@ -28,15 +31,15 @@ Chat uses Pi's native API adapters and model metadata, with availability from yo
|
|
|
28
31
|
|
|
29
32
|
## Install
|
|
30
33
|
|
|
31
|
-
The extension targets Pi 1.4.0 and CLIProxyAPI 8.0.
|
|
34
|
+
The extension targets Pi 1.4.0 and CLIProxyAPI 8.0.48, 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.
|
|
32
35
|
|
|
33
36
|
If you installed `pi-cliproxy-native`, remove it before installing the renamed package. Both register the same `cliproxyapi` provider.
|
|
34
37
|
|
|
35
38
|
```sh
|
|
36
|
-
pi install npm:pi-cliproxyapi-native@0.3.
|
|
39
|
+
pi install npm:pi-cliproxyapi-native@0.3.2
|
|
37
40
|
```
|
|
38
41
|
|
|
39
|
-
For Git installation, use `pi install git:github.com/hawkff/pi-cliproxyapi-native@v0.3.
|
|
42
|
+
For Git installation, use `pi install git:github.com/hawkff/pi-cliproxyapi-native@v0.3.2`. For a local checkout, use `pi install /path/to/pi-cliproxyapi-native`.
|
|
40
43
|
|
|
41
44
|
The default proxy address is `http://localhost:8317`. For another address, set the [connection](#connection) before logging in. Restart Pi, then run:
|
|
42
45
|
|
|
@@ -62,10 +65,10 @@ Set `CLIPROXYAPI_BASE_URL` or create `~/.pi/agent/pi-cliproxyapi.json`:
|
|
|
62
65
|
}
|
|
63
66
|
```
|
|
64
67
|
|
|
65
|
-
| Setting
|
|
66
|
-
|
|
|
67
|
-
| `CLIPROXYAPI_BASE_URL`
|
|
68
|
-
| `CLIPROXYAPI_API_KEY`
|
|
68
|
+
| Setting | Behavior |
|
|
69
|
+
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
70
|
+
| `CLIPROXYAPI_BASE_URL` | Overrides `baseUrl` in the configuration file. |
|
|
71
|
+
| `CLIPROXYAPI_API_KEY` | Supplies a client key when Pi has no stored login credential. |
|
|
69
72
|
| `CLIPROXYAPI_MANAGEMENT_KEY` | Enables live Claude quota lookup through the same proxy's management API. Keep this separate from the client key. |
|
|
70
73
|
|
|
71
74
|
Use an absolute URL. Root URLs and URLs ending in `/v1` or `/v1beta` work; the extension preserves path prefixes. Remote endpoints require HTTPS. HTTP works on loopback addresses. URLs cannot contain credentials, query strings, or fragments.
|
|
@@ -92,6 +95,8 @@ Search by friendly name, exact ID, backend, owner, or image/video purpose. The p
|
|
|
92
95
|
|
|
93
96
|
The terminal picker is keyboard-only. Type to search, use the selection keys to move, press Enter to select, or Escape to cancel. It respects custom selection keybindings. Mouse clicks and wheel events do not change selection.
|
|
94
97
|
|
|
98
|
+
Starting session or branch navigation discards pending picker commands, even if another extension later cancels navigation. Run the selection again if you stay on the same branch.
|
|
99
|
+
|
|
95
100
|
RPC, print, and JSON modes return text instead of opening the picker. Print mode uses stderr, JSON mode emits custom-message events, and RPC sends notifications. Listings show up to 100 matches; narrow larger lists with `search`. Use `select <exact ID>` to choose a model in these modes.
|
|
96
101
|
|
|
97
102
|
### Generate media
|
|
@@ -105,12 +110,12 @@ Generate a 1-second video of a red circle moving left with grok-imagine-video.
|
|
|
105
110
|
Check video status for request_id <returned-id>.
|
|
106
111
|
```
|
|
107
112
|
|
|
108
|
-
| Tool
|
|
109
|
-
|
|
|
110
|
-
| `cliproxyapi_media_models`
|
|
111
|
-
| `cliproxyapi_generate_image` | `prompt`; optional `model`, `size`, `resolution`, `aspect_ratio
|
|
112
|
-
| `cliproxyapi_generate_video` | `prompt`; optional `model`, `resolution`, `aspect_ratio`, and integer `duration` from 1 to 15 seconds
|
|
113
|
-
| `cliproxyapi_video_status`
|
|
113
|
+
| Tool | Arguments | Result |
|
|
114
|
+
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
115
|
+
| `cliproxyapi_media_models` | None | Available image/video IDs, supported output controls, and effective defaults. |
|
|
116
|
+
| `cliproxyapi_generate_image` | `prompt`; optional `model`, `size`, `resolution`, `aspect_ratio`, and [OpenAI controls and editing inputs](#openai-image-controls-and-editing) | Saved images and paths, plus previews when the chat model supports images. |
|
|
117
|
+
| `cliproxyapi_generate_video` | `prompt`; optional `model`, `resolution`, `aspect_ratio`, and integer `duration` from 1 to 15 seconds | A `request_id` for the submitted video. |
|
|
118
|
+
| `cliproxyapi_video_status` | `request_id` | One status check: pending, completed with a URL, or failed. |
|
|
114
119
|
|
|
115
120
|
Pass `model` to choose an exact ID for one tool call. Otherwise, the tool uses its saved `/cli:model` selection. Without either, eligible OpenAI/GPT chats default to `gpt-image-2.5-sunburst` for images. Other chats need an image selection or explicit ID. Videos need a selection or explicit ID.
|
|
116
121
|
|
|
@@ -124,12 +129,12 @@ Invalid or disabled saved selections block automatic fallback until you clear or
|
|
|
124
129
|
|
|
125
130
|
Pass output controls for each generation, or omit them to keep the backend defaults. `cliproxyapi_media_models` lists each model's `controls.size`, `controls.resolution`, and `controls.aspect_ratio`. Empty lists mean the control is unsupported; `size_limits` describes custom pixel sizes beyond the listed presets.
|
|
126
131
|
|
|
127
|
-
| Models
|
|
128
|
-
|
|
|
129
|
-
| OpenAI images | `size`: `auto` or `WIDTHxHEIGHT`. No separate `resolution` or `aspect_ratio`.
|
|
130
|
-
| Gemini images | `aspect_ratio` and model-specific `resolution` tiers.
|
|
131
|
-
| xAI images
|
|
132
|
-
| xAI videos
|
|
132
|
+
| Models | Controls |
|
|
133
|
+
| ------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
134
|
+
| OpenAI images | `size`: `auto` or `WIDTHxHEIGHT`. No separate `resolution` or `aspect_ratio`. |
|
|
135
|
+
| Gemini images | `aspect_ratio` and model-specific `resolution` tiers. |
|
|
136
|
+
| xAI images | `resolution`: `1k` or `2k`, plus `aspect_ratio`. |
|
|
137
|
+
| xAI videos | `resolution`: `480p` or `720p`, plus `aspect_ratio`. The 1.5 model and its preview alias also accept `1080p`. |
|
|
133
138
|
|
|
134
139
|
GPT Image 1.5 accepts `auto`, `1024x1024`, `1536x1024`, and `1024x1536`. GPT Image 2 and 2.5 also accept custom dimensions: both edges must be multiples of 16 and at most 3840 pixels. The longer edge cannot exceed three times the shorter one. Total pixels must be between 655,360 and 8,294,400. OpenAI marks resolutions above `2560x1440` as experimental.
|
|
135
140
|
|
|
@@ -147,17 +152,57 @@ Larger outputs can increase generation cost and latency. The extension saves the
|
|
|
147
152
|
|
|
148
153
|
Control values follow the [OpenAI image guide](https://developers.openai.com/api/docs/guides/image-generation#customize-image-output), [Gemini image guide](https://ai.google.dev/gemini-api/docs/generate-content/image-generation#aspect_ratios_and_image_size), and xAI's [image](https://docs.x.ai/developers/model-capabilities/images/generation#configuration) and [video](https://docs.x.ai/developers/model-capabilities/video/generation#configuration) documentation, restricted to what CLIProxyAPI forwards.
|
|
149
154
|
|
|
155
|
+
### OpenAI image controls and editing
|
|
156
|
+
|
|
157
|
+
Use `cliproxyapi_media_models` to inspect controls for the exact advertised model ID. Omitted options keep the backend defaults. The extension rejects unsupported values before contacting the proxy.
|
|
158
|
+
|
|
159
|
+
| Option | Supported values |
|
|
160
|
+
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
161
|
+
| `quality` | `auto`, `low`, `medium`, `high`. Sunburst and Flare also accept `xhigh` and `max`. |
|
|
162
|
+
| `background` | `auto`, `opaque`, `transparent`. Transparent output requires PNG or WebP; GPT Image 2 transparency is in preview upstream. |
|
|
163
|
+
| `output_format` | `png`, `jpeg`, `webp`. JPEG and WebP can reduce file size and help keep results within the response limit. |
|
|
164
|
+
| `output_compression` | Integer from 0 to 100, with an explicit `jpeg` or `webp` format. Zero is a valid value. |
|
|
165
|
+
| `moderation` | `auto` or `low`. OpenAI still applies its content policies. |
|
|
166
|
+
| `n` | Integer from 1 to 10 on an explicit `api/` image route. Other routes allow only 1 because subscription translation can drop the count. |
|
|
167
|
+
| `partial_images` | Integer from 0 to 3. The proxy streams up to that many draft frames while the image renders, and Pi shows them as tool progress. Requires `n` of 1. Each draft adds about 100 image output tokens. |
|
|
168
|
+
| `images` | One to 16 local PNG, JPEG, or WebP paths. Supplying these selects `/v1/images/edits` instead of generation. |
|
|
169
|
+
| `mask` | Optional local PNG mask. The first image must also be PNG with matching dimensions. The mask needs a grayscale-alpha or RGBA alpha channel; transparent regions mark edits. |
|
|
170
|
+
| `input_fidelity` | `low` or `high`, for GPT Image 1.5 editing only. Omit it for other models. |
|
|
171
|
+
|
|
172
|
+
Editing uploads the files you name to the configured proxy. Paths can be absolute or relative to the working directory. The extension accepts regular files, checks file signatures, and limits uploads to 14 MiB per file and 20 MiB total including the mask. It does not fetch input URLs, resize inputs, or modify source files. OpenAI performs full image decoding and validates the edit. These local limits keep base64 uploads bounded and may be lower than upstream limits.
|
|
173
|
+
|
|
174
|
+
```text
|
|
175
|
+
Generate two images with api/gpt-image-2.5-sunburst at 1536x864, quality max, output_format webp, output_compression 85.
|
|
176
|
+
Edit ./photo.png with api/gpt-image-2.5-flare: replace the background with a garden, using ./mask.png as the mask.
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
All returned images share the existing 32 MiB response limit and 4 MiB preview budget. Streamed requests allow another 32 MiB for each draft frame. The extension saves only the final image and drops drafts it cannot decode. Larger sizes, higher quality, and multiple images can increase cost and latency. Use fewer images or JPEG/WebP if the response exceeds the limit. Generation and editing do not retry after a failure.
|
|
180
|
+
|
|
181
|
+
The extension supports the verified options above, not a general OpenAI API passthrough. Remote file IDs, annotation markup, audio, batches, and realtime APIs are not exposed. Adding an upstream API key does not enable endpoints that the proxy or extension cannot handle.
|
|
182
|
+
|
|
183
|
+
### Image usage and cost
|
|
184
|
+
|
|
185
|
+
OpenAI image responses report token usage. The image tool returns it to Pi, so image calls count toward the session totals in the footer and `/session`.
|
|
186
|
+
|
|
187
|
+
Costs are estimates at OpenAI list prices per million tokens: $5 for text input, $8 for image input, and $30 for image output on GPT Image 2 and 2.5 Sunburst and Flare. GPT Image 1.5 output costs $32. The plain `gpt-image-2.5` subscription model has no published price, so it reports tokens at zero cost. Subscription routes show the same estimates when the proxy reports usage, even though the plan covers them. Google and xAI image results report no usage.
|
|
188
|
+
|
|
189
|
+
### Pi image models
|
|
190
|
+
|
|
191
|
+
When Pi refreshes the catalog, at interactive startup or through `/cliproxyapi-refresh`, the advertised image models also register as Pi image models. Codemode scripts list them with `models.getAvailableOfType("image", "cliproxyapi")` and call `models.generateImages()`, and other extensions can use `ctx.modelRegistry.generateImages()`.
|
|
192
|
+
|
|
193
|
+
These calls send the prompt with backend defaults. Pi's image interface has no size, quality, count, mask, or preview options. Image blocks in the input select OpenAI editing, under the same 16-image, 14 MiB per image, and 20 MiB total limits. Google and xAI models accept text only. Results arrive as image blocks with usage and are not saved to disk. Use `cliproxyapi_generate_image` when you need controls, masks, or saved files.
|
|
194
|
+
|
|
150
195
|
### Supported media models
|
|
151
196
|
|
|
152
197
|
Your proxy must advertise the exact ID for a model to be available.
|
|
153
198
|
|
|
154
|
-
| Provider
|
|
155
|
-
|
|
|
156
|
-
| OpenAI models | `gpt-image-2.5-flare`, `gpt-image-2.5-sunburst`, `gpt-image-2.5`, `gpt-image-2`, `gpt-image-1.5`
|
|
157
|
-
| Google models | `gemini-2.5-flash-image`, `gemini-3.1-flash-image`, `gemini-3-pro-image`, `gemini-3.1-flash-lite-image`
|
|
158
|
-
| xAI models
|
|
199
|
+
| Provider | Models |
|
|
200
|
+
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
201
|
+
| OpenAI models | `gpt-image-2.5-flare`, `gpt-image-2.5-sunburst`, `gpt-image-2.5`, `gpt-image-2`, `gpt-image-1.5` |
|
|
202
|
+
| Google models | `gemini-2.5-flash-image`, `gemini-3.1-flash-image`, `gemini-3-pro-image`, `gemini-3.1-flash-lite-image` |
|
|
203
|
+
| 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` |
|
|
159
204
|
|
|
160
|
-
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
|
|
205
|
+
Supported media IDs can also use advertised `api/` and `oauth/` credential routes. OpenAI's API has no plain `gpt-image-2.5`, so the picker disables `api/gpt-image-2.5` and points to Sunburst or Flare; subscription routes keep it. 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 non-OpenAI editing are unsupported.
|
|
161
206
|
|
|
162
207
|
The extension disables these retired Imagen IDs, including their supported prefixed forms:
|
|
163
208
|
|
|
@@ -179,25 +224,27 @@ Gemini responses must contain one completed candidate. The extension skips thoug
|
|
|
179
224
|
|
|
180
225
|
Video submission returns a request ID without waiting for generation. Call `cliproxyapi_video_status` with that ID for each later check. Upstream `done` maps to completed; `failed`, `expired`, and moderation-rejected results map to failed. The extension omits URLs for moderation failures. It returns successful video URLs without downloading them or sending proxy credentials to them. xAI video URLs are temporary.
|
|
181
226
|
|
|
182
|
-
| Operation
|
|
183
|
-
|
|
|
184
|
-
| Catalog fetch
|
|
185
|
-
| Media listing or video status
|
|
186
|
-
| OpenAI image generation | 10 minutes | 32 MiB |
|
|
187
|
-
| Other image generation
|
|
188
|
-
| Video submission
|
|
227
|
+
| Operation | Deadline | Response limit |
|
|
228
|
+
| ---------------------------------- | ---------- | ---------------------------------------------- |
|
|
229
|
+
| Catalog fetch | 10 seconds | 4 MiB |
|
|
230
|
+
| Media listing or video status | 15 seconds | 4 MiB for the catalog; 64 KiB for video status |
|
|
231
|
+
| OpenAI image generation or editing | 10 minutes | 32 MiB, plus 32 MiB per streamed draft |
|
|
232
|
+
| Other image generation | 3 minutes | 32 MiB |
|
|
233
|
+
| Video submission | 60 seconds | 64 KiB |
|
|
189
234
|
|
|
190
|
-
Media deadlines include authentication and network waits. Discovery and media requests reject redirects and
|
|
235
|
+
Media deadlines include input-file reads, authentication, and network waits. Discovery and media requests reject redirects. Media errors quote the `message` from an upstream JSON error, cut to 500 characters, with credentials and control characters removed. Other error bodies and discovery errors report only the HTTP status.
|
|
191
236
|
|
|
192
237
|
The media tools do not retry generation. Cancellation or timeout can leave work running upstream. Check a known video request ID before submitting another generation.
|
|
193
238
|
|
|
194
239
|
## Chat models
|
|
195
240
|
|
|
196
|
-
The extension discovers available IDs through `/v1/models` and matches them against Pi's
|
|
241
|
+
The extension discovers available IDs through `/v1/models` and matches them against Pi's Anthropic, OpenAI, OpenAI Codex, Google, and xAI metadata for limits, prices, inputs, and thinking capabilities. xAI Grok models use Pi's native Responses adapter through the proxy.
|
|
242
|
+
|
|
243
|
+
During a session, the extension also reads the provider catalogs that Pi refreshes from its catalog service. Models newer than Pi's bundled data can then register; for models Pi already bundles, the bundled metadata wins.
|
|
197
244
|
|
|
198
245
|
### Refresh
|
|
199
246
|
|
|
200
|
-
Run `/cliproxyapi-refresh` to update the chat
|
|
247
|
+
Run `/cliproxyapi-refresh` to update the chat and image catalogs. It reports both counts and lists up to 20 advertised IDs that it could not register because Pi has no metadata for them or the match is ambiguous.
|
|
201
248
|
|
|
202
249
|
### Wait for quota reset
|
|
203
250
|
|
|
@@ -331,11 +378,11 @@ Run `/reload` after changing settings and `/cliproxyapi-refresh` before starting
|
|
|
331
378
|
|
|
332
379
|
Claude uses the legacy fine-grained tool-streaming header in place of eager tool fields. The extension disables unverified deferred-tool and strict-tool capabilities. Responses function tools use `strict: null` when Pi omits strictness, so optional arguments remain optional. Explicit payload hooks retain final control.
|
|
333
380
|
|
|
334
|
-
OpenAI images POST `model`, `prompt`,
|
|
381
|
+
OpenAI images POST `model`, `prompt`, `n`, and supplied controls to `/v1/images/generations`, without `response_format`. Editing uses `/v1/images/edits` with JSON `images` and optional `mask` data URLs. A nonzero `partial_images` adds `stream: true`, and the extension reads server-sent events. Each `*.partial_image` frame becomes a progress preview, and the `*.completed` frame carries the final image and usage. 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.
|
|
335
382
|
|
|
336
|
-
OpenAI image POSTs use a dedicated HTTP connection and
|
|
383
|
+
OpenAI image POSTs use a dedicated HTTP connection. It follows `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` from the environment, where Pi also places its `httpProxy` setting, on Node 22.21+ and 24.5+. Older Node versions connect directly. The connection disables socket timeouts so the ten-minute deadline controls the request, requests identity encoding, and rejects compressed responses.
|
|
337
384
|
|
|
338
|
-
The implementation follows CLIProxyAPI
|
|
385
|
+
The OpenAI image implementation follows CLIProxyAPI v8.0.48's [image handler](https://github.com/router-for-me/CLIProxyAPI/blob/v8.0.48/sdk/api/handlers/openai/openai_images_handlers.go) and [Codex image executor](https://github.com/router-for-me/CLIProxyAPI/blob/v8.0.48/internal/runtime/executor/codex_openai_images.go), plus the OpenAI [generation](https://developers.openai.com/api/reference/resources/images/methods/generate) and [editing](https://developers.openai.com/api/reference/resources/images/methods/edit) references. 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.
|
|
339
386
|
|
|
340
387
|
## License
|
|
341
388
|
|
package/extensions/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { type ExtensionAPI, type ExtensionContext, getAgentDir } from "@earendil
|
|
|
4
4
|
import { PROVIDER_ID, parseConfig } from "../src/config.ts";
|
|
5
5
|
import { registerMediaTools } from "../src/media.ts";
|
|
6
6
|
import { registerModelPicker } from "../src/picker.ts";
|
|
7
|
-
import { createCliproxyProvider } from "../src/provider.ts";
|
|
7
|
+
import { createCliproxyProvider, metadataCatalog } from "../src/provider.ts";
|
|
8
8
|
import { registerAutoRetry } from "../src/retry.ts";
|
|
9
9
|
|
|
10
10
|
export default async function (pi: ExtensionAPI) {
|
|
@@ -37,32 +37,32 @@ export default async function (pi: ExtensionAPI) {
|
|
|
37
37
|
context = undefined;
|
|
38
38
|
waits.clear();
|
|
39
39
|
});
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
waits.set(id, seconds);
|
|
40
|
+
const provider = createCliproxyProvider(
|
|
41
|
+
config,
|
|
42
|
+
// Session context exposes Pi's refreshed catalogs, including models newer than its bundled data.
|
|
43
|
+
() => metadataCatalog(context && ((id) => context?.modelRegistry.getProvider(id)?.getModels())),
|
|
44
|
+
(id, seconds) => {
|
|
45
|
+
if (!context) return;
|
|
46
|
+
if (seconds === undefined) waits.delete(id);
|
|
47
|
+
else {
|
|
48
|
+
if (!waits.has(id)) {
|
|
49
|
+
const message = `CLIProxyAPI quota limit. Retrying at ${new Date(Date.now() + seconds * 1000).toLocaleString()}. Cancel the current request to stop waiting.`;
|
|
50
|
+
if (context.mode === "print" || context.mode === "json") process.stderr.write(`${message}\n`);
|
|
51
|
+
else context.ui.notify(message, "warning");
|
|
54
52
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
53
|
+
waits.set(id, seconds);
|
|
54
|
+
}
|
|
55
|
+
const remaining = Math.min(...waits.values());
|
|
56
|
+
context.ui.setStatus(
|
|
57
|
+
"cliproxyapi-quota",
|
|
58
|
+
waits.size
|
|
59
|
+
? `CLIProxyAPI quota: retry in ${Math.floor(remaining / 3600)}h ${Math.floor(remaining / 60) % 60}m ${remaining % 60}s`
|
|
60
|
+
: undefined,
|
|
61
|
+
);
|
|
62
|
+
},
|
|
63
|
+
compactionSignals,
|
|
65
64
|
);
|
|
65
|
+
pi.registerProvider(provider);
|
|
66
66
|
registerAutoRetry(pi);
|
|
67
67
|
registerMediaTools(pi, config);
|
|
68
68
|
registerModelPicker(pi, config);
|
|
@@ -88,7 +88,16 @@ export default async function (pi: ExtensionAPI) {
|
|
|
88
88
|
return;
|
|
89
89
|
}
|
|
90
90
|
const count = ctx.modelRegistry.getAll().filter((model) => model.provider === PROVIDER_ID).length;
|
|
91
|
-
ctx.
|
|
91
|
+
const images = ctx.modelRegistry.getModelsOfType("image", PROVIDER_ID).length;
|
|
92
|
+
const skipped = provider.skippedModels();
|
|
93
|
+
const listed = [
|
|
94
|
+
...skipped.slice(0, 20),
|
|
95
|
+
...(skipped.length > 20 ? [`${skipped.length - 20} more`] : []),
|
|
96
|
+
];
|
|
97
|
+
ctx.ui.notify(
|
|
98
|
+
`CLIProxyAPI: ${count} chat models, ${images} image models.${listed.length ? ` Not registered (missing or ambiguous Pi metadata): ${listed.join(", ")}.` : ""}`,
|
|
99
|
+
"info",
|
|
100
|
+
);
|
|
92
101
|
},
|
|
93
102
|
});
|
|
94
103
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cliproxyapi-native",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "CLIProxyAPI chat discovery and
|
|
3
|
+
"version": "0.3.2",
|
|
4
|
+
"description": "CLIProxyAPI chat discovery, image generation and editing, and video generation with Pi's native adapters",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -38,12 +38,12 @@
|
|
|
38
38
|
"check": "pnpm typecheck && pnpm lint && pnpm test"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@biomejs/biome": "^2.
|
|
41
|
+
"@biomejs/biome": "^2.6.4",
|
|
42
42
|
"@earendil-works/pi-ai": "^1.4.0",
|
|
43
43
|
"@earendil-works/pi-coding-agent": "^1.4.0",
|
|
44
44
|
"@earendil-works/pi-tui": "^1.4.0",
|
|
45
|
-
"@types/node": "^24.
|
|
46
|
-
"typescript": "
|
|
45
|
+
"@types/node": "^24.19.2",
|
|
46
|
+
"typescript": "7.0.2"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"@earendil-works/pi-ai": "*",
|
package/src/catalog.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
-
import { type Config, isModelId, isRecord, PROVIDER_ID } from "./config.ts";
|
|
2
|
+
import { type Config, deadline, isModelId, isRecord, PROVIDER_ID, validateKey } from "./config.ts";
|
|
3
3
|
|
|
4
4
|
export type CpaApi =
|
|
5
5
|
| "anthropic-messages"
|
|
@@ -34,6 +34,51 @@ export function parseCatalog(value: unknown) {
|
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export async function fetchCatalog(config: Config, key: string, signal: AbortSignal) {
|
|
38
|
+
validateKey(key);
|
|
39
|
+
const boundedSignal = deadline(signal, 10000);
|
|
40
|
+
let response: Response;
|
|
41
|
+
try {
|
|
42
|
+
response = await fetch(`${config.baseUrl}/v1/models`, {
|
|
43
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
44
|
+
redirect: "error",
|
|
45
|
+
signal: boundedSignal,
|
|
46
|
+
});
|
|
47
|
+
} catch {
|
|
48
|
+
signal.throwIfAborted();
|
|
49
|
+
throw new Error("CLIProxyAPI discovery failed: connection, timeout, or redirect error.");
|
|
50
|
+
}
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
await response.body?.cancel();
|
|
53
|
+
throw new Error(`CLIProxyAPI discovery failed (HTTP ${response.status}).`);
|
|
54
|
+
}
|
|
55
|
+
let payload: unknown;
|
|
56
|
+
const reader = response.body?.getReader();
|
|
57
|
+
if (!reader) throw new Error("CLIProxyAPI discovery returned an empty response.");
|
|
58
|
+
try {
|
|
59
|
+
const decoder = new TextDecoder();
|
|
60
|
+
let text = "";
|
|
61
|
+
let size = 0;
|
|
62
|
+
while (true) {
|
|
63
|
+
const chunk = await reader.read();
|
|
64
|
+
if (chunk.done) break;
|
|
65
|
+
size += chunk.value.byteLength;
|
|
66
|
+
if (size > 4 * 1024 * 1024) {
|
|
67
|
+
await reader.cancel();
|
|
68
|
+
throw new Error("Catalog too large.");
|
|
69
|
+
}
|
|
70
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
71
|
+
}
|
|
72
|
+
payload = JSON.parse(text + decoder.decode());
|
|
73
|
+
} catch {
|
|
74
|
+
signal.throwIfAborted();
|
|
75
|
+
throw new Error("CLIProxyAPI catalog is invalid, incomplete, or larger than 4 MiB.");
|
|
76
|
+
} finally {
|
|
77
|
+
reader.releaseLock();
|
|
78
|
+
}
|
|
79
|
+
return payload;
|
|
80
|
+
}
|
|
81
|
+
|
|
37
82
|
// Resolve metadata and capabilities without changing registry or request IDs.
|
|
38
83
|
export function modelRoute(id: string) {
|
|
39
84
|
const match = /^(vertex|antigravity|oauth|api)\/(.+)$/.exec(id);
|
|
@@ -59,17 +104,31 @@ export function routedName(id: string, name = id) {
|
|
|
59
104
|
export type MediaPurpose = "image" | "video";
|
|
60
105
|
|
|
61
106
|
const imagenRetirement = "Retired on Vertex (June 2026); disabled. Select a Nano Banana image model instead.";
|
|
107
|
+
// OpenAI list prices in USD per million tokens: text input, image input, image output.
|
|
108
|
+
const gptImage2Price = [5, 8, 30] as const;
|
|
62
109
|
|
|
63
110
|
// Exact media IDs, including retired entries; vision input alone does not imply image output.
|
|
64
111
|
const mediaModels = new Map<
|
|
65
112
|
string,
|
|
66
|
-
{
|
|
113
|
+
{
|
|
114
|
+
name: string;
|
|
115
|
+
purpose: MediaPurpose;
|
|
116
|
+
route?: "xai" | "gemini" | "openai";
|
|
117
|
+
disabledReason?: string;
|
|
118
|
+
price?: readonly [number, number, number];
|
|
119
|
+
}
|
|
67
120
|
>([
|
|
68
|
-
[
|
|
69
|
-
|
|
121
|
+
[
|
|
122
|
+
"gpt-image-2.5-flare",
|
|
123
|
+
{ name: "GPT Image 2.5 Flare", purpose: "image", route: "openai", price: gptImage2Price },
|
|
124
|
+
],
|
|
125
|
+
[
|
|
126
|
+
"gpt-image-2.5-sunburst",
|
|
127
|
+
{ name: "GPT Image 2.5 Sunburst", purpose: "image", route: "openai", price: gptImage2Price },
|
|
128
|
+
],
|
|
70
129
|
["gpt-image-2.5", { name: "GPT Image 2.5", purpose: "image", route: "openai" }],
|
|
71
|
-
["gpt-image-2", { name: "GPT Image 2", purpose: "image", route: "openai" }],
|
|
72
|
-
["gpt-image-1.5", { name: "GPT Image 1.5", purpose: "image", route: "openai" }],
|
|
130
|
+
["gpt-image-2", { name: "GPT Image 2", purpose: "image", route: "openai", price: gptImage2Price }],
|
|
131
|
+
["gpt-image-1.5", { name: "GPT Image 1.5", purpose: "image", route: "openai", price: [5, 8, 32] }],
|
|
73
132
|
["grok-imagine-image", { name: "Grok Imagine Image", purpose: "image", route: "xai" }],
|
|
74
133
|
["grok-imagine-image-quality", { name: "Grok Imagine Image Quality", purpose: "image", route: "xai" }],
|
|
75
134
|
["grok-imagine-image-2.0", { name: "Grok Imagine Image 2.0", purpose: "image", route: "xai" }],
|
|
@@ -100,11 +159,20 @@ const mediaModels = new Map<
|
|
|
100
159
|
]);
|
|
101
160
|
|
|
102
161
|
export function mediaCapability(id: string) {
|
|
103
|
-
const { metadataId, backend } = modelRoute(id);
|
|
162
|
+
const { metadataId, backend, authType } = modelRoute(id);
|
|
104
163
|
const capability = mediaModels.get(metadataId);
|
|
105
164
|
if (backend && (capability?.route === "xai" || capability?.route === "openai")) {
|
|
106
165
|
return { ...capability, route: undefined, disabledReason: `Unsupported media execution on ${backend}.` };
|
|
107
166
|
}
|
|
167
|
+
// CLIProxyAPI 8.0.48 advertises it for API keys, but OpenAI's Images API rejects the bare 2.5 name.
|
|
168
|
+
if (capability && authType === "api-key" && metadataId === "gpt-image-2.5") {
|
|
169
|
+
return {
|
|
170
|
+
...capability,
|
|
171
|
+
route: undefined,
|
|
172
|
+
disabledReason:
|
|
173
|
+
"OpenAI's API has no gpt-image-2.5 model. Use api/gpt-image-2.5-sunburst or api/gpt-image-2.5-flare.",
|
|
174
|
+
};
|
|
175
|
+
}
|
|
108
176
|
return capability;
|
|
109
177
|
}
|
|
110
178
|
|
|
@@ -114,7 +182,8 @@ export function mediaPurpose(id: string) {
|
|
|
114
182
|
|
|
115
183
|
export function mediaControls(id: string) {
|
|
116
184
|
const capability = mediaCapability(id);
|
|
117
|
-
const { metadataId } = modelRoute(id);
|
|
185
|
+
const { metadataId, authType } = modelRoute(id);
|
|
186
|
+
const openai = capability?.route === "openai";
|
|
118
187
|
const commonRatios = ["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3"];
|
|
119
188
|
let resolution: string[] = [];
|
|
120
189
|
let aspect_ratio: string[] = [];
|
|
@@ -144,9 +213,52 @@ export function mediaControls(id: string) {
|
|
|
144
213
|
: undefined,
|
|
145
214
|
resolution,
|
|
146
215
|
aspect_ratio,
|
|
216
|
+
quality: openai
|
|
217
|
+
? [
|
|
218
|
+
"auto",
|
|
219
|
+
"low",
|
|
220
|
+
"medium",
|
|
221
|
+
"high",
|
|
222
|
+
...(/^gpt-image-2\.5-(sunburst|flare)$/.test(metadataId) ? ["xhigh", "max"] : []),
|
|
223
|
+
]
|
|
224
|
+
: [],
|
|
225
|
+
background: openai ? ["auto", "opaque", "transparent"] : [],
|
|
226
|
+
output_format: openai ? ["png", "jpeg", "webp"] : [],
|
|
227
|
+
output_compression: openai ? { min: 0, max: 100, formats: ["jpeg", "webp"] } : undefined,
|
|
228
|
+
moderation: openai ? ["auto", "low"] : [],
|
|
229
|
+
// Subscription image-tool translation does not forward n.
|
|
230
|
+
n: openai ? { min: 1, max: authType === "api-key" ? 10 : 1 } : undefined,
|
|
231
|
+
partial_images: openai ? { min: 0, max: 3 } : undefined,
|
|
232
|
+
editing: openai
|
|
233
|
+
? { max_images: 16, max_file_bytes: 14 * 1024 * 1024, max_total_bytes: 20 * 1024 * 1024, mask: "png" }
|
|
234
|
+
: undefined,
|
|
235
|
+
input_fidelity: openai && metadataId === "gpt-image-1.5" ? ["low", "high"] : [],
|
|
147
236
|
};
|
|
148
237
|
}
|
|
149
238
|
|
|
239
|
+
export const IMAGE_API = "cliproxyapi-images";
|
|
240
|
+
|
|
241
|
+
// Pi image models for codemode and extensions; video stays tool-only because Pi has no video interface.
|
|
242
|
+
export function mapImageModels(value: unknown, config: Config) {
|
|
243
|
+
return mapMediaCatalog(value).flatMap(({ id, purpose, name, controls }) => {
|
|
244
|
+
if (purpose !== "image") return [];
|
|
245
|
+
const [input = 0, , output = 0] = mediaCapability(id)?.price ?? [];
|
|
246
|
+
return [
|
|
247
|
+
{
|
|
248
|
+
type: "image" as const,
|
|
249
|
+
id,
|
|
250
|
+
name,
|
|
251
|
+
api: IMAGE_API,
|
|
252
|
+
provider: PROVIDER_ID,
|
|
253
|
+
baseUrl: config.baseUrl,
|
|
254
|
+
input: controls.editing ? ["text" as const, "image" as const] : ["text" as const],
|
|
255
|
+
output: ["image" as const],
|
|
256
|
+
cost: { input, output, cacheRead: 0, cacheWrite: 0 },
|
|
257
|
+
},
|
|
258
|
+
];
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
150
262
|
export function mapMediaCatalog(value: unknown) {
|
|
151
263
|
const models = new Map<
|
|
152
264
|
string,
|
|
@@ -174,6 +286,7 @@ const owners: Readonly<Record<string, string>> = {
|
|
|
174
286
|
google: "google",
|
|
175
287
|
gemini: "google",
|
|
176
288
|
"gemini-cli": "google",
|
|
289
|
+
xai: "xai",
|
|
177
290
|
};
|
|
178
291
|
|
|
179
292
|
// Built-in aliases for advertised IDs without native metadata; exact metadata still wins.
|
|
@@ -189,11 +302,7 @@ export function mapCatalog(value: unknown, config: Config, known: readonly Model
|
|
|
189
302
|
const byId = Map.groupBy(known, (model) => model.id);
|
|
190
303
|
const byReference = Map.groupBy(known, (model) => `${model.provider}/${model.id}`);
|
|
191
304
|
for (const entry of parseCatalog(value)) {
|
|
192
|
-
if (entry.hidden || models.has(entry.id)) continue;
|
|
193
|
-
if (mediaPurpose(entry.id)) {
|
|
194
|
-
skipped.add(entry.id);
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
305
|
+
if (entry.hidden || models.has(entry.id) || mediaPurpose(entry.id)) continue;
|
|
197
306
|
const { metadataId, prefix } = modelRoute(entry.id);
|
|
198
307
|
const routed = !entry.id.includes("/") || prefix;
|
|
199
308
|
const alias = Object.hasOwn(config.aliases, entry.id)
|
package/src/config.ts
CHANGED
|
@@ -9,6 +9,17 @@ export function isModelId(value: unknown): value is string {
|
|
|
9
9
|
return typeof value === "string" && /^[!-~]{1,256}$/.test(value);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export function validateKey(key: string) {
|
|
13
|
+
if (!/^[!-~]{1,4096}$/.test(key)) {
|
|
14
|
+
throw new Error("CLIProxyAPI requires a non-empty API key without whitespace or control characters.");
|
|
15
|
+
}
|
|
16
|
+
return key;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function deadline(signal: AbortSignal | undefined, milliseconds: number) {
|
|
20
|
+
return AbortSignal.any([...(signal ? [signal] : []), AbortSignal.timeout(milliseconds)]);
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
export function normalizeBaseUrl(value: string) {
|
|
13
24
|
let url: URL;
|
|
14
25
|
try {
|