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/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
|
|
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<
|
|
@@ -212,6 +211,15 @@ export class ModelPicker extends Container implements Focusable {
|
|
|
212
211
|
|
|
213
212
|
export function registerModelPicker(pi: ExtensionAPI, config: Config) {
|
|
214
213
|
let commandSequence = 0;
|
|
214
|
+
let navigation = 0;
|
|
215
|
+
const invalidate = () => {
|
|
216
|
+
navigation++;
|
|
217
|
+
};
|
|
218
|
+
pi.on("session_before_switch", invalidate);
|
|
219
|
+
pi.on("session_before_fork", invalidate);
|
|
220
|
+
pi.on("session_before_tree", invalidate);
|
|
221
|
+
pi.on("session_start", invalidate);
|
|
222
|
+
pi.on("session_shutdown", invalidate);
|
|
215
223
|
const savedSequence = { image: 0, video: 0 };
|
|
216
224
|
pi.registerCommand("cli:model", {
|
|
217
225
|
description: "Search CLIProxyAPI image/video models and select a session media default",
|
|
@@ -221,7 +229,9 @@ export function registerModelPicker(pi: ExtensionAPI, config: Config) {
|
|
|
221
229
|
.map((value) => ({ value, label: value })),
|
|
222
230
|
async handler(args, ctx) {
|
|
223
231
|
const sequence = ++commandSequence;
|
|
232
|
+
const generation = navigation;
|
|
224
233
|
const report = (content: string, error = false) => {
|
|
234
|
+
if (generation !== navigation) return;
|
|
225
235
|
if (ctx.hasUI) ctx.ui.notify(content, error ? "error" : "info");
|
|
226
236
|
else if (ctx.mode === "print") console.error(content);
|
|
227
237
|
else
|
|
@@ -233,6 +243,7 @@ export function registerModelPicker(pi: ExtensionAPI, config: Config) {
|
|
|
233
243
|
try {
|
|
234
244
|
const input = args.trim();
|
|
235
245
|
const save = (choice: string) => {
|
|
246
|
+
if (generation !== navigation) return;
|
|
236
247
|
const clearing = clearedPurpose(choice);
|
|
237
248
|
const capability = clearing ? undefined : mediaCapability(choice);
|
|
238
249
|
const purpose = clearing ?? capability?.purpose;
|
|
@@ -265,6 +276,7 @@ export function registerModelPicker(pi: ExtensionAPI, config: Config) {
|
|
|
265
276
|
const signal = deadline(ctx.signal, 15000);
|
|
266
277
|
const key = await mediaKey(ctx, signal);
|
|
267
278
|
const rows = pickerCatalog(await fetchCatalog(config, key, signal));
|
|
279
|
+
if (generation !== navigation) return;
|
|
268
280
|
const { defaults, automatic } = effectiveMediaDefaults(config, ctx);
|
|
269
281
|
const items = pickerItems(rows, defaults);
|
|
270
282
|
const query = input === "list" ? "" : input.startsWith("search ") ? input.slice(7) : input;
|
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 {
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|