pi-cliproxyapi-native 0.3.1 → 0.3.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/src/picker.ts CHANGED
@@ -7,8 +7,8 @@ import {
7
7
  type TuiMouseEvent,
8
8
  truncateToWidth,
9
9
  } from "@earendil-works/pi-tui";
10
- import { backendLabel, mediaCapability, parseCatalog, routedName } from "./catalog.ts";
11
- import type { Config } from "./config.ts";
10
+ import { backendLabel, fetchCatalog, mediaCapability, parseCatalog, routedName } from "./catalog.ts";
11
+ import { type Config, deadline } from "./config.ts";
12
12
  import { mediaKey } from "./media.ts";
13
13
  import {
14
14
  effectiveMediaDefaults,
@@ -16,7 +16,6 @@ import {
16
16
  type MediaDefaults,
17
17
  readMediaDefaults,
18
18
  } from "./media-defaults.ts";
19
- import { deadline, fetchCatalog } from "./provider.ts";
20
19
 
21
20
  export function pickerCatalog(value: unknown) {
22
21
  const rows = new Map<
package/src/provider.ts CHANGED
@@ -18,10 +18,19 @@ import {
18
18
  openAIResponsesApi,
19
19
  } from "@earendil-works/pi-ai/compat";
20
20
  import { getBuiltinModels } from "@earendil-works/pi-ai/providers/all";
21
- import { type CpaApi, endpoint, mapCatalog, modelRoute } from "./catalog.ts";
21
+ import {
22
+ type CpaApi,
23
+ endpoint,
24
+ fetchCatalog,
25
+ IMAGE_API,
26
+ mapCatalog,
27
+ mapImageModels,
28
+ modelRoute,
29
+ } from "./catalog.ts";
22
30
  import { type QuotaWaitHandler, waitForClaudeQuota } from "./claude-quota.ts";
23
31
  import { withCompactionRecovery } from "./compaction.ts";
24
- import { type Config, isRecord, PROVIDER_ID } from "./config.ts";
32
+ import { type Config, isRecord, PROVIDER_ID, validateKey } from "./config.ts";
33
+ import { generateNativeImages } from "./media.ts";
25
34
 
26
35
  export type { QuotaWaitHandler } from "./claude-quota.ts";
27
36
 
@@ -31,63 +40,21 @@ export function builtinCatalog() {
31
40
  ...getBuiltinModels("openai"),
32
41
  ...getBuiltinModels("openai-codex"),
33
42
  ...getBuiltinModels("google"),
43
+ ...getBuiltinModels("xai"),
34
44
  ];
35
45
  }
36
46
 
37
- export function validateKey(key: string) {
38
- if (!/^[!-~]{1,4096}$/.test(key)) {
39
- throw new Error("CLIProxyAPI requires a non-empty API key without whitespace or control characters.");
40
- }
41
- return key;
42
- }
43
-
44
- export function deadline(signal: AbortSignal | undefined, milliseconds: number) {
45
- return AbortSignal.any([...(signal ? [signal] : []), AbortSignal.timeout(milliseconds)]);
46
- }
47
-
48
- export async function fetchCatalog(config: Config, key: string, signal: AbortSignal) {
49
- validateKey(key);
50
- const boundedSignal = deadline(signal, 10000);
51
- let response: Response;
52
- try {
53
- response = await fetch(`${config.baseUrl}/v1/models`, {
54
- headers: { Authorization: `Bearer ${key}` },
55
- redirect: "error",
56
- signal: boundedSignal,
57
- });
58
- } catch {
59
- signal.throwIfAborted();
60
- throw new Error("CLIProxyAPI discovery failed: connection, timeout, or redirect error.");
61
- }
62
- if (!response.ok) {
63
- await response.body?.cancel();
64
- throw new Error(`CLIProxyAPI discovery failed (HTTP ${response.status}).`);
65
- }
66
- let payload: unknown;
67
- const reader = response.body?.getReader();
68
- if (!reader) throw new Error("CLIProxyAPI discovery returned an empty response.");
69
- try {
70
- const decoder = new TextDecoder();
71
- let text = "";
72
- let size = 0;
73
- while (true) {
74
- const chunk = await reader.read();
75
- if (chunk.done) break;
76
- size += chunk.value.byteLength;
77
- if (size > 4 * 1024 * 1024) {
78
- await reader.cancel();
79
- throw new Error("Catalog too large.");
80
- }
81
- text += decoder.decode(chunk.value, { stream: true });
82
- }
83
- payload = JSON.parse(text + decoder.decode());
84
- } catch {
85
- signal.throwIfAborted();
86
- throw new Error("CLIProxyAPI catalog is invalid, incomplete, or larger than 4 MiB.");
87
- } finally {
88
- reader.releaseLock();
89
- }
90
- return payload;
47
+ // Pi's live catalogs add models released after this Pi build; bundled metadata wins for known IDs.
48
+ export function metadataCatalog(live?: (provider: string) => readonly Model<Api>[] | undefined) {
49
+ const bundled = builtinCatalog();
50
+ if (!live) return bundled;
51
+ const ids = new Set(bundled.map(({ provider, id }) => `${provider}/${id}`));
52
+ return [
53
+ ...bundled,
54
+ ...["anthropic", "openai", "openai-codex", "google", "xai"]
55
+ .flatMap((provider) => live(provider) ?? [])
56
+ .filter(({ provider, id }) => !ids.has(`${provider}/${id}`)),
57
+ ];
91
58
  }
92
59
 
93
60
  export async function discover(
@@ -345,10 +312,12 @@ function route(
345
312
 
346
313
  export function createCliproxyProvider(
347
314
  config: Config,
348
- known: readonly Model<Api>[] = builtinCatalog(),
315
+ known: readonly Model<Api>[] | (() => readonly Model<Api>[]) = builtinCatalog,
349
316
  onQuotaWait?: QuotaWaitHandler,
350
317
  compactionSignals = new WeakSet<AbortSignal>(),
351
318
  ) {
319
+ const metadata = typeof known === "function" ? known : () => known;
320
+ let skipped: string[] = [];
352
321
  const standardAuth = envApiKeyAuth("CLIProxyAPI API key", ["CLIPROXYAPI_API_KEY"]);
353
322
  const scope = createHash("sha256")
354
323
  .update(JSON.stringify([5, { baseUrl: config.baseUrl, aliases: config.aliases }]))
@@ -364,7 +333,7 @@ export function createCliproxyProvider(
364
333
  const credential = await standardAuth.login?.(interaction);
365
334
  if (!credential?.key) throw new Error("CLIProxyAPI requires an API key.");
366
335
  validateKey(credential.key);
367
- await discover(config, credential.key, interaction.signal, known);
336
+ await discover(config, credential.key, interaction.signal, metadata());
368
337
  return credential;
369
338
  },
370
339
  async resolve(input) {
@@ -380,7 +349,15 @@ export function createCliproxyProvider(
380
349
  if (context.credential?.type !== "api_key" || !context.credential.key) {
381
350
  throw new Error("Use /login cliproxyapi or set CLIPROXYAPI_API_KEY before refreshing.");
382
351
  }
383
- return (await discover(config, context.credential.key, context.signal, known)).models;
352
+ const payload = await fetchCatalog(config, context.credential.key, context.signal);
353
+ const chat = mapCatalog(payload, config, metadata());
354
+ skipped = chat.skipped;
355
+ return [...chat.models, ...mapImageModels(payload, config)];
356
+ },
357
+ images: {
358
+ [IMAGE_API]: {
359
+ generateImages: (model, context, options) => generateNativeImages(config, model, context, options),
360
+ },
384
361
  },
385
362
  api: {
386
363
  "anthropic-messages": route(anthropicMessagesApi(), config, compactionSignals, onQuotaWait),
@@ -392,6 +369,8 @@ export function createCliproxyProvider(
392
369
  const refresh = provider.refreshModels;
393
370
  return {
394
371
  ...provider,
372
+ // Chat IDs without Pi metadata from the latest network refresh, for diagnostics.
373
+ skippedModels: () => skipped,
395
374
  async refreshModels(context: RefreshModelsContext) {
396
375
  await refresh?.({
397
376
  ...context,