pi-provider-litellm 2.2.1 → 3.0.0

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/dist/discover.js DELETED
@@ -1,333 +0,0 @@
1
- import { isIP } from "node:net";
2
- import { getModels, getProviders } from "@earendil-works/pi-ai/compat";
3
- const DEFAULT_TIMEOUT_MS = 5000;
4
- const DEFAULT_CONTEXT_WINDOW = 128_000;
5
- const DEFAULT_MAX_TOKENS = 16_384;
6
- const KNOWN_PROVIDER_SET = new Set(getProviders());
7
- export function normalizeBaseUrl(input, allowInsecureHttp = false) {
8
- const url = new URL(input);
9
- const hostname = url.hostname.toLowerCase();
10
- const loopback = hostname === "localhost" || hostname === "[::1]" || (isIP(hostname) === 4 && hostname.startsWith("127."));
11
- if (url.protocol !== "https:" && !(url.protocol === "http:" && (loopback || allowInsecureHttp))) {
12
- throw new Error("LiteLLM base URL must use HTTPS except for loopback hosts");
13
- }
14
- return input.replace(/\/+$/, "").replace(/\/v1\/?$/i, "");
15
- }
16
- const RESPONSES_MODE_PATTERN = /^responses?$/i;
17
- function isResponsesMode(mode) {
18
- return mode != null && RESPONSES_MODE_PATTERN.test(mode);
19
- }
20
- function isChatStyleMode(mode) {
21
- return mode == null || mode === "chat" || isResponsesMode(mode);
22
- }
23
- // Matches both the conventional `anthropic/...` prefix and aliases that
24
- // LiteLLM deployments commonly assign to Anthropic-backed routes (e.g.
25
- // `google/claude-sonnet-4-6`, `opus-4.7`, `sonnet-4.6`, `haiku-4.5`). Without
26
- // the `cacheControlFormat: "anthropic"` flag, pi never relays cache_control
27
- // markers through the proxy, so prompt caching silently no-ops on Claude models.
28
- const ANTHROPIC_MODEL_PATTERN = /(?:^|[-_/.:])(?:anthropic\/|(?:claude|opus|sonnet|haiku)(?=$|[-_/.:]))/i;
29
- const MOONSHOT_MODEL_PATTERN = /^(moonshotai\/|moonshot\/|kimi[-/])/i;
30
- const FORCED_THINKING_MODEL_PATTERN = /(?:^|[-/])thinking(?:[-/]|$)/i;
31
- // Deployments expose the gpt-5.5 route under varying names (`llm-gateway/gpt-5.5`,
32
- // bare `gpt-5.5`, dated ids like `gpt-5.5-20260504143601`); match them all so the
33
- // tool+reasoning workaround survives route renames.
34
- const GPT55_MODEL_PATTERN = /(?:^|\/)gpt-5\.5(?:$|[-.])/i;
35
- export function isMoonshotModel(modelId) {
36
- return MOONSHOT_MODEL_PATTERN.test(modelId);
37
- }
38
- export function isGpt55Model(modelId) {
39
- return GPT55_MODEL_PATTERN.test(modelId);
40
- }
41
- export function shouldSuppressReasoningContent(modelId) {
42
- return isMoonshotModel(modelId) && !FORCED_THINKING_MODEL_PATTERN.test(modelId);
43
- }
44
- export function buildCompat(modelId) {
45
- if (isMoonshotModel(modelId)) {
46
- return {
47
- supportsStore: false,
48
- supportsDeveloperRole: false,
49
- supportsReasoningEffort: false,
50
- supportsStrictMode: false,
51
- maxTokensField: "max_tokens",
52
- };
53
- }
54
- if (ANTHROPIC_MODEL_PATTERN.test(modelId)) {
55
- return { supportsStore: false, cacheControlFormat: "anthropic" };
56
- }
57
- return { supportsStore: false };
58
- }
59
- function toKnownProvider(provider) {
60
- if (!provider)
61
- return undefined;
62
- const normalized = provider.toLowerCase();
63
- return KNOWN_PROVIDER_SET.has(normalized) ? normalized : undefined;
64
- }
65
- function findCatalogModel(id, ownedBy) {
66
- const prefixProvider = toKnownProvider(id.split("/")[0]);
67
- const lookupIds = catalogLookupIds(id);
68
- const candidates = [toKnownProvider(ownedBy), prefixProvider, lookupIds.length > 1 ? "anthropic" : undefined].filter((provider) => provider !== undefined);
69
- for (const provider of candidates) {
70
- const match = findCatalogModelInProvider(provider, lookupIds);
71
- if (match)
72
- return match;
73
- }
74
- for (const provider of getProviders()) {
75
- const match = findCatalogModelInProvider(provider, lookupIds);
76
- if (match)
77
- return match;
78
- }
79
- return undefined;
80
- }
81
- export function enrichCachedModel(model) {
82
- // ponytail: legacy cache lacks field provenance; add per-field cache provenance if strict preservation becomes necessary.
83
- if (!model.name.endsWith(" (no metadata)") ||
84
- model.reasoning ||
85
- model.thinkingLevelMap !== undefined ||
86
- model.input.length !== 1 ||
87
- model.input[0] !== "text" ||
88
- model.cost.input !== 0 ||
89
- model.cost.output !== 0 ||
90
- model.cost.cacheRead !== 0 ||
91
- model.cost.cacheWrite !== 0 ||
92
- model.cost.tiers !== undefined ||
93
- model.contextWindow !== DEFAULT_CONTEXT_WINDOW ||
94
- model.maxTokens !== DEFAULT_MAX_TOKENS) {
95
- return model;
96
- }
97
- const catalogModel = findCatalogModel(model.id);
98
- if (!catalogModel)
99
- return model;
100
- return {
101
- ...model,
102
- name: catalogModel.name,
103
- reasoning: catalogModel.reasoning,
104
- thinkingLevelMap: catalogModel.thinkingLevelMap,
105
- input: catalogModel.input,
106
- cost: catalogModel.cost,
107
- contextWindow: catalogModel.contextWindow,
108
- maxTokens: catalogModel.maxTokens,
109
- };
110
- }
111
- function catalogLookupIds(id) {
112
- const lookupIds = new Set([id]);
113
- const unprefixed = id.includes("/") ? id.slice(id.indexOf("/") + 1) : id;
114
- lookupIds.add(unprefixed);
115
- const anthropicAlias = unprefixed.toLowerCase().replaceAll(".", "-");
116
- const match = /^(?:claude-)?(opus|sonnet|haiku)-(\d+)-(\d+)$/.exec(anthropicAlias);
117
- if (match)
118
- lookupIds.add(`claude-${match[1]}-${match[2]}-${match[3]}`);
119
- if (anthropicAlias === "fable-5" || anthropicAlias === "opus-5")
120
- lookupIds.add(`claude-${anthropicAlias}`);
121
- return [...lookupIds];
122
- }
123
- function findCatalogModelInProvider(provider, lookupIds) {
124
- for (const lookupId of lookupIds) {
125
- const exact = getModels(provider).find((model) => model.id === lookupId);
126
- if (exact)
127
- return exact;
128
- const providerQualified = getModels(provider).find((model) => model.id === `${provider}/${lookupId}`);
129
- if (providerQualified)
130
- return providerQualified;
131
- }
132
- return undefined;
133
- }
134
- function mapModelInfoCost(info, fallback) {
135
- return {
136
- input: info.input_cost_per_token !== undefined ? info.input_cost_per_token * 1_000_000 : (fallback?.input ?? 0),
137
- output: info.output_cost_per_token !== undefined ? info.output_cost_per_token * 1_000_000 : (fallback?.output ?? 0),
138
- cacheRead: info.cache_read_input_token_cost !== undefined
139
- ? info.cache_read_input_token_cost * 1_000_000
140
- : (fallback?.cacheRead ?? 0),
141
- cacheWrite: info.cache_creation_input_token_cost !== undefined
142
- ? info.cache_creation_input_token_cost * 1_000_000
143
- : (fallback?.cacheWrite ?? 0),
144
- ...(fallback?.tiers ? { tiers: fallback.tiers } : {}),
145
- };
146
- }
147
- async function fetchJson(url, apiKey, options) {
148
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
149
- const response = await fetch(url, {
150
- headers: { ...options.headers, Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
151
- signal: options.signal
152
- ? AbortSignal.any([options.signal, AbortSignal.timeout(timeoutMs)])
153
- : AbortSignal.timeout(timeoutMs),
154
- });
155
- if (!response.ok)
156
- return { ok: false, status: response.status };
157
- const data = (await response.json());
158
- return { ok: true, data };
159
- }
160
- function mapReasoningEfforts(info) {
161
- const flags = [
162
- ["off", "none", info.supports_none_reasoning_effort],
163
- ["minimal", "minimal", info.supports_minimal_reasoning_effort],
164
- ["low", "low", info.supports_low_reasoning_effort],
165
- ["medium", "medium", info.supports_medium_reasoning_effort],
166
- ["high", "high", info.supports_high_reasoning_effort],
167
- ["xhigh", "xhigh", info.supports_xhigh_reasoning_effort],
168
- ["max", "max", info.supports_max_reasoning_effort],
169
- ];
170
- const map = Object.fromEntries(flags
171
- .filter(([, , supported]) => supported !== undefined)
172
- .map(([level, value, supported]) => [level, supported ? value : null]));
173
- return Object.keys(map).length > 0 ? map : undefined;
174
- }
175
- function mapFromModelInfo(entry) {
176
- const id = entry.model_name;
177
- if (!id)
178
- return undefined;
179
- const info = entry.model_info ?? {};
180
- if (!isChatStyleMode(info.mode))
181
- return undefined;
182
- const responsesMode = isResponsesMode(info.mode);
183
- const catalogModel = findCatalogModel(id);
184
- const reasoningEffortMap = mapReasoningEfforts(info);
185
- const thinkingLevelMap = catalogModel?.thinkingLevelMap || reasoningEffortMap
186
- ? { ...catalogModel?.thinkingLevelMap, ...reasoningEffortMap }
187
- : undefined;
188
- return {
189
- id,
190
- name: id,
191
- reasoning: info.supports_reasoning ?? false,
192
- ...(thinkingLevelMap ? { thinkingLevelMap } : {}),
193
- input: info.supports_vision ? ["text", "image"] : ["text"],
194
- cost: mapModelInfoCost(info, catalogModel?.cost),
195
- contextWindow: info.max_input_tokens ?? DEFAULT_CONTEXT_WINDOW,
196
- maxTokens: info.max_output_tokens ?? DEFAULT_MAX_TOKENS,
197
- compat: buildCompat(id),
198
- ...(responsesMode ? { api: "openai-responses" } : {}),
199
- };
200
- }
201
- function mapFromHealthModelInfo(entry, fallbackId) {
202
- if (entry.model_name || !fallbackId)
203
- return mapFromModelInfo(entry);
204
- const model = mapFromModelInfo({ ...entry, model_name: fallbackId });
205
- if (model)
206
- delete model.thinkingLevelMap;
207
- return model;
208
- }
209
- function mapFromHealthEndpoint(entry) {
210
- const id = entry.model;
211
- if (!id)
212
- return undefined;
213
- const catalogModel = findCatalogModel(id);
214
- return {
215
- id,
216
- name: catalogModel?.name ?? id,
217
- reasoning: catalogModel?.reasoning ?? false,
218
- thinkingLevelMap: catalogModel?.thinkingLevelMap,
219
- input: catalogModel?.input ?? ["text"],
220
- cost: catalogModel?.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
221
- contextWindow: catalogModel?.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
222
- maxTokens: catalogModel?.maxTokens ?? DEFAULT_MAX_TOKENS,
223
- compat: buildCompat(id),
224
- };
225
- }
226
- function mapFromModelsList(entry) {
227
- const id = entry.id;
228
- if (!id)
229
- return undefined;
230
- const catalogModel = findCatalogModel(id, entry.owned_by);
231
- return {
232
- id,
233
- name: catalogModel?.name ?? `${id} (no metadata)`,
234
- reasoning: catalogModel?.reasoning ?? false,
235
- thinkingLevelMap: catalogModel?.thinkingLevelMap,
236
- input: catalogModel?.input ?? ["text"],
237
- cost: catalogModel?.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
238
- contextWindow: catalogModel?.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
239
- maxTokens: catalogModel?.maxTokens ?? DEFAULT_MAX_TOKENS,
240
- compat: buildCompat(id),
241
- };
242
- }
243
- async function discoverFromHealth(base, apiKey, options) {
244
- const progress = options.silent ? undefined : options.onProgress;
245
- progress?.("Querying /health endpoint...");
246
- const healthResult = await fetchJson(`${base}/health`, apiKey, options);
247
- if (!healthResult.ok)
248
- return [];
249
- const endpoints = (healthResult.data.healthy_endpoints ?? []).filter((entry) => entry.model || entry.model_id);
250
- progress?.(`Discovered ${endpoints.length} model endpoints, fetching details...`);
251
- let completed = 0;
252
- const models = await Promise.all(endpoints.map(async (endpoint) => {
253
- let model = mapFromHealthEndpoint(endpoint);
254
- if (endpoint.model_id) {
255
- const infoResult = await fetchJson(`${base}/model/info?litellm_model_id=${encodeURIComponent(endpoint.model_id)}`, apiKey, options);
256
- const entry = infoResult.ok ? infoResult.data.data?.[0] : undefined;
257
- if (entry)
258
- model = mapFromHealthModelInfo(entry, endpoint.model);
259
- }
260
- completed++;
261
- if (completed % 10 === 0 || completed === endpoints.length) {
262
- progress?.(`Fetched ${completed}/${endpoints.length} models...`);
263
- }
264
- return model;
265
- }));
266
- return models.filter((model) => model !== undefined);
267
- }
268
- function deduplicateModels(models) {
269
- const seen = new Set();
270
- return models.filter((m) => {
271
- if (seen.has(m.id))
272
- return false;
273
- seen.add(m.id);
274
- return true;
275
- });
276
- }
277
- export async function discoverModels(baseUrl, apiKey, options = {}) {
278
- const base = normalizeBaseUrl(baseUrl, options.allowInsecureHttp);
279
- const progress = options.silent ? undefined : options.onProgress;
280
- progress?.("Querying /model/info endpoint...");
281
- const infoResult = await fetchJson(`${base}/model/info`, apiKey, options);
282
- if (infoResult.ok) {
283
- const entries = new Map();
284
- for (const entry of infoResult.data.data ?? []) {
285
- if (!entry.model_name)
286
- continue;
287
- const previous = entries.get(entry.model_name);
288
- entries.set(entry.model_name, {
289
- ...previous,
290
- ...entry,
291
- model_info: { ...previous?.model_info, ...entry.model_info },
292
- });
293
- }
294
- let models = [...entries.values()].map(mapFromModelInfo).filter((m) => m !== undefined);
295
- // LiteLLM's /model/info does NOT expand wildcard model_name entries (e.g.
296
- // "lemonade/*" backed by model: openai/* + check_provider_endpoint: true)
297
- // — it returns the literal wildcard only. The discovered ids live in
298
- // /v1/models instead. When /model/info contains any wildcard id, also query
299
- // /v1/models and merge the expanded (non-wildcard) entries in, dropping the
300
- // raw wildcard row so it doesn't surface as a phantom model choice.
301
- // Ref: docs.litellm.ai/docs/proxy/model_discovery
302
- if (models.some((m) => m.id.includes("*"))) {
303
- progress?.("/model/info has wildcard entries, expanding via /v1/models...");
304
- const listResult = await fetchJson(`${base}/v1/models`, apiKey, options);
305
- if (listResult.ok) {
306
- const expanded = (listResult.data.data ?? [])
307
- .map(mapFromModelsList)
308
- .filter((m) => m !== undefined && !m.id.includes("*"));
309
- const seen = new Set(models.map((m) => m.id));
310
- models = [...models.filter((m) => !m.id.includes("*")), ...expanded.filter((m) => !seen.has(m.id))];
311
- }
312
- }
313
- return { source: "model_info", models: deduplicateModels(models) };
314
- }
315
- if (![401, 403, 404].includes(infoResult.status)) {
316
- throw new Error(`/model/info returned ${infoResult.status}`);
317
- }
318
- progress?.("/model/info unavailable, trying /v1/models...");
319
- const listResult = await fetchJson(`${base}/v1/models`, apiKey, options);
320
- if (!listResult.ok) {
321
- if ([401, 403, 404].includes(listResult.status)) {
322
- progress?.("/v1/models unavailable, falling back to /health endpoint...");
323
- const models = await discoverFromHealth(base, apiKey, options);
324
- if (models.length > 0)
325
- return { source: "health", models: deduplicateModels(models) };
326
- }
327
- throw new Error(`/v1/models returned ${listResult.status}`);
328
- }
329
- const models = (listResult.data.data ?? [])
330
- .map(mapFromModelsList)
331
- .filter((m) => m !== undefined);
332
- return { source: "models_list", models: deduplicateModels(models) };
333
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,8 +0,0 @@
1
- import { getGcloudToken } from "./gcloud-token.js";
2
- const token = await getGcloudToken();
3
- if (token) {
4
- process.stdout.write(token);
5
- }
6
- else {
7
- process.exitCode = 1;
8
- }
@@ -1,7 +0,0 @@
1
- export declare const CACHE_TTL_MS: number;
2
- export declare const GCLOUD_TOKEN_CACHE_KEY = "gcloud-adc";
3
- export declare function isGcloudTokenAuthEnabled(): boolean;
4
- export declare function getGcloudTokenCommand(): string;
5
- export declare function getGcloudTokenCacheKey(): Promise<string | null>;
6
- export declare function getGcloudToken(): Promise<string | null>;
7
- export declare function resetGcloudTokenCache(): void;
@@ -1,133 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { readFile } from "node:fs/promises";
3
- import { homedir } from "node:os";
4
- import { join } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- export const CACHE_TTL_MS = 50 * 60 * 1000;
7
- export const GCLOUD_TOKEN_CACHE_KEY = "gcloud-adc";
8
- const ADC_FILENAME = "application_default_credentials.json";
9
- let cachedToken = null;
10
- let cachedAt = 0;
11
- let cachedTokenKey = null;
12
- function isAuthorizedUserCredentials(credentials) {
13
- return (credentials.type === "authorized_user" &&
14
- typeof credentials.client_id === "string" &&
15
- typeof credentials.client_secret === "string" &&
16
- typeof credentials.refresh_token === "string");
17
- }
18
- export function isGcloudTokenAuthEnabled() {
19
- const raw = process.env.LITELLM_GCLOUD_TOKEN_AUTH;
20
- return raw !== undefined && raw !== "" && raw !== "0";
21
- }
22
- function shellQuote(value) {
23
- return `'${value.replaceAll("'", "'\\''")}'`;
24
- }
25
- export function getGcloudTokenCommand() {
26
- const cliPath = fileURLToPath(new URL("./gcloud-token-cli.js", import.meta.url));
27
- return `!${shellQuote(process.execPath)} ${shellQuote(cliPath)}`;
28
- }
29
- function getAdcPath() {
30
- const envPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
31
- if (envPath)
32
- return envPath;
33
- const candidates = [join(homedir(), ".config", "gcloud", ADC_FILENAME)];
34
- if (process.env.APPDATA)
35
- candidates.push(join(process.env.APPDATA, "gcloud", ADC_FILENAME));
36
- return candidates.find((path) => existsSync(path)) ?? null;
37
- }
38
- function getCredentialsCacheKey(credentials) {
39
- if (isAuthorizedUserCredentials(credentials)) {
40
- return `${GCLOUD_TOKEN_CACHE_KEY}:authorized_user:${credentials.client_id}:${credentials.refresh_token}`;
41
- }
42
- return null;
43
- }
44
- async function readCredentials(path) {
45
- try {
46
- return JSON.parse(await readFile(path, "utf8"));
47
- }
48
- catch {
49
- return null;
50
- }
51
- }
52
- export async function getGcloudTokenCacheKey() {
53
- const adcPath = getAdcPath();
54
- if (!adcPath) {
55
- console.warn("LiteLLM gcloud auth: No Google ADC file found. Set GOOGLE_APPLICATION_CREDENTIALS or run `gcloud auth application-default login`.");
56
- return null;
57
- }
58
- const credentials = await readCredentials(adcPath);
59
- if (!credentials) {
60
- console.warn(`LiteLLM gcloud auth: Failed to read ADC file: ${adcPath}`);
61
- return null;
62
- }
63
- const cacheKey = getCredentialsCacheKey(credentials);
64
- if (cacheKey)
65
- return cacheKey;
66
- if (credentials.type === "service_account") {
67
- console.warn("LiteLLM gcloud auth: Service account credentials are not supported; use authorized_user ADC.");
68
- return null;
69
- }
70
- console.warn(`LiteLLM gcloud auth: Unknown credential type: ${credentials.type ?? "missing"}`);
71
- return null;
72
- }
73
- async function exchangeRefreshToken(credentials) {
74
- const body = new URLSearchParams({
75
- grant_type: "refresh_token",
76
- client_id: credentials.client_id,
77
- client_secret: credentials.client_secret,
78
- refresh_token: credentials.refresh_token,
79
- }).toString();
80
- try {
81
- const response = await fetch("https://oauth2.googleapis.com/token", {
82
- method: "POST",
83
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
84
- body,
85
- signal: AbortSignal.timeout(10_000),
86
- });
87
- if (!response.ok) {
88
- console.warn(`LiteLLM gcloud auth: Token exchange failed (${response.status})`);
89
- return null;
90
- }
91
- const data = (await response.json());
92
- return typeof data.access_token === "string" && data.access_token ? data.access_token : null;
93
- }
94
- catch (error) {
95
- console.warn(`LiteLLM gcloud auth: Token exchange failed: ${error instanceof Error ? error.message : String(error)}`);
96
- return null;
97
- }
98
- }
99
- export async function getGcloudToken() {
100
- const adcPath = getAdcPath();
101
- if (!adcPath) {
102
- console.warn("LiteLLM gcloud auth: No Google ADC file found. Set GOOGLE_APPLICATION_CREDENTIALS or run `gcloud auth application-default login`.");
103
- return null;
104
- }
105
- const credentials = await readCredentials(adcPath);
106
- if (!credentials) {
107
- console.warn(`LiteLLM gcloud auth: Failed to read ADC file: ${adcPath}`);
108
- return null;
109
- }
110
- const cacheKey = getCredentialsCacheKey(credentials);
111
- if (cacheKey && cachedToken && cachedTokenKey === cacheKey && Date.now() - cachedAt < CACHE_TTL_MS)
112
- return cachedToken;
113
- if (isAuthorizedUserCredentials(credentials)) {
114
- const token = await exchangeRefreshToken(credentials);
115
- if (token) {
116
- cachedToken = token;
117
- cachedTokenKey = cacheKey;
118
- cachedAt = Date.now();
119
- }
120
- return token;
121
- }
122
- if (credentials.type === "service_account") {
123
- console.warn("LiteLLM gcloud auth: Service account credentials are not supported; use authorized_user ADC.");
124
- return null;
125
- }
126
- console.warn(`LiteLLM gcloud auth: Unknown credential type: ${credentials.type ?? "missing"}`);
127
- return null;
128
- }
129
- export function resetGcloudTokenCache() {
130
- cachedToken = null;
131
- cachedTokenKey = null;
132
- cachedAt = 0;
133
- }
package/dist/index.d.ts DELETED
@@ -1,2 +0,0 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- export default function (pi: ExtensionAPI): Promise<void>;