pi-provider-litellm 3.3.0 → 4.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/README.md +18 -60
- package/package.json +3 -3
- package/src/backend-identity.ts +1 -1
- package/src/discover.ts +67 -8
- package/src/index.ts +128 -399
- package/src/model-groups.ts +4 -1
- package/src/provider.ts +1 -1
- package/src/skills.ts +1 -2
- package/src/types.ts +9 -24
- package/src/mcp-tools.ts +0 -1536
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-provider-litellm
|
|
2
2
|
|
|
3
|
-
LiteLLM proxy native Provider extension for [Pi](https://pi.dev). Pi 0.
|
|
3
|
+
LiteLLM proxy native Provider extension for [Pi](https://pi.dev). Pi 0.99.2+ is required; on older Pi, install `npm:pi-provider-litellm@3.4.0`.
|
|
4
4
|
|
|
5
5
|
Discovers models from self-hosted LiteLLM proxies and registers them under Pi providers. The default provider is `litellm`; optional aliases can register additional LiteLLM providers with separate credentials. Supports `/login litellm`, LiteLLM MCP tools, LiteLLM Skills Gateway prompt injection, and Google ADC token auth. Tries `/model/info` first (admin endpoint with rich metadata), falls back to `/v1/models` (OpenAI-compatible) on 401/403/404, then tries `/health` plus per-endpoint `/model/info` for older LiteLLM proxies.
|
|
6
6
|
|
|
@@ -177,17 +177,17 @@ LiteLLM Skills and MCP integration are enabled by default. Disable either featur
|
|
|
177
177
|
"enabled": true,
|
|
178
178
|
"exposure": "deferred",
|
|
179
179
|
"toolExposure": {
|
|
180
|
-
"
|
|
181
|
-
"github
|
|
180
|
+
"*delete*": "hidden",
|
|
181
|
+
"github-search_code": "direct"
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
```
|
|
187
187
|
|
|
188
|
-
Setting `skills.enabled` to `false` disables the Skills Gateway management tools, skill fetching, and system-prompt injection. Setting `mcp.enabled` to `false`
|
|
188
|
+
Setting `skills.enabled` to `false` disables the Skills Gateway management tools, skill fetching, and system-prompt injection. Setting `mcp.enabled` to `false` stops the extension from registering the proxy's MCP server. Restart Pi after changing these settings.
|
|
189
189
|
|
|
190
|
-
`mcp.exposure` and `mcp.toolExposure`
|
|
190
|
+
`mcp.exposure` and `mcp.toolExposure` are passed to Pi unchanged and mean what they mean in a Pi `mcp.json` server entry: `codemode` (Pi's default when `exposure` is unset; callable from Pi's `codemode` scripts), `deferred` (declared to the model once Pi's `tool_search` loads it), `direct` (declared on every request), or `hidden`. `toolExposure` keys are tool names as LiteLLM's `/mcp` endpoint offers them, which LiteLLM prefixes with the server name, for example `github-search_code`; `*` matches any characters. Pi validates both settings, and an unusable value is reported once.
|
|
191
191
|
|
|
192
192
|
Treat the configured LiteLLM proxy as trusted: Skills can add instructions to the system prompt, and MCP can expose tools the agent may call. Disable these integrations when the proxy, its administrators, or its configured content are not fully trusted.
|
|
193
193
|
|
|
@@ -197,7 +197,7 @@ Treat the configured LiteLLM proxy as trusted: Skills can add instructions to th
|
|
|
197
197
|
/model
|
|
198
198
|
```
|
|
199
199
|
|
|
200
|
-
To refresh catalogs on demand, run `/litellm-refresh` for every configured LiteLLM provider, or `/litellm-refresh <provider>` for one alias. It works with `PI_OFFLINE` set, which stops Pi's own `/model` refresh from contacting the network. It contacts only the configured proxies: `LITELLM_OFFLINE=1` and `LITELLM_DISCOVERY_TIMEOUT_MS=0` still disable it, and models.dev enrichment stays off under `PI_OFFLINE`. Each provider reports its model count, the failure, or missing credentials.
|
|
200
|
+
To refresh catalogs on demand, run `/litellm-refresh` for every configured LiteLLM provider, or `/litellm-refresh <provider>` for one alias. It works with `PI_OFFLINE` set, which stops Pi's own `/model` refresh from contacting the network. It contacts only the configured proxies: `LITELLM_OFFLINE=1` and `LITELLM_DISCOVERY_TIMEOUT_MS=0` still disable it, and models.dev enrichment stays off under `PI_OFFLINE`. Each provider reports its model count, the failure, or missing credentials. MCP tools are not part of this refresh; run `/mcp reconnect <provider>` to reconnect a provider's MCP server.
|
|
201
201
|
|
|
202
202
|
Every request routed through a configured LiteLLM provider includes Pi's canonical session ID in the
|
|
203
203
|
`x-litellm-session-id` header. This groups Chat Completions, Responses, and native Messages requests in LiteLLM without
|
|
@@ -205,7 +205,7 @@ adding transport-specific fields to request bodies.
|
|
|
205
205
|
|
|
206
206
|
## Model transport
|
|
207
207
|
|
|
208
|
-
A `/model/info` route group uses native Anthropic `/v1/messages` only when every deployment explicitly reports Chat mode, identifies a Claude backend through an Anthropic-, Bedrock-, or Vertex-capable adapter, and every declared backend identity resolves the same Anthropic compatibility policy. A `claude-<name>-<major>-<minor>` backend id resolves through its `claude-<name>-<major>` entry when the exact id is absent; an unknown generation stays on Chat Completions. When `supported_endpoints` is
|
|
208
|
+
A `/model/info` route group uses native Anthropic `/v1/messages` only when every deployment explicitly reports Chat mode, identifies a Claude backend through an Anthropic-, Bedrock-, or Vertex-capable adapter, and every declared backend identity resolves the same Anthropic compatibility policy. A `claude-<name>-<major>-<minor>` backend id resolves through its `claude-<name>-<major>` entry when the exact id is absent; an unknown generation stays on Chat Completions. When `supported_endpoints` is a list, it must include `/v1/messages`; an explicit endpoint list without it keeps the group on Chat Completions. A `null` value, which LiteLLM v1.100 and later report for models absent from their model map, counts as absent. An unresolved routing or base-model identity denies native Messages rather than being discarded from the unanimity check. Mixed-generation Claude groups can use Messages when their serializer policies match, with reasoning levels restricted to their common supported set. Groups with incompatible policies, mixed families, or unknown backend evidence remain on Chat Completions; unanimous explicit Responses mode takes precedence. Routes discovered through `/health` never select native Messages: Messages candidates are downgraded to Chat Completions with reasoning controls closed, while correlated Responses detail remains on Responses. Evidence-free `/v1/models` and wholly health-only `/health` groups take transport and presentation metadata from the bounded Pi catalog lookup: `openai-responses` when the catalog says so, otherwise Chat Completions. They expose no speculative reasoning selector and never select native Messages. Catalog identity such as Amazon Bedrock remains separate from the selected wire protocol.
|
|
209
209
|
|
|
210
210
|
Native Messages authenticates with `x-api-key`; every transport carries the `x-litellm-session-id` header described above. Strict JSON-schema tools are enabled separately from Messages transport and thinking compatibility: every deployment must affirm support for the actual route/API. Generic `supports_response_schema` and `supports_native_structured_output` metadata does not grant strict tools. Only deployments that LiteLLM reports with the `anthropic` adapter carry that evidence today. Claude served through Bedrock or Vertex therefore uses ordinary tool serialization, and a tool that requires strict sampling fails before the request is sent.
|
|
211
211
|
|
|
@@ -221,7 +221,7 @@ Native Messages authenticates with `x-api-key`; every transport carries the `x-l
|
|
|
221
221
|
| `PI_OFFLINE` | unset | Pi documents it as disabling automatic network activity, including model catalog refreshes. Through Pi 1.0.0, any set value (including `0` or an empty string) disables startup model discovery and `/model` network refreshes. Run `/litellm-refresh`, or unset it, to discover; cached models can still be used offline. |
|
|
222
222
|
| `LITELLM_DISCOVERY_TIMEOUT_MS` | `5000` | Background and explicit discovery fetch timeout in ms; `0` disables automatic discovery |
|
|
223
223
|
| `LITELLM_CLI_JWT_EXPIRATION_HOURS` | `24` | CLI SSO token lifetime fallback for older proxies whose poll response omits `expires_in`; mirror a non-default proxy setting locally |
|
|
224
|
-
| `LITELLM_VERBOSE_DISCOVERY` | unset | If `1`, enable progress messages during model
|
|
224
|
+
| `LITELLM_VERBOSE_DISCOVERY` | unset | If `1`, enable progress messages during model discovery (login, refresh, startup), including startup skip reasons and defaulted `/model/info` context windows. Progress messages are off by default |
|
|
225
225
|
| `LITELLM_DEFAULT_CONTEXT_WINDOW` | `128000` | Context window assumed when neither LiteLLM's `/model/info` nor the catalog reports `max_input_tokens`. Set a positive integer for proxies whose custom aliases carry no metadata; an unset or unusable value keeps 128K |
|
|
226
226
|
| `LITELLM_MODELS_DEV` | unset | If `1`, enrich discovered metadata (limits, prices, effort lists) from models.dev, cached for 28 days in `litellm-models-dev.json`. Off by default because LiteLLM's `/model/info` is authoritative; use it when LiteLLM's model map lacks a model's metadata |
|
|
227
227
|
|
|
@@ -245,60 +245,17 @@ Only `authorized_user` ADC files are supported. Service account JSON files are r
|
|
|
245
245
|
|
|
246
246
|
## LiteLLM MCP tools
|
|
247
247
|
|
|
248
|
-
|
|
248
|
+
LiteLLM serves the MCP servers it gateways over streamable HTTP at `/mcp`. For each configured provider, the extension registers that endpoint as a Pi MCP server named after the provider (`litellm`, or the alias name), and Pi's built-in MCP client connects to it, lists its tools, and calls them. Tools are named `mcp__<provider>__<tool>` and appear in `/mcp`, where the server's connection state and errors are shown.
|
|
249
249
|
|
|
250
|
-
|
|
251
|
-
- `POST /mcp-rest/tools/call`
|
|
250
|
+
The registration does not copy the provider's API key or token. It uses Pi's `auth: { provider }` setting, so Pi sends the provider's current credential as `Authorization: Bearer` on every request, and token refreshes, Google ADC, and key helpers apply without registering again. Custom headers (`LITELLM_HEADERS`, or an alias's `headers`) are registered as configured, after `$` and a leading `!` are escaped so Pi does not expand them a second time. Pi sends provider credentials only over https or to a loopback host, so a proxy reached over plain http elsewhere (`allowInsecureHttp`) gets no MCP server, and the refusal is reported once.
|
|
252
251
|
|
|
253
|
-
|
|
252
|
+
The extension registers at startup and checks again before each agent turn. `/login litellm` disconnects the server as soon as it starts, before the new credential is stored, and the next turn connects again with the new credential and proxy root; a logout that leaves no root removes the server. `LITELLM_OFFLINE=1`, `LITELLM_DISCOVERY_TIMEOUT_MS=0`, and `PI_OFFLINE` register nothing. A server with the same name in your own `mcp.json` takes precedence over the extension's.
|
|
254
253
|
|
|
255
|
-
Pi
|
|
254
|
+
Pi reads the provider's credential when it sends each request, not when the server was registered. If another Pi process stores a login for a different proxy, this session's open connection can send that credential to the previous proxy until the next turn moves the server.
|
|
256
255
|
|
|
257
|
-
|
|
256
|
+
Earlier versions discovered tools through `/mcp-rest/tools/list` and registered them as `mcp_<server>_<tool>_<hash>`. Those names and the `<server>/<tool>` form of `toolExposure` keys no longer apply, and the `~/.pi/agent/litellm-mcp-pauses/` directory is no longer used.
|
|
258
257
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
### Which schema a tool ends up with
|
|
262
|
-
|
|
263
|
-
A tool registers with one of two parameter shapes.
|
|
264
|
-
|
|
265
|
-
- **The extension-owned `args` envelope** — a single object-valued `args` property, required at execution. Used when the proxy supplied no schema at all (absent or `{}`), and used as a safe substitute when the supplied schema could not be proven safe (below). Nothing in this shape originates from the proxy.
|
|
266
|
-
- **The supplied schema, passed through unchanged.** Pi validates arguments against it before the call and may first coerce supported primitive values: omitting a property listed in `required` is rejected, while a value of the wrong type is rejected only when it cannot be coerced to the declared type. Unknown extra properties are accepted by default, but an `additionalProperties` schema can validate and coerce them, and constraints such as `additionalProperties: false` or `unevaluatedProperties: false` can reject them. Passing through is not the same as skipping validation. A schema of exactly `{"type": "object"}` with no `properties` therefore accepts any arguments at the top level rather than nesting them under `args`. Real schema properties named `args`, `properties`, or `required` remain intact.
|
|
267
|
-
|
|
268
|
-
An `inputSchema` that is present but is not a JSON object (a string, number, or array) is a malformed tool and is dropped as `invalid-schema`. It is not treated as schemaless, so a proxy emitting junk cannot obtain the permissive envelope.
|
|
269
|
-
|
|
270
|
-
### Regexes and references in supplied schemas
|
|
271
|
-
|
|
272
|
-
`pattern`, and the keys of `patternProperties`, are compiled into a backtracking regular expression and executed with no time limit, so a proxy-supplied expression can block Pi for minutes on a single tool call. A `$ref` resolves an arbitrary JSON pointer, which means an expression can be parked under any key at all, including data-only positions such as `default` or `examples`. Enumerating "positions where a subschema may appear" is therefore not a sound defence.
|
|
273
|
-
|
|
274
|
-
Instead, the whole supplied document is walked — every key and every value — and the tool keeps its schema only if that walk completes and finds nothing dangerous. A walk stops short, and the tool is degraded, when it finds a `pattern` or `patternProperties` keyword at all — whatever its value type, since refusing to vouch for it does not depend on an upstream type guard staying as it is — a `$dynamicRef` or `$recursiveRef`, a `$ref` pointing outside the document, or a graph too deep or too large to inspect within budget. A local `#/...` pointer is accepted only when the walk covers its target *and* that target resolves to something usable as a subschema, with the reference chain proven acyclic — covering the document proves no expression hides behind a pointer, but not that the pointer names a schema at all. A pointer to a non-schema (`#/description`), a pointer with no target (`#/$defs/missing`), an `$anchor` reference (`#name`), and a mutual `$defs` cycle are each degraded rather than forwarded, because the validator would otherwise throw, exhaust the stack, or produce a tool that can never accept any argument. A key that is an argument *name* rather than a keyword — a property literally called `pattern` or `patternProperties` — is left alone.
|
|
275
|
-
|
|
276
|
-
**Degrading replaces the schema, not the tool.** Because regexes are common in real MCP schemas, a tool in this position still registers, with the `args` envelope in place of its schema, and is reported under the `schema-envelope` class. The compatibility cost is real and worth knowing: such a tool loses validation against the proxy-supplied schema but retains Pi's validation of the extension-owned envelope, so its arguments must be passed as an object under `args` and the model sees a less specific contract than the server documents. The server still validates authoritatively, so an argument that violates the proxy-specific contract becomes a server-side tool error rather than a local one.
|
|
277
|
-
|
|
278
|
-
### What you see when a tool is missing or degraded
|
|
279
|
-
|
|
280
|
-
Every raw entry the proxy returns is accounted for exactly once, and the counts reconcile: `raw = prepared + dropped + beyond-the-cap`, where *prepared* is what will be registered. If a registration pass is refused partway, the shortfall between prepared and registered is reported separately by that pass's own diagnostic. A tool is **dropped** for one of four reasons — `invalid-tool` (no usable name or server identity), `duplicate-identity`, `invalid-schema`, or `name-collision` — or discarded for being beyond the 512-tool limit (`tool-cap`). A tool is **degraded** rather than dropped under `schema-envelope`. Each class is reported with a count and a bounded sample of names through Pi notifications when a UI is active, or stderr in non-interactive mode. MCP messages emitted before the TUI context is ready are buffered until session start. For example:
|
|
281
|
-
|
|
282
|
-
```text
|
|
283
|
-
LiteLLM MCP: kept 2 MCP tools but replaced their schema with a safe args envelope, because of a schema that could not be proven free of proxy-supplied regexes or references: mcp_srv_lookup_1c97861c1f, mcp_srv_matcher_3ee4c31a5a.
|
|
284
|
-
LiteLLM MCP: dropped 1 MCP tool with a missing name or server identity: entry_3.
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
A class is re-reported whenever its membership changes, including a change beyond the names shown in the sample, and stays quiet while unchanged — so a stable proxy does not repeat itself across refreshes, while a changing one is always surfaced. A class that stops occurring is cleared, so its recurrence is reported rather than suppressed. These lines never contain the proxy's schema, its response body, its descriptions, or your API key; only generated names, positional placeholders, and counts. They are reported regardless of `LITELLM_VERBOSE_DISCOVERY`; set that variable to also see per-refresh raw, prepared, enveloped, and registered counts. The enveloped count covers every tool carrying the envelope, whether because the proxy supplied no schema or because its schema was degraded, so it is the number of tools with no proxy-specific argument contract.
|
|
288
|
-
|
|
289
|
-
### Registration lifecycle
|
|
290
|
-
|
|
291
|
-
Pi registers a tool synchronously, replacing any existing tool of the same name, and exposes no way to unregister. It has no notion of rejecting one tool while accepting another: the only failure is a staleness check that fires once the extension instance has been superseded, and that check is never reset. A refused registration therefore ends the whole pass rather than skipping one tool.
|
|
292
|
-
|
|
293
|
-
So a pass that is refused partway leaves the tools registered up to that point, reports one bounded diagnostic, and makes no further attempt from that extension instance — retrying would re-run discovery on every refresh and could never succeed. A reload creates a fresh instance, which starts clean on its own. Because names are stable and registration replaces by name, any retry that does happen is idempotent.
|
|
294
|
-
|
|
295
|
-
Network discovery is separate and remains retryable: a catalog that yields no registrable tool is not recorded as settled, so a later refresh tries again, and the empty result is reported rather than passing in silence. A tool that disappears from the proxy's catalog is, on the next discovery pass, re-registered as a hidden withdrawal stub and removed from the active tools on Pi 0.99. Older Pi ignores `hidden`, so the stub stays visible but reports the withdrawal instead of calling the proxy.
|
|
296
|
-
|
|
297
|
-
MCP tools run in Pi's parallel tool mode. Each side-effecting `POST /mcp-rest/tools/call` is attempted exactly once: timeouts, connection failures, HTTP errors, and malformed responses are returned to Pi as tool errors rather than retried. Pi cancellation aborts an in-flight call and preserves its original cancellation reason. Tool-call response bodies are limited to 5 MiB before JSON parsing, and returned result or error text to 64 KiB with a truncation marker. Results also carry `structuredContent` (the MCP `CallToolResult`, or a text wrapper when the proxy returns a bare value) limited to 1 MiB (larger results fall back to the text wrapper), under a declared `outputSchema`, so Pi 0.99 codemode scripts receive data rather than the truncated text; server `isError` results are returned as error results that keep their content (Pi before 0.99 raises them as tool errors instead).
|
|
298
|
-
|
|
299
|
-
A passed-through schema's `format` keyword is evaluated, using the validator's own built-in expressions rather than anything the proxy supplies.
|
|
300
|
-
|
|
301
|
-
Tools discovered this way are only as trustworthy as the MCP servers behind your proxy: their descriptions and results are text the model reads.
|
|
258
|
+
Tools are only as trustworthy as the MCP servers behind your proxy: their descriptions and results are text the model reads.
|
|
302
259
|
|
|
303
260
|
## LiteLLM Skill Hub
|
|
304
261
|
|
|
@@ -405,6 +362,8 @@ Reasoning levels require deployment evidence for the parameter Pi sends: `reason
|
|
|
405
362
|
|
|
406
363
|
LiteLLM omits `supported_openai_params` for a deployment its model map does not describe. Such a deployment keeps the `reasoning_effort` carrier when its `model_info` explicitly sets `supports_reasoning: true`, which is the operator's opt-in. A `supported_openai_params` list that omits `reasoning_effort` still denies effort levels unless `litellm_params.allowed_openai_params` adds it. Kimi and DeepSeek generations are excluded because their contracts name their own carriers. Evidence-free fallback models retain catalog presentation metadata but expose no speculative selector.
|
|
407
364
|
|
|
365
|
+
GPT-5.5 and later reject `reasoning_effort` together with function tools on `/v1/chat/completions`. On a Chat route where any deployment's `model_info.base_model` or `litellm_params.model` names such a backend, tool requests turn reasoning off instead of failing or being answered by a router fallback: `reasoning_effort` is set to the model's declared off value, or removed when it declares none. GPT-6 and later fall back to their default effort when the field is omitted, so they get `none` either way. A model with no backend evidence falls back to its route name. Requests without tools keep their reasoning level. A model that cannot turn reasoning off, such as GPT-6 Astra, cannot call tools on Chat Completions at all. To keep reasoning on tool turns, serve the route through `/v1/responses` (see [Protocols and prompt caching](#protocols-and-prompt-caching)).
|
|
366
|
+
|
|
408
367
|
Kimi K2.5/K2.6 use an on/off thinking switch; K2.7 Code/Highspeed is always thinking. Kimi K3 and DeepSeek V4 use public effort evidence with LiteLLM overrides. Responses levels are translated to valid `reasoning.effort` values; Chat-only compatibility fields never reach Responses models. Legacy cached level maps remain usable while discovery refreshes, and stored Moonshot compatibility can restore safe display normalization.
|
|
409
368
|
|
|
410
369
|
The development probe runs against minimized snapshots with `npm run probe:proxy -- --snapshot tests/fixtures/proxy/prod-model-info-2026-09-04.json`. A bounded live matrix is available with `--live --base-url <proxy-url> --max-requests 100`; its request bodies use the discovered carrier and request policy.
|
|
@@ -431,9 +390,8 @@ The development probe runs against minimized snapshots with `npm run probe:proxy
|
|
|
431
390
|
| Enterprise CLI SSO start/poll fails | Check the proxy logs and verify `/sso/cli/start` and `/sso/cli/poll/{login_id}` are reachable; only 404/405 falls back to legacy login |
|
|
432
391
|
| 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 |
|
|
433
392
|
| Enterprise SSO token prompt fails with "SSO token is required" | The token field was left empty — paste the token copied from the LiteLLM UI |
|
|
434
|
-
| MCP tools not showing | Verify the proxy
|
|
435
|
-
| MCP tools
|
|
436
|
-
| Some MCP tools missing or unvalidated | Check Pi notifications (stderr in non-interactive mode) for `LiteLLM MCP:` messages. Dropped tools are reported under `invalid-tool`, `duplicate-identity`, `invalid-schema`, `name-collision`, or `tool-cap`, each with a count and a bounded sample of generated names. A tool reported under `schema-envelope` is still present but uses the `args` envelope instead of its own schema. A refused registration pass is reported once. Set `LITELLM_VERBOSE_DISCOVERY=1` for per-refresh raw/prepared/enveloped/registered counts |
|
|
393
|
+
| MCP tools not showing | Run `/mcp` and check the provider's server: its state shows a connection or sign-in error. Verify the proxy serves `/mcp` and that the key has MCP access |
|
|
394
|
+
| MCP tools listed but the model cannot call them | `codemode` and `deferred` tools are reached through Pi's `codemode` and `tool_search` tools. Check that neither is disabled; `--no-extensions` disables them and Pi's MCP client alike. Setting `litellm.mcp.exposure` to `direct` declares the tools on every request instead |
|
|
437
395
|
| Skills not affecting prompts | Verify the proxy exposes `/claude-code/marketplace.json` or `/v1/skills` and returns enabled skills |
|
|
438
396
|
|
|
439
397
|
### `No models available` at startup
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-provider-litellm",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "LiteLLM proxy provider extension for Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
"node": ">=22.19.0"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
|
-
"@earendil-works/pi-ai": ">=0.
|
|
52
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
51
|
+
"@earendil-works/pi-ai": ">=0.99.2",
|
|
52
|
+
"@earendil-works/pi-coding-agent": ">=0.99.2"
|
|
53
53
|
},
|
|
54
54
|
"peerDependenciesMeta": {
|
|
55
55
|
"@earendil-works/pi-ai": {
|
package/src/backend-identity.ts
CHANGED
package/src/discover.ts
CHANGED
|
@@ -93,17 +93,31 @@ export function normalizeBaseUrl(input: string, allowInsecureHttp = false): stri
|
|
|
93
93
|
const ANTHROPIC_MODEL_PATTERN = /(?:^|[-_/.:])(?:anthropic\/|(?:claude|fable|opus|sonnet|haiku)(?=$|[-_/.:]))/i;
|
|
94
94
|
const MOONSHOT_MODEL_PATTERN = /^(moonshotai\/|moonshot\/|kimi[-/])/i;
|
|
95
95
|
const FORCED_THINKING_MODEL_PATTERN = /(?:^|[-/])thinking(?:[-/]|$)/i;
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
const
|
|
96
|
+
// GPT ids appear bare, prefixed, or dated (`gpt-6-sol`, `llm-gateway/gpt-5.5`,
|
|
97
|
+
// `gpt-5.5-20260504143601`); match them all so the tool+reasoning workaround
|
|
98
|
+
// survives renames. `gpt-4o` and `gpt-oss` carry no generation number here.
|
|
99
|
+
const GPT_GENERATION_PATTERN = /(?:^|\/)gpt-(\d+)(?:\.(\d+))?(?=$|[-.])/i;
|
|
100
100
|
|
|
101
101
|
export function isMoonshotModel(modelId: string): boolean {
|
|
102
102
|
return MOONSHOT_MODEL_PATTERN.test(modelId);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
function gptGeneration(modelId: string): [major: number, minor: number] | undefined {
|
|
106
|
+
const match = GPT_GENERATION_PATTERN.exec(modelId);
|
|
107
|
+
return match ? [Number(match[1]), Number(match[2] ?? 0)] : undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// GPT-5.5 and later reject `reasoning_effort` alongside function tools on
|
|
111
|
+
// /v1/chat/completions ("use /v1/responses").
|
|
112
|
+
export function isGpt55OrNewerModel(modelId: string): boolean {
|
|
113
|
+
const generation = gptGeneration(modelId);
|
|
114
|
+
return generation !== undefined && (generation[0] > 5 || (generation[0] === 5 && generation[1] >= 5));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// GPT-6 also rejects an omitted effort there: it falls back to its default effort.
|
|
118
|
+
export function isGpt6OrNewerModel(modelId: string): boolean {
|
|
119
|
+
const generation = gptGeneration(modelId);
|
|
120
|
+
return generation !== undefined && generation[0] >= 6;
|
|
107
121
|
}
|
|
108
122
|
|
|
109
123
|
function shouldSuppressReasoningContent(modelId: string): boolean {
|
|
@@ -736,13 +750,29 @@ function mapFromModelInfoGroup(
|
|
|
736
750
|
!hasConflictingFamily &&
|
|
737
751
|
(reduced.deploymentFamilies.includes("kimi") || (unlabeled && isMoonshotModel(reduced.id)));
|
|
738
752
|
if (moonshotEvidence && !unanimousMoonshot) options.withheldRepairRoutes?.push(reduced.id);
|
|
739
|
-
const
|
|
753
|
+
const familyPolicy = requestPolicy(
|
|
740
754
|
reduced.id,
|
|
741
755
|
reduced.deploymentFamilies,
|
|
742
756
|
reduced.normalizeThinkTags,
|
|
743
757
|
reduced.suppressReasoningVisibility,
|
|
744
758
|
unanimousMoonshot,
|
|
745
759
|
);
|
|
760
|
+
// One GPT-5.5+ deployment is enough: a tool request routed to it fails with
|
|
761
|
+
// reasoning attached, while dropping reasoning elsewhere only loses effort.
|
|
762
|
+
const chatBackendIds =
|
|
763
|
+
api === "openai-completions"
|
|
764
|
+
? entries.flatMap((entry) => resolveBackendIdentity({ ...entry, model_name: undefined })?.modelId ?? [])
|
|
765
|
+
: [];
|
|
766
|
+
const modelPolicy: LiteLLMModelPolicy | undefined = chatBackendIds.some(isGpt55OrNewerModel)
|
|
767
|
+
? {
|
|
768
|
+
normalizeStrictToolMessages: false,
|
|
769
|
+
normalizeThinkTags: false,
|
|
770
|
+
suppressReasoningVisibility: false,
|
|
771
|
+
...familyPolicy,
|
|
772
|
+
dropToolReasoning: true,
|
|
773
|
+
...(chatBackendIds.some(isGpt6OrNewerModel) ? { explicitToolReasoningOff: true } : {}),
|
|
774
|
+
}
|
|
775
|
+
: familyPolicy;
|
|
746
776
|
return {
|
|
747
777
|
id: reduced.id,
|
|
748
778
|
// Reduced groups never borrow the ` (no metadata)` sentinel, which authorizes
|
|
@@ -1008,6 +1038,10 @@ function applyWildcardEvidence(
|
|
|
1008
1038
|
...(policies.every((policy) => policy.normalizeGeminiReasoningEffort)
|
|
1009
1039
|
? { normalizeGeminiReasoningEffort: true as const }
|
|
1010
1040
|
: {}),
|
|
1041
|
+
...(policies.some((policy) => policy.dropToolReasoning) ? { dropToolReasoning: true as const } : {}),
|
|
1042
|
+
...(policies.some((policy) => policy.explicitToolReasoningOff)
|
|
1043
|
+
? { explicitToolReasoningOff: true as const }
|
|
1044
|
+
: {}),
|
|
1011
1045
|
}
|
|
1012
1046
|
: undefined;
|
|
1013
1047
|
const {
|
|
@@ -1062,6 +1096,12 @@ function mapFromWildcardExpansion(
|
|
|
1062
1096
|
...(modelPolicies.every((policy) => policy?.normalizeGeminiReasoningEffort === true)
|
|
1063
1097
|
? { normalizeGeminiReasoningEffort: true as const }
|
|
1064
1098
|
: {}),
|
|
1099
|
+
...(api === "openai-completions" && modelPolicies.some((policy) => policy?.dropToolReasoning === true)
|
|
1100
|
+
? { dropToolReasoning: true as const }
|
|
1101
|
+
: {}),
|
|
1102
|
+
...(api === "openai-completions" && modelPolicies.some((policy) => policy?.explicitToolReasoningOff === true)
|
|
1103
|
+
? { explicitToolReasoningOff: true as const }
|
|
1104
|
+
: {}),
|
|
1065
1105
|
}
|
|
1066
1106
|
: !hasFamilyEvidence && isMoonshotModel(id)
|
|
1067
1107
|
? moonshotPolicy(id)
|
|
@@ -1329,7 +1369,26 @@ export function restoreCachedModelPolicy(model: Model<Api>): Model<Api> {
|
|
|
1329
1369
|
// re-proven offline, so fail closed until the forced network refresh below.
|
|
1330
1370
|
restored = { ...cached, compat: Object.keys(compat).length > 0 ? compat : undefined };
|
|
1331
1371
|
}
|
|
1332
|
-
if (restored.litellmPolicy
|
|
1372
|
+
if (restored.litellmPolicy) return restored;
|
|
1373
|
+
// The cache no longer holds the deployment rows that prove a GPT-5.5+ backend, so an
|
|
1374
|
+
// OpenAI-family Chat entry fails toward the tool-reasoning workaround until refreshed.
|
|
1375
|
+
if (
|
|
1376
|
+
restored.api === "openai-completions" &&
|
|
1377
|
+
restored.litellmBackendFamily === "openai" &&
|
|
1378
|
+
isGpt55OrNewerModel(restored.id)
|
|
1379
|
+
) {
|
|
1380
|
+
return {
|
|
1381
|
+
...restored,
|
|
1382
|
+
litellmPolicy: {
|
|
1383
|
+
normalizeStrictToolMessages: false,
|
|
1384
|
+
normalizeThinkTags: false,
|
|
1385
|
+
suppressReasoningVisibility: false,
|
|
1386
|
+
dropToolReasoning: true,
|
|
1387
|
+
...(isGpt6OrNewerModel(restored.id) ? { explicitToolReasoningOff: true } : {}),
|
|
1388
|
+
},
|
|
1389
|
+
} as LiteLLMModel;
|
|
1390
|
+
}
|
|
1391
|
+
if (!hasMoonshotCompatEvidence(restored.compat)) return restored;
|
|
1333
1392
|
return { ...restored, litellmPolicy: moonshotPolicy(restored.id) } as LiteLLMModel;
|
|
1334
1393
|
}
|
|
1335
1394
|
|