@preapexis/pi-kit 1.2.0 → 1.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preapexis/pi-kit",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Personal Pi coding-agent kit with safety extensions, status UI, prompt workflows, skills, and themes.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -1,305 +0,0 @@
1
- // cSpell:words litellm preapexis deepseek
2
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
-
4
- type LiteLlmModelInfo = {
5
- model_name?: string;
6
- model_info?: {
7
- id?: string;
8
- name?: string;
9
- display_name?: string;
10
- max_tokens?: number;
11
- max_input_tokens?: number;
12
- context_window?: number;
13
- input_cost_per_token?: number;
14
- output_cost_per_token?: number;
15
- supports_vision?: boolean;
16
- };
17
- };
18
-
19
- type OpenAiModel = {
20
- id?: string;
21
- };
22
-
23
- type ModelPayload = {
24
- data?: Array<LiteLlmModelInfo | OpenAiModel>;
25
- };
26
-
27
- type PiModel = {
28
- id: string;
29
- name: string;
30
- reasoning: boolean;
31
- input: Array<"text" | "image">;
32
- cost: {
33
- input: number;
34
- output: number;
35
- cacheRead: number;
36
- cacheWrite: number;
37
- };
38
- contextWindow: number;
39
- maxTokens: number;
40
- };
41
-
42
- const PROVIDER_ID = "litellm";
43
- const DEFAULT_BASE_URL = "http://localhost:4000/v1";
44
-
45
- export default async function (pi: ExtensionAPI): Promise<void> {
46
- const baseUrl = normalizeBaseUrl(
47
- process.env.LITELLM_BASE_URL ?? DEFAULT_BASE_URL
48
- );
49
-
50
- async function registerLiteLlmProvider(): Promise<number> {
51
- const models = await discoverModels(baseUrl);
52
-
53
- if (models.length === 0) {
54
- return 0;
55
- }
56
-
57
- pi.registerProvider(PROVIDER_ID, {
58
- name: "LiteLLM",
59
- baseUrl,
60
- apiKey: "$LITELLM_API_KEY",
61
- api: "openai-completions",
62
- models
63
- });
64
-
65
- return models.length;
66
- }
67
-
68
- try {
69
- const count = await registerLiteLlmProvider();
70
-
71
- if (count > 0) {
72
- console.log(`[litellm] Registered ${count} models from ${baseUrl}`);
73
- } else {
74
- console.log("[litellm] No models found. Provider was not registered.");
75
- }
76
- } catch (error) {
77
- console.log(
78
- [
79
- "[litellm] Provider skipped.",
80
- error instanceof Error ? error.message : String(error),
81
- "Start LiteLLM and run /litellm-refresh."
82
- ].join("\n")
83
- );
84
- }
85
-
86
- pi.registerCommand("litellm-refresh", {
87
- description: "Refresh LiteLLM models from the LiteLLM proxy",
88
- handler: async (_args, ctx) => {
89
- try {
90
- const count = await registerLiteLlmProvider();
91
-
92
- if (count === 0) {
93
- ctx.ui.notify(
94
- [
95
- "LiteLLM refresh finished, but no models were found.",
96
- "",
97
- `Base URL: ${baseUrl}`,
98
- "",
99
- "Make sure LiteLLM is running."
100
- ].join("\n"),
101
- "warning"
102
- );
103
-
104
- return;
105
- }
106
-
107
- ctx.ui.notify(
108
- [
109
- "LiteLLM models refreshed.",
110
- "",
111
- `Models found: ${count}`,
112
- `Base URL: ${baseUrl}`,
113
- "",
114
- "Run /model to select a LiteLLM model."
115
- ].join("\n"),
116
- "info"
117
- );
118
- } catch (error) {
119
- ctx.ui.notify(
120
- [
121
- "LiteLLM model refresh failed.",
122
- "",
123
- error instanceof Error ? error.message : String(error)
124
- ].join("\n"),
125
- "error"
126
- );
127
- }
128
- }
129
- });
130
- }
131
-
132
- function normalizeBaseUrl(value: string): string {
133
- return value.replace(/\/+$/, "");
134
- }
135
-
136
- function getProxyRoot(baseUrl: string): string {
137
- return baseUrl.endsWith("/v1") ? baseUrl.slice(0, -3) : baseUrl;
138
- }
139
-
140
- async function discoverModels(baseUrl: string): Promise<PiModel[]> {
141
- const proxyRoot = getProxyRoot(baseUrl);
142
-
143
- const endpoints = [
144
- `${proxyRoot}/v1/model/info`,
145
- `${proxyRoot}/model/info`,
146
- `${baseUrl}/models`
147
- ];
148
-
149
- for (const endpoint of endpoints) {
150
- try {
151
- const payload = await fetchModelPayload(endpoint);
152
- const models = payloadToPiModels(payload);
153
-
154
- if (models.length > 0) {
155
- return models;
156
- }
157
- } catch {
158
- // Try next endpoint.
159
- }
160
- }
161
-
162
- return getFallbackModels();
163
- }
164
-
165
- async function fetchModelPayload(url: string): Promise<ModelPayload> {
166
- const headers: Record<string, string> = {
167
- accept: "application/json"
168
- };
169
-
170
- const apiKey = process.env.LITELLM_API_KEY;
171
-
172
- if (apiKey) {
173
- headers.authorization = `Bearer ${apiKey}`;
174
- }
175
-
176
- const response = await fetch(url, {
177
- method: "GET",
178
- headers
179
- });
180
-
181
- if (!response.ok) {
182
- throw new Error(`${url} returned HTTP ${response.status}`);
183
- }
184
-
185
- return (await response.json()) as ModelPayload;
186
- }
187
-
188
- function payloadToPiModels(payload: ModelPayload): PiModel[] {
189
- const data = Array.isArray(payload.data) ? payload.data : [];
190
- const seen = new Set<string>();
191
- const models: PiModel[] = [];
192
-
193
- for (const item of data) {
194
- const model = modelFromPayloadItem(item);
195
-
196
- if (!model) continue;
197
- if (seen.has(model.id)) continue;
198
-
199
- seen.add(model.id);
200
- models.push(model);
201
- }
202
-
203
- return models.sort((a, b) => a.id.localeCompare(b.id));
204
- }
205
-
206
- function modelFromPayloadItem(
207
- item: LiteLlmModelInfo | OpenAiModel
208
- ): PiModel | undefined {
209
- const modelInfo = "model_info" in item ? item.model_info : undefined;
210
-
211
- const id =
212
- "model_name" in item && item.model_name
213
- ? item.model_name
214
- : "id" in item && item.id
215
- ? item.id
216
- : modelInfo?.id;
217
-
218
- if (!id) return undefined;
219
-
220
- const name = modelInfo?.display_name ?? modelInfo?.name ?? id;
221
-
222
- const contextWindow =
223
- modelInfo?.context_window ?? modelInfo?.max_input_tokens ?? 128000;
224
-
225
- const maxTokens = modelInfo?.max_tokens ?? 4096;
226
-
227
- const supportsVision =
228
- modelInfo?.supports_vision === true || looksLikeVisionModel(id);
229
-
230
- return {
231
- id,
232
- name,
233
- reasoning: looksLikeReasoningModel(id),
234
- input: supportsVision ? ["text", "image"] : ["text"],
235
- cost: {
236
- input: costPerMillion(modelInfo?.input_cost_per_token),
237
- output: costPerMillion(modelInfo?.output_cost_per_token),
238
- cacheRead: 0,
239
- cacheWrite: 0
240
- },
241
- contextWindow,
242
- maxTokens
243
- };
244
- }
245
-
246
- function costPerMillion(value: number | undefined): number {
247
- if (typeof value !== "number" || Number.isNaN(value)) {
248
- return 0;
249
- }
250
-
251
- return value * 1_000_000;
252
- }
253
-
254
- function looksLikeVisionModel(id: string): boolean {
255
- const lower = id.toLowerCase();
256
-
257
- return (
258
- lower.includes("vision") ||
259
- lower.includes("vl") ||
260
- lower.includes("gpt-4o") ||
261
- lower.includes("gemini") ||
262
- lower.includes("claude")
263
- );
264
- }
265
-
266
- function looksLikeReasoningModel(id: string): boolean {
267
- const lower = id.toLowerCase();
268
-
269
- return (
270
- lower.includes("reason") ||
271
- lower.includes("thinking") ||
272
- lower.includes("o1") ||
273
- lower.includes("o3") ||
274
- lower.includes("o4") ||
275
- lower.includes("r1") ||
276
- lower.includes("deepseek")
277
- );
278
- }
279
-
280
- function getFallbackModels(): PiModel[] {
281
- const raw = process.env.LITELLM_MODELS;
282
-
283
- if (!raw?.trim()) {
284
- return [];
285
- }
286
-
287
- return raw
288
- .split(",")
289
- .map((value) => value.trim())
290
- .filter(Boolean)
291
- .map((id) => ({
292
- id,
293
- name: id,
294
- reasoning: looksLikeReasoningModel(id),
295
- input: looksLikeVisionModel(id) ? ["text", "image"] : ["text"],
296
- cost: {
297
- input: 0,
298
- output: 0,
299
- cacheRead: 0,
300
- cacheWrite: 0
301
- },
302
- contextWindow: 128000,
303
- maxTokens: 4096
304
- }));
305
- }