pi-provider-litellm 1.2.7 → 1.2.8
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 +17 -33
- package/dist/discover.d.ts +4 -0
- package/dist/discover.js +49 -25
- package/dist/index.js +107 -10
- package/dist/mcp-tools.js +1 -45
- package/package.json +3 -5
package/README.md
CHANGED
|
@@ -44,6 +44,21 @@ You can also run `/login`, select `Use a subscription`, then select `LiteLLM`.
|
|
|
44
44
|
|
|
45
45
|
You'll be prompted for the base URL and API key. Credentials are persisted to `~/.pi/agent/auth.json`.
|
|
46
46
|
|
|
47
|
+
#### Enterprise SSO login
|
|
48
|
+
|
|
49
|
+
If your LiteLLM proxy requires SSO/OAuth authentication (enterprise deployments), you can authenticate via a browser SSO flow and optionally pair the resulting JWT with a stable virtual key:
|
|
50
|
+
|
|
51
|
+
1. Run `/login litellm` inside pi
|
|
52
|
+
2. Enter the proxy URL
|
|
53
|
+
3. At the login method prompt, enter `2` for SSO / Enterprise JWT
|
|
54
|
+
4. Your default browser opens the LiteLLM SSO login URL (e.g. `https://litellm.your-domain.com/sso/key/generate`) automatically — the URL is also displayed in case it can't be opened. Authenticate via SSO
|
|
55
|
+
5. Copy your token from the LiteLLM UI and paste it at the prompt (copying a full `Bearer ...` header value is fine — the prefix is stripped automatically)
|
|
56
|
+
6. When prompted to generate a virtual key, press Enter to accept (recommended) or enter `n` to use the JWT directly
|
|
57
|
+
|
|
58
|
+
When you generate a virtual key, the resulting `sk-...` key is stored as your credential and used for all API requests. If the proxy's key policy attaches an expiry to the generated key, Pi will prompt you to re-authenticate when it nears expiry; otherwise the key is treated as permanent until revoked in LiteLLM.
|
|
59
|
+
|
|
60
|
+
When using a JWT directly, the extension reads its `exp` claim and Pi will prompt you to re-authenticate when the token nears expiry. Run `/login litellm` again to refresh.
|
|
61
|
+
|
|
47
62
|
### Option B — environment variables
|
|
48
63
|
|
|
49
64
|
```bash
|
|
@@ -100,39 +115,6 @@ If your LiteLLM proxy exposes `/v1/skills`, enabled skills are fetched before ea
|
|
|
100
115
|
- `litellm_skill_create`
|
|
101
116
|
- `litellm_skill_delete`
|
|
102
117
|
|
|
103
|
-
## Mocked LiteLLM smoke workflow
|
|
104
|
-
|
|
105
|
-
The `LiteLLM Smoke` GitHub Actions workflow starts VidaiMock and a real LiteLLM proxy on the runner. LiteLLM exposes OpenAI-compatible and Anthropic routes whose upstreams are served by VidaiMock, then this extension's smoke runner discovers those models through LiteLLM and sends `/v1/chat/completions` requests through the proxy.
|
|
106
|
-
|
|
107
|
-
This keeps the LiteLLM integration path under test but does not call real LLM APIs. No provider API keys or GitHub Models permission are required. The workflow also runs a non-interactive Pi CLI smoke with `--list-models` and `-p` so extension loading, model discovery, and a real completion path are covered without opening the TUI.
|
|
108
|
-
|
|
109
|
-
## Development
|
|
110
|
-
|
|
111
|
-
This package requires Node.js `>=22.19.0`. CI currently uses Node `24.16.0`.
|
|
112
|
-
|
|
113
|
-
```bash
|
|
114
|
-
npm ci
|
|
115
|
-
npm run check
|
|
116
|
-
npm run clean && npm run build
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
`npm run check` runs Biome, type checking, and the Vitest suite. Runtime changes must be built before local Pi smoke checks because the extension entrypoint is `./dist/index.js`.
|
|
120
|
-
|
|
121
|
-
Before changing package contents or dependency policy, also run:
|
|
122
|
-
|
|
123
|
-
```bash
|
|
124
|
-
npm run supply-chain:guard
|
|
125
|
-
npm pack --dry-run
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
The published npm package should contain only `dist`, `README.md`, and `LICENSE`.
|
|
129
|
-
|
|
130
|
-
## Release
|
|
131
|
-
|
|
132
|
-
Releases are driven by semver tags named `v*.*.*`. The GitHub release workflow installs from the lockfile, runs the checks, builds `dist`, verifies the package tarball, publishes to npm with provenance, and creates a GitHub release.
|
|
133
|
-
|
|
134
|
-
Before tagging a release, keep `package.json` and `package-lock.json` versions in sync and verify the dry-run package contents.
|
|
135
|
-
|
|
136
118
|
## Slash commands
|
|
137
119
|
|
|
138
120
|
- `/litellm-refresh` — force re-fetch the model list, ignoring cache
|
|
@@ -153,6 +135,8 @@ If the cache is older than 24 hours, the extension refreshes it in the backgroun
|
|
|
153
135
|
| Discovery times out | Increase `LITELLM_DISCOVERY_TIMEOUT_MS` or set `LITELLM_OFFLINE=1` to fall back on cached models |
|
|
154
136
|
| `401 Token expired` | Set `LITELLM_API_KEY_HELPER`. |
|
|
155
137
|
| No models with gcloud auth | Verify `gcloud auth application-default login` has been run or set `GOOGLE_APPLICATION_CREDENTIALS` to an `authorized_user` ADC file |
|
|
138
|
+
| Enterprise SSO login shows "virtual key generation failed" | The LiteLLM instance may lack a database (`/key/generate` requires one), your user account may lack key-generation permission, or the request timed out; the JWT is used directly as a fallback |
|
|
139
|
+
| Enterprise SSO token prompt fails with "SSO token is required" | The token field was left empty — paste the token copied from the LiteLLM UI |
|
|
156
140
|
| MCP tools not showing | Verify the proxy exposes `/mcp-rest/tools/list` and run `/litellm-refresh` after fixing the proxy |
|
|
157
141
|
| Skills not affecting prompts | Verify the proxy exposes `/v1/skills` and returns enabled skills |
|
|
158
142
|
|
package/dist/discover.d.ts
CHANGED
|
@@ -4,4 +4,8 @@ export declare function normalizeBaseUrl(input: string): string;
|
|
|
4
4
|
export declare function isMoonshotModel(modelId: string): boolean;
|
|
5
5
|
export declare function shouldSuppressReasoningContent(modelId: string): boolean;
|
|
6
6
|
export declare function buildCompat(modelId: string): ProviderModelConfig["compat"];
|
|
7
|
+
export declare function withTimeout(timeoutMs: number, signal?: AbortSignal): {
|
|
8
|
+
signal: AbortSignal;
|
|
9
|
+
cancel: () => void;
|
|
10
|
+
};
|
|
7
11
|
export declare function discoverModels(baseUrl: string, apiKey: string, options?: DiscoveryOptions): Promise<DiscoveryResult>;
|
package/dist/discover.js
CHANGED
|
@@ -45,22 +45,53 @@ function toKnownProvider(provider) {
|
|
|
45
45
|
}
|
|
46
46
|
function findCatalogModel(id, ownedBy) {
|
|
47
47
|
const prefixProvider = toKnownProvider(id.split("/")[0]);
|
|
48
|
-
const
|
|
48
|
+
const lookupIds = catalogLookupIds(id);
|
|
49
|
+
const candidates = [toKnownProvider(ownedBy), prefixProvider, lookupIds.length > 1 ? "anthropic" : undefined].filter((provider) => provider !== undefined);
|
|
49
50
|
for (const provider of candidates) {
|
|
50
|
-
const
|
|
51
|
-
if (
|
|
52
|
-
return
|
|
53
|
-
const providerQualified = getModels(provider).find((model) => model.id === `${provider}/${id}`);
|
|
54
|
-
if (providerQualified)
|
|
55
|
-
return providerQualified;
|
|
51
|
+
const match = findCatalogModelInProvider(provider, lookupIds);
|
|
52
|
+
if (match)
|
|
53
|
+
return match;
|
|
56
54
|
}
|
|
57
55
|
for (const provider of getProviders()) {
|
|
58
|
-
const
|
|
56
|
+
const match = findCatalogModelInProvider(provider, lookupIds);
|
|
57
|
+
if (match)
|
|
58
|
+
return match;
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
function catalogLookupIds(id) {
|
|
63
|
+
const lookupIds = new Set([id]);
|
|
64
|
+
const unprefixed = id.includes("/") ? id.slice(id.indexOf("/") + 1) : id;
|
|
65
|
+
lookupIds.add(unprefixed);
|
|
66
|
+
const anthropicAlias = unprefixed.toLowerCase().replaceAll(".", "-");
|
|
67
|
+
const match = /^(?:claude-)?(opus|sonnet|haiku)-(\d+)-(\d+)$/.exec(anthropicAlias);
|
|
68
|
+
if (match)
|
|
69
|
+
lookupIds.add(`claude-${match[1]}-${match[2]}-${match[3]}`);
|
|
70
|
+
return [...lookupIds];
|
|
71
|
+
}
|
|
72
|
+
function findCatalogModelInProvider(provider, lookupIds) {
|
|
73
|
+
for (const lookupId of lookupIds) {
|
|
74
|
+
const exact = getModels(provider).find((model) => model.id === lookupId);
|
|
59
75
|
if (exact)
|
|
60
76
|
return exact;
|
|
77
|
+
const providerQualified = getModels(provider).find((model) => model.id === `${provider}/${lookupId}`);
|
|
78
|
+
if (providerQualified)
|
|
79
|
+
return providerQualified;
|
|
61
80
|
}
|
|
62
81
|
return undefined;
|
|
63
82
|
}
|
|
83
|
+
function mapModelInfoCost(info, fallback) {
|
|
84
|
+
return {
|
|
85
|
+
input: info.input_cost_per_token !== undefined ? info.input_cost_per_token * 1_000_000 : (fallback?.input ?? 0),
|
|
86
|
+
output: info.output_cost_per_token !== undefined ? info.output_cost_per_token * 1_000_000 : (fallback?.output ?? 0),
|
|
87
|
+
cacheRead: info.cache_read_input_token_cost !== undefined
|
|
88
|
+
? info.cache_read_input_token_cost * 1_000_000
|
|
89
|
+
: (fallback?.cacheRead ?? 0),
|
|
90
|
+
cacheWrite: info.cache_creation_input_token_cost !== undefined
|
|
91
|
+
? info.cache_creation_input_token_cost * 1_000_000
|
|
92
|
+
: (fallback?.cacheWrite ?? 0),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
64
95
|
function getFallbackProviderAndModel(id, ownedBy) {
|
|
65
96
|
const [prefix, ...rest] = id.split("/");
|
|
66
97
|
const prefixProvider = toKnownProvider(prefix);
|
|
@@ -75,7 +106,7 @@ function findModelsDevModel(catalog, id, ownedBy) {
|
|
|
75
106
|
return undefined;
|
|
76
107
|
return catalog?.[provider]?.models?.[modelId];
|
|
77
108
|
}
|
|
78
|
-
function withTimeout(timeoutMs, signal) {
|
|
109
|
+
export function withTimeout(timeoutMs, signal) {
|
|
79
110
|
const controller = new AbortController();
|
|
80
111
|
const onAbort = () => controller.abort(signal?.reason);
|
|
81
112
|
if (signal) {
|
|
@@ -170,17 +201,13 @@ function mapFromModelInfo(entry) {
|
|
|
170
201
|
const info = entry.model_info ?? {};
|
|
171
202
|
if (info.mode && info.mode !== "chat")
|
|
172
203
|
return undefined;
|
|
204
|
+
const catalogModel = findCatalogModel(id);
|
|
173
205
|
return {
|
|
174
206
|
id,
|
|
175
207
|
name: id,
|
|
176
208
|
reasoning: info.supports_reasoning ?? false,
|
|
177
209
|
input: info.supports_vision ? ["text", "image"] : ["text"],
|
|
178
|
-
cost:
|
|
179
|
-
input: (info.input_cost_per_token ?? 0) * 1_000_000,
|
|
180
|
-
output: (info.output_cost_per_token ?? 0) * 1_000_000,
|
|
181
|
-
cacheRead: (info.cache_read_input_token_cost ?? 0) * 1_000_000,
|
|
182
|
-
cacheWrite: (info.cache_creation_input_token_cost ?? 0) * 1_000_000,
|
|
183
|
-
},
|
|
210
|
+
cost: mapModelInfoCost(info, catalogModel?.cost),
|
|
184
211
|
contextWindow: info.max_input_tokens ?? DEFAULT_CONTEXT_WINDOW,
|
|
185
212
|
maxTokens: info.max_output_tokens ?? DEFAULT_MAX_TOKENS,
|
|
186
213
|
compat: buildCompat(id),
|
|
@@ -192,19 +219,16 @@ function mapFromHealthModelInfo(entry, fallbackId) {
|
|
|
192
219
|
return model;
|
|
193
220
|
if (entry.model_info?.mode && entry.model_info.mode !== "chat")
|
|
194
221
|
return undefined;
|
|
222
|
+
const info = entry.model_info ?? {};
|
|
223
|
+
const catalogModel = findCatalogModel(fallbackId);
|
|
195
224
|
return {
|
|
196
225
|
id: fallbackId,
|
|
197
226
|
name: fallbackId,
|
|
198
|
-
reasoning:
|
|
199
|
-
input:
|
|
200
|
-
cost:
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
cacheRead: (entry.model_info?.cache_read_input_token_cost ?? 0) * 1_000_000,
|
|
204
|
-
cacheWrite: (entry.model_info?.cache_creation_input_token_cost ?? 0) * 1_000_000,
|
|
205
|
-
},
|
|
206
|
-
contextWindow: entry.model_info?.max_input_tokens ?? DEFAULT_CONTEXT_WINDOW,
|
|
207
|
-
maxTokens: entry.model_info?.max_output_tokens ?? DEFAULT_MAX_TOKENS,
|
|
227
|
+
reasoning: info.supports_reasoning ?? false,
|
|
228
|
+
input: info.supports_vision ? ["text", "image"] : ["text"],
|
|
229
|
+
cost: mapModelInfoCost(info, catalogModel?.cost),
|
|
230
|
+
contextWindow: info.max_input_tokens ?? DEFAULT_CONTEXT_WINDOW,
|
|
231
|
+
maxTokens: info.max_output_tokens ?? DEFAULT_MAX_TOKENS,
|
|
208
232
|
compat: buildCompat(fallbackId),
|
|
209
233
|
};
|
|
210
234
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
1
|
+
import { execSync, spawn } from "node:child_process";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { AuthStorage, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { fingerprint, readCache, writeCache } from "./cache.js";
|
|
6
6
|
import { setupLiteLLMCostTracking } from "./cost.js";
|
|
7
|
-
import { discoverModels, normalizeBaseUrl, shouldSuppressReasoningContent } from "./discover.js";
|
|
7
|
+
import { discoverModels, normalizeBaseUrl, shouldSuppressReasoningContent, withTimeout } from "./discover.js";
|
|
8
8
|
import { getGcloudToken, getGcloudTokenCacheKey, getGcloudTokenCommand, isGcloudTokenAuthEnabled, } from "./gcloud-token.js";
|
|
9
9
|
import { getSessionIdFromFile } from "./litellm.js";
|
|
10
10
|
import { createMcpToolDefinitions } from "./mcp-tools.js";
|
|
@@ -72,6 +72,45 @@ function tokenExpiresAt(apiKey, opaqueFallback = PERMANENT_TOKEN_EXPIRES_AT) {
|
|
|
72
72
|
return opaqueFallback;
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
+
function openInBrowser(url) {
|
|
76
|
+
// Never invoke a shell here: cmd.exe re-parses metacharacters before `start`
|
|
77
|
+
// runs, which would make URL contents injectable. Launch is best-effort; the
|
|
78
|
+
// URL is also shown to the user, so launcher failures must not crash the process.
|
|
79
|
+
const [cmd, args] = process.platform === "darwin"
|
|
80
|
+
? ["open", [url]]
|
|
81
|
+
: process.platform === "win32"
|
|
82
|
+
? ["rundll32", ["url.dll,FileProtocolHandler", url]]
|
|
83
|
+
: ["xdg-open", [url]];
|
|
84
|
+
spawn(cmd, args, { stdio: "ignore", detached: true })
|
|
85
|
+
.on("error", () => undefined)
|
|
86
|
+
.unref();
|
|
87
|
+
}
|
|
88
|
+
async function generateVirtualKey(baseUrl, userToken, signal) {
|
|
89
|
+
const { signal: boundedSignal, cancel } = withTimeout(LOGIN_TIMEOUT_MS, signal);
|
|
90
|
+
try {
|
|
91
|
+
const response = await fetch(`${baseUrl}/key/generate`, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: {
|
|
94
|
+
Authorization: `Bearer ${userToken}`,
|
|
95
|
+
"Content-Type": "application/json",
|
|
96
|
+
},
|
|
97
|
+
body: JSON.stringify({}),
|
|
98
|
+
signal: boundedSignal,
|
|
99
|
+
});
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
const text = await response.text().catch(() => "");
|
|
102
|
+
throw new Error(`Virtual key generation failed (${response.status}): ${text}`);
|
|
103
|
+
}
|
|
104
|
+
const data = (await response.json());
|
|
105
|
+
if (typeof data.key !== "string" || !data.key)
|
|
106
|
+
throw new Error("No key in response from /key/generate");
|
|
107
|
+
const expiresMs = typeof data.expires === "string" ? Date.parse(data.expires) : Number.NaN;
|
|
108
|
+
return { key: data.key, expiresAt: Number.isNaN(expiresMs) ? undefined : expiresMs };
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
cancel();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
75
114
|
function getLiteLLMApiKey(credentials) {
|
|
76
115
|
return credentials.access;
|
|
77
116
|
}
|
|
@@ -164,12 +203,63 @@ async function loginLiteLLM(callbacks, onCacheWrite) {
|
|
|
164
203
|
message: "Enter LiteLLM proxy URL (no trailing /v1):",
|
|
165
204
|
placeholder: "https://litellm.example.com",
|
|
166
205
|
})).trim();
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
throw new Error("Both base URL and API key are required");
|
|
206
|
+
if (!rawBaseUrl)
|
|
207
|
+
throw new Error("Base URL is required");
|
|
170
208
|
const baseUrl = normalizeBaseUrl(rawBaseUrl);
|
|
171
|
-
const
|
|
172
|
-
|
|
209
|
+
const method = (await callbacks.onPrompt({
|
|
210
|
+
message: "Select login method (1 = API key / !command, 2 = SSO / Enterprise JWT):",
|
|
211
|
+
})).trim();
|
|
212
|
+
let apiKey;
|
|
213
|
+
let refresh;
|
|
214
|
+
let expires;
|
|
215
|
+
if (method === "2") {
|
|
216
|
+
callbacks.onAuth?.({
|
|
217
|
+
url: `${baseUrl}/sso/key/generate`,
|
|
218
|
+
instructions: "Authenticate via SSO, then copy your token from the LiteLLM UI.",
|
|
219
|
+
});
|
|
220
|
+
const rawToken = (await callbacks.onPrompt({ message: "Paste your SSO token from the LiteLLM UI:" }))
|
|
221
|
+
.trim()
|
|
222
|
+
.replace(/^Bearer\s+/i, "")
|
|
223
|
+
.trim();
|
|
224
|
+
if (!rawToken)
|
|
225
|
+
throw new Error("SSO token is required");
|
|
226
|
+
const wantVirtualKey = (await callbacks.onPrompt({ message: "Generate a LiteLLM virtual key from this token? (y/n):" }))
|
|
227
|
+
.trim()
|
|
228
|
+
.toLowerCase();
|
|
229
|
+
if (wantVirtualKey !== "n" && wantVirtualKey !== "no") {
|
|
230
|
+
try {
|
|
231
|
+
callbacks.onProgress?.("Generating virtual key...");
|
|
232
|
+
const generated = await generateVirtualKey(baseUrl, rawToken, callbacks.signal);
|
|
233
|
+
apiKey = generated.key;
|
|
234
|
+
refresh = "";
|
|
235
|
+
expires =
|
|
236
|
+
generated.expiresAt === undefined
|
|
237
|
+
? PERMANENT_TOKEN_EXPIRES_AT
|
|
238
|
+
: Math.max(Date.now(), generated.expiresAt - TOKEN_REFRESH_LEAD_MS);
|
|
239
|
+
callbacks.onProgress?.("Virtual key generated and will be used for API calls.");
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
243
|
+
callbacks.onProgress?.(`LiteLLM: virtual key generation failed (${message}); using SSO token directly.`);
|
|
244
|
+
apiKey = rawToken;
|
|
245
|
+
refresh = "";
|
|
246
|
+
expires = tokenExpiresAt(rawToken, PERMANENT_TOKEN_EXPIRES_AT);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
apiKey = rawToken;
|
|
251
|
+
refresh = "";
|
|
252
|
+
expires = tokenExpiresAt(rawToken, PERMANENT_TOKEN_EXPIRES_AT);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
const apiKeyInput = (await callbacks.onPrompt({ message: "Enter API key:" })).trim();
|
|
257
|
+
if (!apiKeyInput)
|
|
258
|
+
throw new Error("Both base URL and API key are required");
|
|
259
|
+
refresh = apiKeyInput.startsWith("!") ? apiKeyInput : "";
|
|
260
|
+
apiKey = refresh ? executeApiKeyCommand(refresh) : apiKeyInput;
|
|
261
|
+
expires = tokenExpiresAt(apiKey, refresh ? EXPIRE_TOKEN_IMMEDIATELY : PERMANENT_TOKEN_EXPIRES_AT);
|
|
262
|
+
}
|
|
173
263
|
const { models, source } = await discoverModels(baseUrl, apiKey, {
|
|
174
264
|
timeoutMs: LOGIN_TIMEOUT_MS,
|
|
175
265
|
signal: callbacks.signal,
|
|
@@ -187,13 +277,17 @@ async function loginLiteLLM(callbacks, onCacheWrite) {
|
|
|
187
277
|
return {
|
|
188
278
|
access: apiKey,
|
|
189
279
|
refresh,
|
|
190
|
-
expires
|
|
280
|
+
expires,
|
|
191
281
|
baseUrl,
|
|
192
282
|
};
|
|
193
283
|
}
|
|
194
284
|
async function refreshLiteLLM(credentials) {
|
|
195
|
-
if (!credentials.refresh.startsWith("!"))
|
|
285
|
+
if (!credentials.refresh.startsWith("!")) {
|
|
286
|
+
if (credentials.expires < PERMANENT_TOKEN_EXPIRES_AT) {
|
|
287
|
+
throw new Error("LiteLLM credential cannot be refreshed; run /login litellm again");
|
|
288
|
+
}
|
|
196
289
|
return credentials;
|
|
290
|
+
}
|
|
197
291
|
const access = executeApiKeyCommand(credentials.refresh);
|
|
198
292
|
return { ...credentials, access, expires: tokenExpiresAt(access, EXPIRE_TOKEN_IMMEDIATELY) };
|
|
199
293
|
}
|
|
@@ -453,7 +547,10 @@ export default async function (pi) {
|
|
|
453
547
|
}
|
|
454
548
|
async function runLogin(ctx) {
|
|
455
549
|
const credential = await loginLiteLLM({
|
|
456
|
-
onAuth: () =>
|
|
550
|
+
onAuth: ({ url, instructions }) => {
|
|
551
|
+
openInBrowser(url);
|
|
552
|
+
ctx.ui.notify(instructions ? `${url} — ${instructions}` : url, "info");
|
|
553
|
+
},
|
|
457
554
|
onDeviceCode: () => undefined,
|
|
458
555
|
onPrompt: async ({ message, placeholder }) => {
|
|
459
556
|
const value = await ctx.ui.input(message, placeholder);
|
package/dist/mcp-tools.js
CHANGED
|
@@ -92,37 +92,6 @@ function sanitizeName(name) {
|
|
|
92
92
|
.replace(/^_+|_+$/g, "");
|
|
93
93
|
return sanitized || "tool";
|
|
94
94
|
}
|
|
95
|
-
function isMappableSchema(value) {
|
|
96
|
-
if (!value || typeof value !== "object")
|
|
97
|
-
return false;
|
|
98
|
-
const schema = value;
|
|
99
|
-
if ("$ref" in schema || "anyOf" in schema || "oneOf" in schema || "allOf" in schema)
|
|
100
|
-
return false;
|
|
101
|
-
if (["string", "number", "integer", "boolean"].includes(String(schema.type)))
|
|
102
|
-
return true;
|
|
103
|
-
if (schema.type === "array") {
|
|
104
|
-
const items = schema.items;
|
|
105
|
-
return items?.type === "string";
|
|
106
|
-
}
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
function mapPropertySchema(name, schema) {
|
|
110
|
-
const description = typeof schema.description === "string" ? schema.description : name;
|
|
111
|
-
switch (schema.type) {
|
|
112
|
-
case "string":
|
|
113
|
-
return Type.String({ description });
|
|
114
|
-
case "number":
|
|
115
|
-
return Type.Number({ description });
|
|
116
|
-
case "integer":
|
|
117
|
-
return Type.Integer({ description });
|
|
118
|
-
case "boolean":
|
|
119
|
-
return Type.Boolean({ description });
|
|
120
|
-
case "array":
|
|
121
|
-
return Type.Array(Type.String(), { description });
|
|
122
|
-
default:
|
|
123
|
-
return Type.Unknown({ description });
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
95
|
function buildParameters(inputSchema) {
|
|
127
96
|
const properties = inputSchema.properties;
|
|
128
97
|
if (!properties || typeof properties !== "object") {
|
|
@@ -130,20 +99,7 @@ function buildParameters(inputSchema) {
|
|
|
130
99
|
args: Type.Record(Type.String(), Type.Unknown(), { description: "Tool arguments as key-value pairs" }),
|
|
131
100
|
});
|
|
132
101
|
}
|
|
133
|
-
|
|
134
|
-
? inputSchema.required.filter((key) => typeof key === "string")
|
|
135
|
-
: [];
|
|
136
|
-
const mapped = {};
|
|
137
|
-
for (const [key, property] of Object.entries(properties)) {
|
|
138
|
-
if (!isMappableSchema(property)) {
|
|
139
|
-
return Type.Object({
|
|
140
|
-
args: Type.Record(Type.String(), Type.Unknown(), { description: "Tool arguments as key-value pairs" }),
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
const field = mapPropertySchema(key, property);
|
|
144
|
-
mapped[key] = required.includes(key) ? field : Type.Optional(field);
|
|
145
|
-
}
|
|
146
|
-
return Type.Object(mapped);
|
|
102
|
+
return Type.Unsafe(inputSchema);
|
|
147
103
|
}
|
|
148
104
|
export async function createMcpToolDefinitions(baseUrl, getApiKey) {
|
|
149
105
|
const discoveryApiKey = await getApiKey();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-provider-litellm",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.8",
|
|
4
4
|
"description": "LiteLLM proxy provider extension for Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"image": "https://raw.githubusercontent.com/balcsida/pi-provider-litellm/refs/heads/main/assets/pi_litellm_gallery.png"
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
|
-
"clean": "
|
|
43
|
+
"clean": "rm -rf dist",
|
|
44
44
|
"build": "tsgo -p tsconfig.build.json",
|
|
45
45
|
"dev": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput",
|
|
46
46
|
"typecheck": "tsgo --noEmit",
|
|
@@ -68,10 +68,8 @@
|
|
|
68
68
|
"@earendil-works/pi-coding-agent": "^0.79.1",
|
|
69
69
|
"@biomejs/biome": "^2.4.15",
|
|
70
70
|
"@types/node": "^25.9.1",
|
|
71
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
72
|
-
"shx": "^0.4.0",
|
|
71
|
+
"@typescript/native-preview": "7.0.0-dev.20260617.2",
|
|
73
72
|
"tsx": "^4.21.0",
|
|
74
|
-
"typescript": "^6.0.3",
|
|
75
73
|
"vitest": "^4.1.7"
|
|
76
74
|
}
|
|
77
75
|
}
|