pi-tinyllm 0.1.0 → 0.1.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 +3 -3
- package/extensions/tinyllm.ts +121 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,9 +52,9 @@ pi --provider tinyllm --model anthropic/claude-sonnet-4-6
|
|
|
52
52
|
|
|
53
53
|
## Behavior and limitations
|
|
54
54
|
|
|
55
|
-
-
|
|
56
|
-
- Generated OpenAI `-fast` IDs are filtered from discovery
|
|
57
|
-
- `/fast` is available only while a
|
|
55
|
+
- Discovery uses TinyLLM's `/api/v1/models` endpoint, where `configured_models` contains explicit IDs and `providers` identifies every configured provider. Provider descriptors expand from Pi's matching installed catalog, including custom TinyLLM prefixes; a `404` falls back to `/v1/models` for explicit-ID discovery from older servers. Canonical `codex` maps to `openai-codex`, while canonical `openai` prefers `openai-codex` metadata and then `openai`. Expansion describes routable catalog models; it does not probe account-specific upstream entitlements.
|
|
56
|
+
- The extension status line shows the active Pi provider, such as `[tinyllm]` or `[openai-codex]`. Generated OpenAI `-fast` IDs are filtered from discovery; run `/fast` on an advertised OpenAI GPT model to toggle request-time fast routing. While enabled, the status line reads `[tinyllm] fast` for a selected TinyLLM model.
|
|
57
|
+
- `/fast` is available only while a TinyLLM model sourced from an OpenAI catalog has a native `gpt-*` ID. Other models are left unchanged and produce a warning. The toggle follows the active session branch and is restored on reload or resume.
|
|
58
58
|
- Anthropic Messages, OpenAI Responses, and Chat Completions models use TinyLLM's corresponding routes.
|
|
59
59
|
- Unknown aliases, unknown models, and models using unsupported wire APIs are omitted rather than assigned guessed metadata.
|
|
60
60
|
- Pi owns the persisted model catalog. Failed refreshes retain the last known good catalog, and offline refreshes restore it.
|
package/extensions/tinyllm.ts
CHANGED
|
@@ -21,6 +21,8 @@ const SUPPORTED_APIS = new Map<Api, Api>([
|
|
|
21
21
|
]);
|
|
22
22
|
|
|
23
23
|
type Warn = (message: string) => void;
|
|
24
|
+
type BuiltinProvider = ReturnType<typeof getBuiltinProviders>[number];
|
|
25
|
+
type CatalogMap = ReadonlyMap<string, BuiltinProvider>;
|
|
24
26
|
|
|
25
27
|
export interface TinyllmProviderOptions {
|
|
26
28
|
baseUrl?: string;
|
|
@@ -38,10 +40,72 @@ export function apiBaseUrl(baseUrl: string, api: Api): string {
|
|
|
38
40
|
return api === "anthropic-messages" ? `${root}/anthropic` : `${root}/v1`;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
function
|
|
43
|
+
function isBuiltinProvider(value: string): value is BuiltinProvider {
|
|
44
|
+
return getBuiltinProviders().includes(value as BuiltinProvider);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function catalogCandidates(prefix: string, catalog?: BuiltinProvider): BuiltinProvider[] {
|
|
48
|
+
if (catalog) return [catalog];
|
|
42
49
|
if (prefix === "openai") return ["openai-codex", "openai"];
|
|
43
50
|
if (prefix === "codex") return ["openai-codex"];
|
|
44
|
-
return
|
|
51
|
+
return isBuiltinProvider(prefix) ? [prefix] : [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function diagnostic(value: string): string {
|
|
55
|
+
const sanitized = value.replace(/[\u0000-\u001f\u007f-\u009f]/g, "?");
|
|
56
|
+
return sanitized.length > MAX_DIAGNOSTIC_ID_LENGTH
|
|
57
|
+
? `${sanitized.slice(0, MAX_DIAGNOSTIC_ID_LENGTH - 3)}...`
|
|
58
|
+
: sanitized;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function warnOmitted(label: string, omitted: string[], count: number, warn: Warn): void {
|
|
62
|
+
if (omitted.length === 0) return;
|
|
63
|
+
const suffix = count > omitted.length ? ` (+${count - omitted.length} more)` : "";
|
|
64
|
+
warn(`TinyLLM omitted ${label}: ${omitted.join(", ")}${suffix}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function providerCatalogs(
|
|
68
|
+
value: unknown,
|
|
69
|
+
warn: Warn,
|
|
70
|
+
): { catalogs: Map<string, BuiltinProvider>; ids: string[] } {
|
|
71
|
+
const catalogs = new Map<string, BuiltinProvider>();
|
|
72
|
+
const ids: string[] = [];
|
|
73
|
+
if (value === undefined) return { catalogs, ids };
|
|
74
|
+
const entries = Array.isArray(value) ? value : [undefined];
|
|
75
|
+
const omitted: string[] = [];
|
|
76
|
+
let omittedCount = 0;
|
|
77
|
+
const omit = (description: string) => {
|
|
78
|
+
omittedCount++;
|
|
79
|
+
if (omitted.length < MAX_DIAGNOSTIC_IDS) omitted.push(diagnostic(description));
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
if (!entry || typeof entry !== "object") {
|
|
84
|
+
omit("<invalid provider>");
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const { id, type, auth } = entry as { id?: unknown; type?: unknown; auth?: unknown };
|
|
88
|
+
if (
|
|
89
|
+
typeof id !== "string"
|
|
90
|
+
|| !/^[A-Za-z0-9_.-]{1,64}$/.test(id)
|
|
91
|
+
|| typeof type !== "string"
|
|
92
|
+
|| (type === "openai" && auth !== "api_key" && auth !== "subscription")
|
|
93
|
+
) {
|
|
94
|
+
omit(typeof id === "string" && typeof type === "string" ? `${id}:${type}` : "<invalid provider>");
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const catalogName = type === "openai" && auth === "subscription" ? "openai-codex" : type;
|
|
98
|
+
if (!isBuiltinProvider(catalogName)) {
|
|
99
|
+
omit(`${id}:${type}`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (catalogs.has(id)) continue;
|
|
103
|
+
catalogs.set(id, catalogName);
|
|
104
|
+
for (const model of getBuiltinModels(catalogName)) ids.push(`${id}/${model.id}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
warnOmitted("invalid or unsupported providers", omitted, omittedCount, warn);
|
|
108
|
+
return { catalogs, ids };
|
|
45
109
|
}
|
|
46
110
|
|
|
47
111
|
function parsePublicId(publicId: string): { prefix: string; nativeId: string } | undefined {
|
|
@@ -51,11 +115,13 @@ function parsePublicId(publicId: string): { prefix: string; nativeId: string } |
|
|
|
51
115
|
return { prefix: publicId.slice(0, slash), nativeId: publicId.slice(slash + 1) };
|
|
52
116
|
}
|
|
53
117
|
|
|
54
|
-
function lookupCatalogModel(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
118
|
+
function lookupCatalogModel(
|
|
119
|
+
prefix: string,
|
|
120
|
+
nativeId: string,
|
|
121
|
+
catalogs: CatalogMap,
|
|
122
|
+
): Model<Api> | undefined {
|
|
123
|
+
for (const provider of catalogCandidates(prefix, catalogs.get(prefix))) {
|
|
124
|
+
const model = getBuiltinModels(provider).find((candidate) => candidate.id === nativeId);
|
|
59
125
|
if (model) return model as Model<Api>;
|
|
60
126
|
}
|
|
61
127
|
return undefined;
|
|
@@ -78,6 +144,7 @@ export function mapDiscoveredModels(
|
|
|
78
144
|
ids: readonly unknown[],
|
|
79
145
|
baseUrl: string,
|
|
80
146
|
warn: Warn = console.warn,
|
|
147
|
+
catalogs: CatalogMap = new Map(),
|
|
81
148
|
): Model<Api>[] {
|
|
82
149
|
const models: Model<Api>[] = [];
|
|
83
150
|
const seen = new Set<string>();
|
|
@@ -85,11 +152,7 @@ export function mapDiscoveredModels(
|
|
|
85
152
|
let omittedCount = 0;
|
|
86
153
|
const omit = (id: string) => {
|
|
87
154
|
omittedCount++;
|
|
88
|
-
|
|
89
|
-
const display = sanitized.length > MAX_DIAGNOSTIC_ID_LENGTH
|
|
90
|
-
? `${sanitized.slice(0, MAX_DIAGNOSTIC_ID_LENGTH - 3)}...`
|
|
91
|
-
: sanitized;
|
|
92
|
-
if (omitted.length < MAX_DIAGNOSTIC_IDS) omitted.push(display);
|
|
155
|
+
if (omitted.length < MAX_DIAGNOSTIC_IDS) omitted.push(diagnostic(id));
|
|
93
156
|
};
|
|
94
157
|
|
|
95
158
|
for (const value of ids) {
|
|
@@ -98,8 +161,13 @@ export function mapDiscoveredModels(
|
|
|
98
161
|
continue;
|
|
99
162
|
}
|
|
100
163
|
const discovered = parsePublicId(value);
|
|
164
|
+
const discoveredCatalog = discovered ? catalogs.get(discovered.prefix) : undefined;
|
|
101
165
|
if (
|
|
102
|
-
discovered
|
|
166
|
+
discovered
|
|
167
|
+
&& (discovered.prefix === "openai"
|
|
168
|
+
|| discovered.prefix === "codex"
|
|
169
|
+
|| discoveredCatalog === "openai"
|
|
170
|
+
|| discoveredCatalog === "openai-codex")
|
|
103
171
|
&& discovered.nativeId.startsWith("gpt-")
|
|
104
172
|
&& discovered.nativeId.endsWith("-fast")
|
|
105
173
|
) {
|
|
@@ -109,7 +177,7 @@ export function mapDiscoveredModels(
|
|
|
109
177
|
if (seen.has(publicId)) continue;
|
|
110
178
|
seen.add(publicId);
|
|
111
179
|
const parsed = parsePublicId(publicId);
|
|
112
|
-
const source = parsed ? lookupCatalogModel(parsed.prefix, parsed.nativeId) : undefined;
|
|
180
|
+
const source = parsed ? lookupCatalogModel(parsed.prefix, parsed.nativeId, catalogs) : undefined;
|
|
113
181
|
const api = source ? SUPPORTED_APIS.get(source.api) : undefined;
|
|
114
182
|
if (!parsed || !source || !api) {
|
|
115
183
|
omit(value);
|
|
@@ -125,10 +193,7 @@ export function mapDiscoveredModels(
|
|
|
125
193
|
});
|
|
126
194
|
}
|
|
127
195
|
|
|
128
|
-
|
|
129
|
-
const suffix = omittedCount > omitted.length ? ` (+${omittedCount - omitted.length} more)` : "";
|
|
130
|
-
warn(`TinyLLM omitted unknown, malformed, or unsupported models: ${omitted.join(", ")}${suffix}`);
|
|
131
|
-
}
|
|
196
|
+
warnOmitted("unknown, malformed, or unsupported models", omitted, omittedCount, warn);
|
|
132
197
|
return models;
|
|
133
198
|
}
|
|
134
199
|
|
|
@@ -139,10 +204,17 @@ export async function discoverModels(
|
|
|
139
204
|
fetchImpl: typeof fetch = fetch,
|
|
140
205
|
warn: Warn = console.warn,
|
|
141
206
|
): Promise<Model<Api>[]> {
|
|
142
|
-
const
|
|
207
|
+
const root = normalizeBaseUrl(baseUrl);
|
|
208
|
+
const request = {
|
|
143
209
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
144
210
|
signal,
|
|
145
|
-
}
|
|
211
|
+
};
|
|
212
|
+
let modelKey: "configured_models" | "data" = "configured_models";
|
|
213
|
+
let response = await fetchImpl(`${root}/api/v1/models`, request);
|
|
214
|
+
if (response.status === 404) {
|
|
215
|
+
modelKey = "data";
|
|
216
|
+
response = await fetchImpl(`${root}/v1/models`, request);
|
|
217
|
+
}
|
|
146
218
|
if (!response.ok) throw new Error(`TinyLLM model discovery failed with HTTP ${response.status}`);
|
|
147
219
|
|
|
148
220
|
let payload: unknown;
|
|
@@ -152,15 +224,25 @@ export async function discoverModels(
|
|
|
152
224
|
signal.throwIfAborted();
|
|
153
225
|
throw new Error("TinyLLM model discovery returned invalid JSON", { cause: error });
|
|
154
226
|
}
|
|
155
|
-
if (!payload || typeof payload !== "object"
|
|
227
|
+
if (!payload || typeof payload !== "object") {
|
|
156
228
|
throw new Error("TinyLLM model discovery returned an invalid payload");
|
|
157
229
|
}
|
|
230
|
+
const body = payload as Record<string, unknown>;
|
|
231
|
+
const configuredModels = body[modelKey];
|
|
232
|
+
if (!Array.isArray(configuredModels)) {
|
|
233
|
+
throw new Error("TinyLLM model discovery returned an invalid payload");
|
|
234
|
+
}
|
|
235
|
+
const discovery = providerCatalogs(body.providers, warn);
|
|
158
236
|
return mapDiscoveredModels(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
237
|
+
[
|
|
238
|
+
...configuredModels.map((entry) =>
|
|
239
|
+
entry && typeof entry === "object" && "id" in entry ? (entry as { id?: unknown }).id : undefined,
|
|
240
|
+
),
|
|
241
|
+
...discovery.ids,
|
|
242
|
+
],
|
|
162
243
|
baseUrl,
|
|
163
244
|
warn,
|
|
245
|
+
discovery.catalogs,
|
|
164
246
|
);
|
|
165
247
|
}
|
|
166
248
|
|
|
@@ -260,7 +342,7 @@ export async function bootstrapProvider(provider: Provider<Api>, apiKey: string,
|
|
|
260
342
|
}
|
|
261
343
|
|
|
262
344
|
export function isFastCapable(model: Pick<Model<Api>, "provider" | "id"> | undefined): boolean {
|
|
263
|
-
return model?.provider === "tinyllm" && /^
|
|
345
|
+
return model?.provider === "tinyllm" && /^[^/]+\/gpt-.+/.test(model.id) && !model.id.endsWith("-fast");
|
|
264
346
|
}
|
|
265
347
|
|
|
266
348
|
export function rewriteFastPayload(
|
|
@@ -276,10 +358,20 @@ export function rewriteFastPayload(
|
|
|
276
358
|
return { ...request, model: `${request.model}-fast` };
|
|
277
359
|
}
|
|
278
360
|
|
|
361
|
+
const PROVIDER_STATUS = "model-provider";
|
|
279
362
|
const FAST_STATE = "tinyllm-fast-mode";
|
|
280
363
|
|
|
364
|
+
export function registerProviderStatus(pi: ExtensionAPI): void {
|
|
365
|
+
const update = (ctx: ExtensionContext, provider = ctx.model?.provider) =>
|
|
366
|
+
ctx.ui.setStatus(PROVIDER_STATUS, provider ? `[${provider}]` : undefined);
|
|
367
|
+
pi.on("session_start", (_event, ctx) => update(ctx));
|
|
368
|
+
pi.on("session_tree", (_event, ctx) => update(ctx));
|
|
369
|
+
pi.on("model_select", (event, ctx) => update(ctx, event.model.provider));
|
|
370
|
+
}
|
|
371
|
+
|
|
281
372
|
export function registerFastMode(pi: ExtensionAPI): void {
|
|
282
373
|
let enabled = false;
|
|
374
|
+
const updateStatus = (ctx: ExtensionContext) => ctx.ui.setStatus(FAST_STATE, enabled ? "fast" : undefined);
|
|
283
375
|
const restore = (ctx: ExtensionContext) => {
|
|
284
376
|
enabled = false;
|
|
285
377
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
@@ -287,6 +379,7 @@ export function registerFastMode(pi: ExtensionAPI): void {
|
|
|
287
379
|
enabled = (entry.data as { enabled?: unknown } | undefined)?.enabled === true;
|
|
288
380
|
}
|
|
289
381
|
}
|
|
382
|
+
updateStatus(ctx);
|
|
290
383
|
};
|
|
291
384
|
|
|
292
385
|
pi.on("session_start", (_event, ctx) => restore(ctx));
|
|
@@ -296,17 +389,19 @@ export function registerFastMode(pi: ExtensionAPI): void {
|
|
|
296
389
|
description: "Toggle TinyLLM fast routing for the selected OpenAI GPT model",
|
|
297
390
|
handler: async (_args, ctx) => {
|
|
298
391
|
if (!isFastCapable(ctx.model)) {
|
|
299
|
-
ctx.ui.notify("/fast is only available for TinyLLM
|
|
392
|
+
ctx.ui.notify("/fast is only available for TinyLLM OpenAI GPT models", "warning");
|
|
300
393
|
return;
|
|
301
394
|
}
|
|
302
395
|
enabled = !enabled;
|
|
303
396
|
pi.appendEntry(FAST_STATE, { enabled });
|
|
397
|
+
updateStatus(ctx);
|
|
304
398
|
ctx.ui.notify(`TinyLLM fast routing ${enabled ? "enabled" : "disabled"}`, "info");
|
|
305
399
|
},
|
|
306
400
|
});
|
|
307
401
|
}
|
|
308
402
|
|
|
309
403
|
export default async function tinyllmExtension(pi: ExtensionAPI): Promise<void> {
|
|
404
|
+
registerProviderStatus(pi);
|
|
310
405
|
registerFastMode(pi);
|
|
311
406
|
const baseUrl = process.env.TINYLLM_BASE_URL ?? DEFAULT_BASE_URL;
|
|
312
407
|
const provider = createTinyllmProvider({ baseUrl });
|