pi-cliproxyapi-native 0.1.5 → 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 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 1.0.0 and CLIProxyAPI 8.0.7, 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.
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.5
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.5`. For a local checkout, use `pi install /path/to/pi-cliproxyapi-native`.
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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cliproxyapi-native",
3
- "version": "0.1.5",
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": "^1.0.0",
43
- "@earendil-works/pi-coding-agent": "^1.0.0",
44
- "@earendil-works/pi-tui": "^1.0.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
- : undefined;
189
- const candidates = known.filter((model) =>
190
- alias
191
- ? `${model.provider}/${model.id}` === alias
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
@@ -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>();
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 defaults = readMediaDefaults(config, ctx);
231
- if (choice === "clear image") delete defaults.image;
232
- else if (choice === "clear video") delete defaults.video;
233
- else {
234
- const capability = mediaCapability(choice);
235
- if (!capability || capability.disabledReason)
236
- throw new Error("Unsupported CLIProxyAPI media selection.");
237
- defaults[capability.purpose] = choice;
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 (clear) {
244
- save(`clear ${clear}`);
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 disabledReason = input.startsWith("select ")
249
- ? mediaCapability(input.slice(7).trim())?.disabledReason
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 = AbortSignal.any([...(ctx.signal ? [ctx.signal] : []), AbortSignal.timeout(15000)]);
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 === "clear image" || choice === "clear video") {
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(row.id);
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 = AbortSignal.any([signal, AbortSignal.timeout(10000)]);
45
+ const boundedSignal = deadline(signal, 10000);
42
46
  let response: Response;
43
47
  try {
44
48
  response = await fetch(`${config.baseUrl}/v1/models`, {