pi-provider-litellm 1.2.0 → 1.2.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 +11 -13
- package/dist/cache.js +3 -2
- package/dist/discover.d.ts +2 -0
- package/dist/discover.js +17 -0
- package/dist/index.js +93 -7
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-provider-litellm
|
|
2
2
|
|
|
3
|
-
LiteLLM proxy provider extension for [
|
|
3
|
+
LiteLLM proxy provider extension for [Pi](https://pi.dev).
|
|
4
4
|
|
|
5
5
|
Discovers models from a self-hosted LiteLLM proxy and registers them under the `litellm` provider. Supports `/login litellm` and `/litellm-refresh`. Tries `/model/info` first (admin endpoint with rich metadata), falls back to `/v1/models` (OpenAI-compatible) on 401/403/404.
|
|
6
6
|
|
|
@@ -32,14 +32,7 @@ npm run build
|
|
|
32
32
|
|
|
33
33
|
## Configure
|
|
34
34
|
|
|
35
|
-
### Option A —
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
export LITELLM_BASE_URL="https://litellm.your-domain.com"
|
|
39
|
-
export LITELLM_API_KEY="sk-..."
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
### Option B — interactive login
|
|
35
|
+
### Option A — interactive login
|
|
43
36
|
|
|
44
37
|
Inside pi:
|
|
45
38
|
|
|
@@ -49,16 +42,21 @@ Inside pi:
|
|
|
49
42
|
|
|
50
43
|
You'll be prompted for the base URL and API key. Credentials are persisted to `~/.pi/agent/auth.json`.
|
|
51
44
|
|
|
45
|
+
### Option B — environment variables
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
export LITELLM_BASE_URL="https://litellm.your-domain.com"
|
|
49
|
+
export LITELLM_API_KEY="sk-..."
|
|
50
|
+
```
|
|
51
|
+
|
|
52
52
|
Stored pi credentials for `litellm` take precedence over `LITELLM_API_KEY`; the environment key is used when no saved credential exists. `LITELLM_BASE_URL` is used when no saved login base URL exists.
|
|
53
53
|
|
|
54
54
|
## Use
|
|
55
55
|
|
|
56
56
|
```
|
|
57
|
-
/model
|
|
57
|
+
/model
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
(Whatever model IDs your proxy exposes — see the discovered list at startup.)
|
|
61
|
-
|
|
62
60
|
## Optional environment variables
|
|
63
61
|
|
|
64
62
|
| Variable | Default | Effect |
|
|
@@ -74,7 +72,7 @@ Stored pi credentials for `litellm` take precedence over `LITELLM_API_KEY`; the
|
|
|
74
72
|
|
|
75
73
|
## Cache
|
|
76
74
|
|
|
77
|
-
The model list is cached at `~/.pi/agent/litellm-models.json` with a
|
|
75
|
+
The model list is cached at `~/.pi/agent/litellm-models.json` with a keyed fingerprint of the base URL + API key. Changing either invalidates the cache automatically.
|
|
78
76
|
|
|
79
77
|
## Troubleshooting
|
|
80
78
|
|
package/dist/cache.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { scryptSync } from "node:crypto";
|
|
2
2
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
|
+
const FINGERPRINT_SALT = "pi-provider-litellm-cache-fingerprint-v1";
|
|
4
5
|
export function fingerprint(apiKey) {
|
|
5
|
-
return
|
|
6
|
+
return scryptSync(apiKey, FINGERPRINT_SALT, 32).toString("hex");
|
|
6
7
|
}
|
|
7
8
|
export function isCacheValid(cache, baseUrl, apiKey) {
|
|
8
9
|
if (!cache)
|
package/dist/discover.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { DiscoveryOptions, DiscoveryResult } from "./types.js";
|
|
3
3
|
export declare function normalizeBaseUrl(input: string): string;
|
|
4
|
+
export declare function isMoonshotModel(modelId: string): boolean;
|
|
5
|
+
export declare function shouldSuppressReasoningContent(modelId: string): boolean;
|
|
4
6
|
export declare function buildCompat(modelId: string): ProviderModelConfig["compat"];
|
|
5
7
|
export declare function discoverModels(baseUrl: string, apiKey: string, options?: DiscoveryOptions): Promise<DiscoveryResult>;
|
package/dist/discover.js
CHANGED
|
@@ -10,7 +10,24 @@ export function normalizeBaseUrl(input) {
|
|
|
10
10
|
// `cacheControlFormat: "anthropic"` flag, pi never relays cache_control markers
|
|
11
11
|
// through the proxy, so prompt caching silently no-ops on Claude models.
|
|
12
12
|
const ANTHROPIC_MODEL_PATTERN = /^(anthropic\/|claude|opus|sonnet|haiku)/i;
|
|
13
|
+
const MOONSHOT_MODEL_PATTERN = /^(moonshotai\/|moonshot\/|kimi[-/])/i;
|
|
14
|
+
const FORCED_THINKING_MODEL_PATTERN = /(?:^|[-/])thinking(?:[-/]|$)/i;
|
|
15
|
+
export function isMoonshotModel(modelId) {
|
|
16
|
+
return MOONSHOT_MODEL_PATTERN.test(modelId);
|
|
17
|
+
}
|
|
18
|
+
export function shouldSuppressReasoningContent(modelId) {
|
|
19
|
+
return isMoonshotModel(modelId) && !FORCED_THINKING_MODEL_PATTERN.test(modelId);
|
|
20
|
+
}
|
|
13
21
|
export function buildCompat(modelId) {
|
|
22
|
+
if (isMoonshotModel(modelId)) {
|
|
23
|
+
return {
|
|
24
|
+
supportsStore: false,
|
|
25
|
+
supportsDeveloperRole: false,
|
|
26
|
+
supportsReasoningEffort: false,
|
|
27
|
+
supportsStrictMode: false,
|
|
28
|
+
maxTokensField: "max_tokens",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
14
31
|
if (ANTHROPIC_MODEL_PATTERN.test(modelId)) {
|
|
15
32
|
return { supportsStore: false, cacheControlFormat: "anthropic" };
|
|
16
33
|
}
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { AuthStorage, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { fingerprint, readCache, writeCache } from "./cache.js";
|
|
5
5
|
import { setupLiteLLMCostTracking } from "./cost.js";
|
|
6
|
-
import { discoverModels, normalizeBaseUrl } from "./discover.js";
|
|
6
|
+
import { discoverModels, normalizeBaseUrl, shouldSuppressReasoningContent } from "./discover.js";
|
|
7
7
|
import { getSessionIdFromFile } from "./litellm.js";
|
|
8
8
|
const PROVIDER_NAME = "litellm";
|
|
9
9
|
const ENV_BASE_URL = "LITELLM_BASE_URL";
|
|
@@ -109,6 +109,87 @@ function modifyLiteLLMModels(models, cred) {
|
|
|
109
109
|
return models;
|
|
110
110
|
return models.map((m) => (m.provider === PROVIDER_NAME ? { ...m, baseUrl: `${baseUrl}/v1` } : m));
|
|
111
111
|
}
|
|
112
|
+
function prepareLiteLLMRequestPayload(payload, modelId, sessionId) {
|
|
113
|
+
let next;
|
|
114
|
+
const update = (key, value) => {
|
|
115
|
+
if (payload[key] !== undefined)
|
|
116
|
+
return;
|
|
117
|
+
next ??= { ...payload };
|
|
118
|
+
next[key] = value;
|
|
119
|
+
};
|
|
120
|
+
if (modelId && shouldSuppressReasoningContent(modelId)) {
|
|
121
|
+
update("include_reasoning", false);
|
|
122
|
+
update("reasoning_content", false);
|
|
123
|
+
update("merge_reasoning_content_in_choices", true);
|
|
124
|
+
update("thinking", { type: "disabled" });
|
|
125
|
+
}
|
|
126
|
+
if (sessionId) {
|
|
127
|
+
next ??= { ...payload };
|
|
128
|
+
next.litellm_session_id = sessionId;
|
|
129
|
+
}
|
|
130
|
+
return next;
|
|
131
|
+
}
|
|
132
|
+
function normalizeThinkTags(message) {
|
|
133
|
+
if (message.provider !== PROVIDER_NAME || !shouldSuppressReasoningContent(message.model))
|
|
134
|
+
return;
|
|
135
|
+
let changed = false;
|
|
136
|
+
const content = [];
|
|
137
|
+
const appendText = (text) => {
|
|
138
|
+
if (!text)
|
|
139
|
+
return;
|
|
140
|
+
const last = content.at(-1);
|
|
141
|
+
if (last?.type === "text") {
|
|
142
|
+
last.text += text;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
content.push({ type: "text", text });
|
|
146
|
+
};
|
|
147
|
+
const appendThinking = (thinking) => {
|
|
148
|
+
if (!thinking)
|
|
149
|
+
return;
|
|
150
|
+
const last = content.at(-1);
|
|
151
|
+
if (last?.type === "thinking") {
|
|
152
|
+
last.thinking += thinking;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
content.push({ type: "thinking", thinking });
|
|
156
|
+
};
|
|
157
|
+
for (let blockIndex = 0; blockIndex < message.content.length; blockIndex++) {
|
|
158
|
+
const block = message.content[blockIndex];
|
|
159
|
+
if (block.type !== "text") {
|
|
160
|
+
content.push(block);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
let index = 0;
|
|
164
|
+
while (index < block.text.length) {
|
|
165
|
+
const start = block.text.indexOf("<think>", index);
|
|
166
|
+
if (start === -1) {
|
|
167
|
+
appendText(block.text.slice(index));
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
changed = true;
|
|
171
|
+
appendText(block.text.slice(index, start));
|
|
172
|
+
const thinkingStart = start + "<think>".length;
|
|
173
|
+
const end = block.text.indexOf("</think>", thinkingStart);
|
|
174
|
+
if (end === -1) {
|
|
175
|
+
const isBeforeNonTextContent = message.content
|
|
176
|
+
.slice(blockIndex + 1)
|
|
177
|
+
.some((nextBlock) => nextBlock.type !== "text");
|
|
178
|
+
if (isBeforeNonTextContent)
|
|
179
|
+
appendThinking(block.text.slice(thinkingStart));
|
|
180
|
+
else
|
|
181
|
+
appendText(block.text.slice(thinkingStart));
|
|
182
|
+
index = block.text.length;
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
appendThinking(block.text.slice(thinkingStart, end));
|
|
186
|
+
index = end + "</think>".length;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (!changed)
|
|
190
|
+
return;
|
|
191
|
+
return { ...message, content };
|
|
192
|
+
}
|
|
112
193
|
export default async function (pi) {
|
|
113
194
|
const creds = await resolveCredentials();
|
|
114
195
|
const cache = await readCache(getCachePath());
|
|
@@ -214,14 +295,19 @@ export default async function (pi) {
|
|
|
214
295
|
pi.on("session_start", async (_event, ctx) => {
|
|
215
296
|
sessionId = getSessionIdFromFile(ctx.sessionManager.getSessionFile());
|
|
216
297
|
});
|
|
217
|
-
pi.on("before_provider_request", (event) => {
|
|
218
|
-
if (
|
|
298
|
+
pi.on("before_provider_request", (event, ctx) => {
|
|
299
|
+
if (ctx.model?.provider !== PROVIDER_NAME)
|
|
219
300
|
return;
|
|
220
301
|
if (typeof event.payload !== "object" || event.payload === null)
|
|
221
302
|
return;
|
|
222
|
-
return
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
303
|
+
return prepareLiteLLMRequestPayload(event.payload, ctx.model?.id, sessionId);
|
|
304
|
+
});
|
|
305
|
+
pi.on("message_end", (event) => {
|
|
306
|
+
if (event.message.role !== "assistant")
|
|
307
|
+
return;
|
|
308
|
+
const message = normalizeThinkTags(event.message);
|
|
309
|
+
if (!message)
|
|
310
|
+
return;
|
|
311
|
+
return { message };
|
|
226
312
|
});
|
|
227
313
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-provider-litellm",
|
|
3
|
-
"version": "1.2.
|
|
4
|
-
"description": "LiteLLM proxy provider extension for
|
|
3
|
+
"version": "1.2.2",
|
|
4
|
+
"description": "LiteLLM proxy provider extension for Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Dávid Balatoni <balcsida@gmail.com>",
|
|
@@ -57,6 +57,11 @@
|
|
|
57
57
|
"@earendil-works/pi-ai": "^0.74.0",
|
|
58
58
|
"@earendil-works/pi-coding-agent": "^0.74.0"
|
|
59
59
|
},
|
|
60
|
+
"overrides": {
|
|
61
|
+
"basic-ftp": "6.0.1",
|
|
62
|
+
"fast-xml-builder": "1.2.0",
|
|
63
|
+
"protobufjs": "8.2.1"
|
|
64
|
+
},
|
|
60
65
|
"devDependencies": {
|
|
61
66
|
"@earendil-works/pi-ai": "^0.74.0",
|
|
62
67
|
"@earendil-works/pi-coding-agent": "^0.74.0",
|