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