pi-cliproxyapi-native 0.1.3
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/LICENSE +21 -0
- package/README.md +246 -0
- package/extensions/index.ts +53 -0
- package/package.json +52 -0
- package/src/catalog.ts +248 -0
- package/src/config.ts +64 -0
- package/src/media-defaults.ts +79 -0
- package/src/media.ts +571 -0
- package/src/picker.ts +310 -0
- package/src/provider.ts +221 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pi CLIProxy Native contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# Pi CLIProxyAPI Native
|
|
2
|
+
|
|
3
|
+
Use [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI) for chat, image generation, and video generation in [Pi](https://pi.dev).
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
You need Pi 0.87.x and Node.js 22.19 or newer. Run CLIProxyAPI with your upstream accounts configured before connecting Pi. This release targets CLIProxyAPI 7.3.11. The extension adds no runtime dependencies and needs no compilation.
|
|
10
|
+
|
|
11
|
+
If you installed `pi-cliproxy-native`, remove it before installing the renamed package. Both register the same `cliproxyapi` provider.
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
pi install npm:pi-cliproxyapi-native@0.1.3
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
For Git installation, use `pi install git:github.com/hawkff/pi-cliproxyapi-native@v0.1.3`. For a local checkout, use `pi install /path/to/pi-cliproxyapi-native`.
|
|
18
|
+
|
|
19
|
+
The default proxy address is `http://localhost:8317`. For another address, set the [connection](#connection) before logging in. Restart Pi, then run:
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
/login cliproxyapi
|
|
23
|
+
/cliproxyapi-refresh
|
|
24
|
+
/model
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Enter your CLIProxyAPI client key at login. Pi validates it against the proxy before saving it. Upstream account authentication stays in CLIProxyAPI.
|
|
28
|
+
|
|
29
|
+
**Use Pi's built-in `/model` picker for chat.** Choose a `cliproxyapi/` entry there. This extension's `/cli:model` picker is only for CLIProxyAPI image and video models. Selecting a media model does not change chat or generate anything.
|
|
30
|
+
|
|
31
|
+
Enable only one extension that registers the `cliproxyapi` provider ID. This extension leaves other providers and their hand-written model lists unchanged.
|
|
32
|
+
|
|
33
|
+
## Connection
|
|
34
|
+
|
|
35
|
+
Set `CLIPROXYAPI_BASE_URL` or create `~/.pi/agent/pi-cliproxyapi.json`:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"baseUrl": "http://localhost:8317"
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| Setting | Behavior |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| `CLIPROXYAPI_BASE_URL` | Overrides `baseUrl` in the configuration file. |
|
|
46
|
+
| `CLIPROXYAPI_API_KEY` | Supplies a client key when Pi has no stored login credential. |
|
|
47
|
+
|
|
48
|
+
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.
|
|
49
|
+
|
|
50
|
+
Connection and alias settings are global. Project-local files cannot redirect your proxy key. Run `/reload` after changing these settings.
|
|
51
|
+
|
|
52
|
+
Pi stores login credentials in its auth store; the extension does not keep another copy. Stored credentials take precedence over `CLIPROXYAPI_API_KEY`. `/logout` removes the stored key but leaves environment variables unchanged.
|
|
53
|
+
|
|
54
|
+
## Images and videos
|
|
55
|
+
|
|
56
|
+
### Choose a media model
|
|
57
|
+
|
|
58
|
+
Open `/cli:model` to search the live image/video catalog, or use its subcommands:
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
/cli:model search banana
|
|
62
|
+
/cli:model list
|
|
63
|
+
/cli:model select gemini-2.5-flash-image
|
|
64
|
+
/cli:model clear image
|
|
65
|
+
/cli:model clear video
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Search by friendly name, exact ID, backend, owner, or image/video purpose. The picker shows known media models, including disabled entries with a reason. It omits chat models and unknown IDs.
|
|
69
|
+
|
|
70
|
+
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.
|
|
71
|
+
|
|
72
|
+
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.
|
|
73
|
+
|
|
74
|
+
### Generate media
|
|
75
|
+
|
|
76
|
+
Ask Pi to use the media tools:
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
List available CLIProxyAPI media models.
|
|
80
|
+
Generate an image of a red circle with grok-imagine-image.
|
|
81
|
+
Generate a 1-second video of a red circle moving left with grok-imagine-video.
|
|
82
|
+
Check video status for request_id <returned-id>.
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
| Tool | Arguments | Result |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| `cliproxyapi_media_models` | None | Available image/video IDs, supported output controls, and effective defaults. |
|
|
88
|
+
| `cliproxyapi_generate_image` | `prompt`; optional `model`, `size`, `resolution`, `aspect_ratio` | Saved images and paths, plus previews when the chat model supports images. |
|
|
89
|
+
| `cliproxyapi_generate_video` | `prompt`; optional `model`, `resolution`, `aspect_ratio`, and integer `duration` from 1 to 15 seconds | A `request_id` for the submitted video. |
|
|
90
|
+
| `cliproxyapi_video_status` | `request_id` | One status check: pending, completed with a URL, or failed. |
|
|
91
|
+
|
|
92
|
+
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.
|
|
93
|
+
|
|
94
|
+
The automatic image default applies to native OpenAI/Codex chats, recognized GPT chat IDs through other providers, and configured OpenAI metadata aliases. An OpenAI-compatible API alone does not qualify.
|
|
95
|
+
|
|
96
|
+
Pi saves image and video selections per session branch and proxy endpoint. Reload, resume, and tree navigation restore them; forks inherit them from the copied branch. New sessions have no saved selections. Clearing an image selection restores the automatic default where it applies.
|
|
97
|
+
|
|
98
|
+
Invalid or disabled saved selections block automatic fallback until you clear or replace them. Generation checks the live catalog before submitting and rejects missing or hidden IDs without choosing a replacement. Picker and media requests contact the proxy even in offline chat mode. They do not refresh Pi's chat catalog.
|
|
99
|
+
|
|
100
|
+
### Resolution and aspect ratio
|
|
101
|
+
|
|
102
|
+
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.
|
|
103
|
+
|
|
104
|
+
| Models | Controls |
|
|
105
|
+
| --- | --- |
|
|
106
|
+
| OpenAI images | `size`: `auto` or `WIDTHxHEIGHT`. No separate `resolution` or `aspect_ratio`. |
|
|
107
|
+
| Gemini images | `aspect_ratio` and model-specific `resolution` tiers. |
|
|
108
|
+
| xAI images | `resolution`: `1k` or `2k`, plus `aspect_ratio`. |
|
|
109
|
+
| xAI videos | `resolution`: `480p` or `720p`, plus `aspect_ratio`. The 1.5 model and its preview alias also accept `1080p`. |
|
|
110
|
+
|
|
111
|
+
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.
|
|
112
|
+
|
|
113
|
+
Gemini 3 Pro Image supports `1K`, `2K`, and `4K`. Gemini 3.1 Flash Image also supports `512`. Flash Lite Image supports `1K` only. Gemini 2.5 Flash Image has no resolution selector. Use the exact case shown in discovery, including uppercase `K` for Gemini and lowercase `k` for xAI.
|
|
114
|
+
|
|
115
|
+
Aspect ratios vary by model and proxy route. For example, CLIProxyAPI 7.3.11 accepts `20:9` for xAI images but drops xAI's `21:9` and `auto` values. The extension rejects unsupported controls before authentication or network access instead of substituting another size or shape.
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
Generate an image with gpt-image-2.5-sunburst at size 2048x2048.
|
|
119
|
+
Generate a 2K image at 16:9 with gemini-3-pro-image.
|
|
120
|
+
Generate a 1-second 1080p video at 9:16 with grok-imagine-video-1.5.
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Larger outputs can increase generation cost and latency. The extension saves the returned image bytes without resizing, and the existing response and preview limits still apply.
|
|
124
|
+
|
|
125
|
+
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.
|
|
126
|
+
|
|
127
|
+
### Supported media models
|
|
128
|
+
|
|
129
|
+
Your proxy must advertise the exact ID for a model to be available.
|
|
130
|
+
|
|
131
|
+
| Provider | Models |
|
|
132
|
+
| --- | --- |
|
|
133
|
+
| OpenAI models | `gpt-image-2.5-flare`, `gpt-image-2.5-sunburst`, `gpt-image-2.5`, `gpt-image-2`, `gpt-image-1.5` |
|
|
134
|
+
| Google models | `gemini-2.5-flash-image`, `gemini-3.1-flash-image`, `gemini-3-pro-image`, `gemini-3.1-flash-lite-image` |
|
|
135
|
+
| 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` |
|
|
136
|
+
|
|
137
|
+
The listed Gemini image models also support advertised `vertex/` and `antigravity/` routes. Those prefixes do not enable OpenAI or xAI media execution; the picker shows those entries as disabled. Other prefixes, media aliases, unknown media IDs, Google Veo, and editing endpoints are unsupported.
|
|
138
|
+
|
|
139
|
+
The extension disables these retired Imagen IDs, including their `vertex/` and `antigravity/` forms:
|
|
140
|
+
|
|
141
|
+
- `imagen-3.0-generate-002`
|
|
142
|
+
- `imagen-3.0-fast-generate-001`
|
|
143
|
+
- `imagen-4.0-generate-001`
|
|
144
|
+
- `imagen-4.0-fast-generate-001`
|
|
145
|
+
- `imagen-4.0-ultra-generate-001`
|
|
146
|
+
|
|
147
|
+
The picker explains their retirement, and generation rejects them before network access. Saved Imagen defaults become invalid without a fallback. Use an available Nano Banana model instead. This follows Google's [Vertex retirement notice](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/release-notes#March_24_2026) for June 30, 2026; it does not imply that every custom gateway rejects Imagen.
|
|
148
|
+
|
|
149
|
+
### Files, status, and limits
|
|
150
|
+
|
|
151
|
+
Image generation saves JPEG, PNG, or WebP files under a new `.pi/cliproxyapi-image-*/` directory in your working directory. The extension checks base64 and file signatures, uses private permissions, and does not overwrite existing files. Keep `.pi/` out of version control.
|
|
152
|
+
|
|
153
|
+
Inline previews require a vision-capable chat model and share a 4 MiB base64 budget per result. Larger images return file paths without previews. Use Pi's `read` tool to inspect saved images. URL-only image responses fail without a download.
|
|
154
|
+
|
|
155
|
+
Gemini responses must contain one completed candidate. The extension skips thought parts and saves up to 16 final images within a 256-part response limit. Safety/refusal signals, incomplete or text-only results, malformed data, MIME/signature mismatches, and excess images or parts fail before saving.
|
|
156
|
+
|
|
157
|
+
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.
|
|
158
|
+
|
|
159
|
+
| Operation | Deadline | Response limit |
|
|
160
|
+
| --- | --- | --- |
|
|
161
|
+
| Catalog fetch | 10 seconds | 4 MiB |
|
|
162
|
+
| Media listing or video status | 15 seconds | 4 MiB for the catalog; 64 KiB for video status |
|
|
163
|
+
| OpenAI image generation | 10 minutes | 32 MiB |
|
|
164
|
+
| Other image generation | 3 minutes | 32 MiB |
|
|
165
|
+
| Video submission | 60 seconds | 64 KiB |
|
|
166
|
+
|
|
167
|
+
Media deadlines include authentication and network waits. Discovery and media requests reject redirects and report HTTP errors without upstream response bodies.
|
|
168
|
+
|
|
169
|
+
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.
|
|
170
|
+
|
|
171
|
+
## Chat models
|
|
172
|
+
|
|
173
|
+
The extension discovers available IDs through `/v1/models` and matches them against Pi's built-in metadata for limits, prices, inputs, and thinking capabilities.
|
|
174
|
+
|
|
175
|
+
### Refresh
|
|
176
|
+
|
|
177
|
+
Run `/cliproxyapi-refresh` to update the chat catalog.
|
|
178
|
+
|
|
179
|
+
### Aliases and model limits
|
|
180
|
+
|
|
181
|
+
The chat catalog skips unknown IDs rather than guessing their capabilities. Recognized catalog owners resolve duplicate metadata IDs; ambiguous cross-family matches stay out.
|
|
182
|
+
|
|
183
|
+
To describe a proxy alias, add a canonical metadata reference to `~/.pi/agent/pi-cliproxyapi.json`:
|
|
184
|
+
|
|
185
|
+
```json
|
|
186
|
+
{
|
|
187
|
+
"aliases": {
|
|
188
|
+
"team-chat": "anthropic/<canonical-model-id>"
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
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.
|
|
194
|
+
|
|
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
|
+
|
|
197
|
+
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
|
+
|
|
199
|
+
## Backend routes
|
|
200
|
+
|
|
201
|
+
Keep the full advertised ID when selecting a backend, for example:
|
|
202
|
+
|
|
203
|
+
```text
|
|
204
|
+
/cli:model select vertex/gemini-2.5-flash-image
|
|
205
|
+
/cli:model select antigravity/gemini-3.1-flash-image
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
These examples select media defaults. Chat routes belong in Pi's built-in `/model` picker.
|
|
209
|
+
|
|
210
|
+
Set the CLIProxyAPI auth record's top-level `prefix` to `vertex` or `antigravity` to advertise those routes. With `force-model-prefix: false`, the proxy retains bare IDs too. This extension does not change proxy configuration or restart it.
|
|
211
|
+
|
|
212
|
+
## Thinking and child sessions
|
|
213
|
+
|
|
214
|
+
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
|
+
|
|
216
|
+
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
|
+
|
|
218
|
+
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:
|
|
219
|
+
|
|
220
|
+
```json
|
|
221
|
+
{
|
|
222
|
+
"subagents": {
|
|
223
|
+
"agentOverrides": {
|
|
224
|
+
"reviewer": {
|
|
225
|
+
"subagentOnlyExtensions": ["/path/to/pi-cliproxyapi-native/extensions/index.ts"]
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Run `/reload` after changing settings and `/cliproxyapi-refresh` before starting new children that need an updated catalog. Extension allowlists and policies still apply. If a child cannot resolve a model, check extension loading, authentication, and the saved catalog before changing its thinking suffix.
|
|
233
|
+
|
|
234
|
+
## Protocol notes
|
|
235
|
+
|
|
236
|
+
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.
|
|
237
|
+
|
|
238
|
+
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
|
+
|
|
240
|
+
OpenAI image POSTs use a dedicated HTTP connection so shorter transport timeouts do not interrupt the ten-minute deadline. They request identity encoding and reject compressed responses.
|
|
241
|
+
|
|
242
|
+
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
|
+
|
|
244
|
+
## License
|
|
245
|
+
|
|
246
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { type ExtensionAPI, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { PROVIDER_ID, parseConfig } from "../src/config.ts";
|
|
5
|
+
import { registerMediaTools } from "../src/media.ts";
|
|
6
|
+
import { registerModelPicker } from "../src/picker.ts";
|
|
7
|
+
import { createCliproxyProvider } from "../src/provider.ts";
|
|
8
|
+
|
|
9
|
+
export default async function (pi: ExtensionAPI) {
|
|
10
|
+
let contents = "{}";
|
|
11
|
+
try {
|
|
12
|
+
contents = await readFile(join(getAgentDir(), "pi-cliproxyapi.json"), "utf8");
|
|
13
|
+
} catch (error) {
|
|
14
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
|
|
15
|
+
throw new Error("Cannot read pi-cliproxyapi.json.");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
let raw: unknown;
|
|
19
|
+
try {
|
|
20
|
+
raw = JSON.parse(contents);
|
|
21
|
+
} catch {
|
|
22
|
+
throw new Error("pi-cliproxyapi.json must contain valid JSON.");
|
|
23
|
+
}
|
|
24
|
+
const config = parseConfig(raw, process.env.CLIPROXYAPI_BASE_URL);
|
|
25
|
+
pi.registerProvider(createCliproxyProvider(config));
|
|
26
|
+
registerMediaTools(pi, config);
|
|
27
|
+
registerModelPicker(pi, config);
|
|
28
|
+
pi.registerCommand("cliproxyapi-refresh", {
|
|
29
|
+
description: "Refresh the CLIProxyAPI model catalog",
|
|
30
|
+
async handler(args, ctx) {
|
|
31
|
+
if (args.trim()) {
|
|
32
|
+
ctx.ui.notify("Usage: /cliproxyapi-refresh", "error");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (ctx.modelRegistry.getProviderAuthStatus(PROVIDER_ID).configured === false) {
|
|
36
|
+
ctx.ui.notify("Use /login cliproxyapi or set CLIPROXYAPI_API_KEY before refreshing.", "warning");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const result = await ctx.modelRegistry.refresh({
|
|
40
|
+
providers: [PROVIDER_ID],
|
|
41
|
+
force: true,
|
|
42
|
+
signal: AbortSignal.timeout(15000),
|
|
43
|
+
});
|
|
44
|
+
const error = result.errors.get(PROVIDER_ID);
|
|
45
|
+
if (error || result.aborted) {
|
|
46
|
+
ctx.ui.notify(error?.message ?? "CLIProxyAPI refresh timed out.", "error");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const count = ctx.modelRegistry.getAll().filter((model) => model.provider === PROVIDER_ID).length;
|
|
50
|
+
ctx.ui.notify(`CLIProxyAPI: ${count} models.`, "info");
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-cliproxyapi-native",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "CLIProxyAPI chat discovery and image/video generation with Pi's native adapters",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=22.19.0"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"pi-package",
|
|
15
|
+
"CLIProxyAPI"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/hawkff/pi-cliproxyapi-native.git"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"extensions",
|
|
23
|
+
"src",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"pi": {
|
|
28
|
+
"extensions": [
|
|
29
|
+
"./extensions/index.ts"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"format": "biome check --write .",
|
|
35
|
+
"lint": "biome check .",
|
|
36
|
+
"test": "node --test test/*.test.ts",
|
|
37
|
+
"check": "pnpm typecheck && pnpm lint && pnpm test"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@biomejs/biome": "^2.5.13",
|
|
41
|
+
"@earendil-works/pi-ai": "^0.87.0",
|
|
42
|
+
"@earendil-works/pi-coding-agent": "^0.87.0",
|
|
43
|
+
"@earendil-works/pi-tui": "^0.87.0",
|
|
44
|
+
"@types/node": "^24.13.4",
|
|
45
|
+
"typescript": "5.9.3"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@earendil-works/pi-ai": "^0.87.0",
|
|
49
|
+
"@earendil-works/pi-coding-agent": "^0.87.0",
|
|
50
|
+
"@earendil-works/pi-tui": "^0.87.0"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/catalog.ts
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import { type Config, isModelId, isRecord, PROVIDER_ID } from "./config.ts";
|
|
3
|
+
|
|
4
|
+
export type CpaApi =
|
|
5
|
+
| "anthropic-messages"
|
|
6
|
+
| "openai-responses"
|
|
7
|
+
| "openai-completions"
|
|
8
|
+
| "google-generative-ai";
|
|
9
|
+
|
|
10
|
+
function isCpaApi(api: string): api is CpaApi {
|
|
11
|
+
return ["anthropic-messages", "openai-responses", "openai-completions", "google-generative-ai"].includes(
|
|
12
|
+
api,
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function endpoint(baseUrl: string, api: string) {
|
|
17
|
+
if (api === "anthropic-messages") return baseUrl;
|
|
18
|
+
return `${baseUrl}/${api === "google-generative-ai" ? "v1beta" : "v1"}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function parseCatalog(value: unknown) {
|
|
22
|
+
if (!isRecord(value) || !Array.isArray(value.data) || value.data.length > 10000) {
|
|
23
|
+
throw new Error("Invalid CLIProxyAPI model catalog: expected a data array.");
|
|
24
|
+
}
|
|
25
|
+
return value.data.map((entry: unknown) => {
|
|
26
|
+
if (
|
|
27
|
+
!isRecord(entry) ||
|
|
28
|
+
!isModelId(entry.id) ||
|
|
29
|
+
(entry.owned_by !== undefined && !isModelId(entry.owned_by))
|
|
30
|
+
) {
|
|
31
|
+
throw new Error("Invalid CLIProxyAPI model catalog entry.");
|
|
32
|
+
}
|
|
33
|
+
return { id: entry.id, owner: entry.owned_by, hidden: entry.visibility === "hide" };
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Resolve metadata and capabilities without changing registry or request IDs.
|
|
38
|
+
export function modelRoute(id: string) {
|
|
39
|
+
const match = /^(vertex|antigravity)\/(.+)$/.exec(id);
|
|
40
|
+
return {
|
|
41
|
+
metadataId: match?.[2] ?? id,
|
|
42
|
+
backend: match ? (match[1] === "vertex" ? "Vertex" : "Antigravity") : undefined,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function backendLabel(id: string) {
|
|
47
|
+
return modelRoute(id).backend ?? (id.includes("/") ? "Unknown backend" : "Automatic (proxy routing)");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function routedName(id: string, name = id) {
|
|
51
|
+
return `${name} · ${backendLabel(id)}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type MediaPurpose = "image" | "video";
|
|
55
|
+
|
|
56
|
+
const imagenRetirement = "Retired on Vertex (June 2026); disabled. Select a Nano Banana image model instead.";
|
|
57
|
+
|
|
58
|
+
// Exact media IDs, including retired entries; vision input alone does not imply image output.
|
|
59
|
+
const mediaModels = new Map<
|
|
60
|
+
string,
|
|
61
|
+
{ name: string; purpose: MediaPurpose; route?: "xai" | "gemini" | "openai"; disabledReason?: string }
|
|
62
|
+
>([
|
|
63
|
+
["gpt-image-2.5-flare", { name: "GPT Image 2.5 Flare", purpose: "image", route: "openai" }],
|
|
64
|
+
["gpt-image-2.5-sunburst", { name: "GPT Image 2.5 Sunburst", purpose: "image", route: "openai" }],
|
|
65
|
+
["gpt-image-2.5", { name: "GPT Image 2.5", purpose: "image", route: "openai" }],
|
|
66
|
+
["gpt-image-2", { name: "GPT Image 2", purpose: "image", route: "openai" }],
|
|
67
|
+
["gpt-image-1.5", { name: "GPT Image 1.5", purpose: "image", route: "openai" }],
|
|
68
|
+
["grok-imagine-image", { name: "Grok Imagine Image", purpose: "image", route: "xai" }],
|
|
69
|
+
["grok-imagine-image-quality", { name: "Grok Imagine Image Quality", purpose: "image", route: "xai" }],
|
|
70
|
+
["grok-imagine-image-2.0", { name: "Grok Imagine Image 2.0", purpose: "image", route: "xai" }],
|
|
71
|
+
["grok-imagine-video", { name: "Grok Imagine Video", purpose: "video", route: "xai" }],
|
|
72
|
+
["grok-imagine-video-1.5", { name: "Grok Imagine Video 1.5", purpose: "video", route: "xai" }],
|
|
73
|
+
[
|
|
74
|
+
"grok-imagine-video-1.5-preview",
|
|
75
|
+
{ name: "Grok Imagine Video 1.5 Preview", purpose: "video", route: "xai" },
|
|
76
|
+
],
|
|
77
|
+
["gemini-2.5-flash-image", { name: "Nano Banana", purpose: "image", route: "gemini" }],
|
|
78
|
+
["gemini-3.1-flash-image", { name: "Nano Banana 2", purpose: "image", route: "gemini" }],
|
|
79
|
+
["gemini-3-pro-image", { name: "Nano Banana Pro", purpose: "image", route: "gemini" }],
|
|
80
|
+
["gemini-3.1-flash-lite-image", { name: "Nano Banana 2 Lite", purpose: "image", route: "gemini" }],
|
|
81
|
+
["imagen-3.0-generate-002", { name: "Imagen 3", purpose: "image", disabledReason: imagenRetirement }],
|
|
82
|
+
[
|
|
83
|
+
"imagen-3.0-fast-generate-001",
|
|
84
|
+
{ name: "Imagen 3 Fast", purpose: "image", disabledReason: imagenRetirement },
|
|
85
|
+
],
|
|
86
|
+
["imagen-4.0-generate-001", { name: "Imagen 4", purpose: "image", disabledReason: imagenRetirement }],
|
|
87
|
+
[
|
|
88
|
+
"imagen-4.0-fast-generate-001",
|
|
89
|
+
{ name: "Imagen 4 Fast", purpose: "image", disabledReason: imagenRetirement },
|
|
90
|
+
],
|
|
91
|
+
[
|
|
92
|
+
"imagen-4.0-ultra-generate-001",
|
|
93
|
+
{ name: "Imagen 4 Ultra", purpose: "image", disabledReason: imagenRetirement },
|
|
94
|
+
],
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
export function mediaCapability(id: string) {
|
|
98
|
+
const { metadataId, backend } = modelRoute(id);
|
|
99
|
+
const capability = mediaModels.get(metadataId);
|
|
100
|
+
if (backend && (capability?.route === "xai" || capability?.route === "openai")) {
|
|
101
|
+
return { ...capability, route: undefined, disabledReason: `Unsupported media execution on ${backend}.` };
|
|
102
|
+
}
|
|
103
|
+
return capability;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function mediaPurpose(id: string) {
|
|
107
|
+
return mediaCapability(id)?.purpose;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function mediaControls(id: string) {
|
|
111
|
+
const capability = mediaCapability(id);
|
|
112
|
+
const { metadataId } = modelRoute(id);
|
|
113
|
+
const commonRatios = ["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3"];
|
|
114
|
+
let resolution: string[] = [];
|
|
115
|
+
let aspect_ratio: string[] = [];
|
|
116
|
+
if (capability?.route === "gemini") {
|
|
117
|
+
aspect_ratio = [...commonRatios, "4:5", "5:4", "21:9"];
|
|
118
|
+
if (metadataId === "gemini-3.1-flash-image") {
|
|
119
|
+
resolution = ["512", "1K", "2K", "4K"];
|
|
120
|
+
aspect_ratio.push("1:4", "4:1", "1:8", "8:1");
|
|
121
|
+
} else if (metadataId === "gemini-3-pro-image") resolution = ["1K", "2K", "4K"];
|
|
122
|
+
else if (metadataId === "gemini-3.1-flash-lite-image") resolution = ["1K"];
|
|
123
|
+
} else if (capability?.route === "xai") {
|
|
124
|
+
aspect_ratio = commonRatios;
|
|
125
|
+
if (capability.purpose === "image") {
|
|
126
|
+
resolution = ["1k", "2k"];
|
|
127
|
+
// CLIProxyAPI 7.3.11 drops other xAI image ratios, including auto.
|
|
128
|
+
aspect_ratio.push("9:20", "20:9");
|
|
129
|
+
} else {
|
|
130
|
+
resolution = ["480p", "720p"];
|
|
131
|
+
if (metadataId !== "grok-imagine-video") resolution.push("1080p");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
size: capability?.route === "openai" ? ["auto", "1024x1024", "1536x1024", "1024x1536"] : [],
|
|
136
|
+
size_limits:
|
|
137
|
+
capability?.route === "openai" && metadataId !== "gpt-image-1.5"
|
|
138
|
+
? { multiple_of: 16, max_edge: 3840, min_pixels: 655360, max_pixels: 8294400, max_aspect_ratio: 3 }
|
|
139
|
+
: undefined,
|
|
140
|
+
resolution,
|
|
141
|
+
aspect_ratio,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function mapMediaCatalog(value: unknown) {
|
|
146
|
+
const models = new Map<
|
|
147
|
+
string,
|
|
148
|
+
{ id: string; purpose: MediaPurpose; name: string; controls: ReturnType<typeof mediaControls> }
|
|
149
|
+
>();
|
|
150
|
+
for (const entry of parseCatalog(value)) {
|
|
151
|
+
const capability = mediaCapability(entry.id);
|
|
152
|
+
if (!entry.hidden && capability && !capability.disabledReason)
|
|
153
|
+
models.set(entry.id, {
|
|
154
|
+
id: entry.id,
|
|
155
|
+
purpose: capability.purpose,
|
|
156
|
+
name: routedName(entry.id, capability.name),
|
|
157
|
+
controls: mediaControls(entry.id),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return [...models.values()];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const owners: Readonly<Record<string, string>> = {
|
|
164
|
+
claude: "anthropic",
|
|
165
|
+
anthropic: "anthropic",
|
|
166
|
+
openai: "openai",
|
|
167
|
+
codex: "openai-codex",
|
|
168
|
+
"openai-codex": "openai-codex",
|
|
169
|
+
google: "google",
|
|
170
|
+
gemini: "google",
|
|
171
|
+
"gemini-cli": "google",
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export function mapCatalog(value: unknown, config: Config, known: readonly Model<Api>[]) {
|
|
175
|
+
const models = new Map<string, Model<CpaApi>>();
|
|
176
|
+
const skipped = new Set<string>();
|
|
177
|
+
for (const entry of parseCatalog(value)) {
|
|
178
|
+
if (entry.hidden || models.has(entry.id)) continue;
|
|
179
|
+
if (mediaPurpose(entry.id)) {
|
|
180
|
+
skipped.add(entry.id);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const { metadataId, backend } = modelRoute(entry.id);
|
|
184
|
+
const alias = Object.hasOwn(config.aliases, entry.id)
|
|
185
|
+
? config.aliases[entry.id]
|
|
186
|
+
: backend && Object.hasOwn(config.aliases, metadataId)
|
|
187
|
+
? config.aliases[metadataId]
|
|
188
|
+
: undefined;
|
|
189
|
+
const candidates = known.filter((model) =>
|
|
190
|
+
alias
|
|
191
|
+
? `${model.provider}/${model.id}` === alias
|
|
192
|
+
: (!entry.id.includes("/") || backend) && model.id === metadataId,
|
|
193
|
+
);
|
|
194
|
+
const families = new Set(
|
|
195
|
+
candidates.map((model) => (model.provider === "openai-codex" ? "openai" : model.provider)),
|
|
196
|
+
);
|
|
197
|
+
const preferred = entry.owner ? owners[entry.owner.toLowerCase()] : undefined;
|
|
198
|
+
const reference = alias
|
|
199
|
+
? candidates.length === 1
|
|
200
|
+
? candidates[0]
|
|
201
|
+
: undefined
|
|
202
|
+
: (candidates.find((model) => model.provider === preferred) ??
|
|
203
|
+
(families.size === 1 ? candidates[0] : undefined));
|
|
204
|
+
if (!reference) {
|
|
205
|
+
skipped.add(entry.id);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const api = reference.api === "openai-codex-responses" ? "openai-responses" : reference.api;
|
|
209
|
+
if (!isCpaApi(api)) {
|
|
210
|
+
skipped.add(entry.id);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const model: Model<CpaApi> = {
|
|
214
|
+
...structuredClone(reference),
|
|
215
|
+
id: entry.id,
|
|
216
|
+
name: routedName(entry.id, alias ? `${entry.id} (${reference.name})` : reference.name),
|
|
217
|
+
provider: PROVIDER_ID,
|
|
218
|
+
api,
|
|
219
|
+
baseUrl: endpoint(config.baseUrl, api),
|
|
220
|
+
headers: undefined,
|
|
221
|
+
compat:
|
|
222
|
+
api === "anthropic-messages"
|
|
223
|
+
? {
|
|
224
|
+
...reference.compat,
|
|
225
|
+
supportsEagerToolInputStreaming: false,
|
|
226
|
+
supportsStrictTools: false,
|
|
227
|
+
supportsMidConvoSystemMessages: false,
|
|
228
|
+
supportsMidConvoToolChanges: false,
|
|
229
|
+
supportsMidConvoEffort: false,
|
|
230
|
+
allowedFallbackModels: undefined,
|
|
231
|
+
}
|
|
232
|
+
: api === "google-generative-ai"
|
|
233
|
+
? reference.compat
|
|
234
|
+
: {
|
|
235
|
+
...reference.compat,
|
|
236
|
+
supportsStrictMode: false,
|
|
237
|
+
supportsMidConvoSystemMessages: false,
|
|
238
|
+
supportsMidConvoToolAdditions: false,
|
|
239
|
+
supportsOpenAIGrammarTools: false,
|
|
240
|
+
supportsToolSearch: false,
|
|
241
|
+
supportsAdditionalTools: false,
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
models.set(model.id, model);
|
|
245
|
+
skipped.delete(model.id);
|
|
246
|
+
}
|
|
247
|
+
return { models: [...models.values()], skipped: [...skipped] };
|
|
248
|
+
}
|