pi-provider-litellm 3.1.0 → 3.3.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 +72 -14
- package/package.json +6 -6
- package/src/backend-identity.ts +31 -5
- package/src/discover.ts +220 -56
- package/src/index.ts +721 -197
- package/src/mcp-tools.ts +225 -30
- package/src/model-groups.ts +44 -13
- package/src/provider.ts +27 -12
- package/src/proxy-version.ts +68 -0
- package/src/public-catalog.ts +15 -8
- package/src/skills.ts +33 -76
- package/src/types.ts +16 -0
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.83.0+ is required.
|
|
3
|
+
LiteLLM proxy native Provider extension for [Pi](https://pi.dev). Pi 0.83.0+ is required; the MCP tool exposure settings need Pi 0.99.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
|
|
|
@@ -56,7 +56,7 @@ If your LiteLLM proxy supports SSO/OAuth authentication, Pi selects its supporte
|
|
|
56
56
|
2. Confirm the offered proxy URL, or enter one if this is the first login
|
|
57
57
|
3. Complete sign-in in the browser Pi opens, then return to Pi
|
|
58
58
|
|
|
59
|
-
Pi first checks `/.well-known/litellm-cli-auth`. A proxy exposing the supported CLI-auth contract uses authorization code + PKCE (`S256`) with a temporary `127.0.0.1` callback; the browser must run on the same machine as Pi. Advertised endpoints must stay on the proxy's origin, and discovery, registration, and token requests do not follow HTTP redirects. Invalid discovery stops login.
|
|
59
|
+
Without an `oidc` setting (see [Direct OIDC login](#direct-oidc-login)), Pi first checks `/.well-known/litellm-cli-auth`. A proxy exposing the supported CLI-auth contract uses authorization code + PKCE (`S256`) with a temporary `127.0.0.1` callback; the browser must run on the same machine as Pi. Advertised endpoints must stay on the proxy's origin, and discovery, registration, and token requests do not follow HTTP redirects. Invalid discovery stops login.
|
|
60
60
|
|
|
61
61
|
Pi stores PKCE access and refresh tokens in `~/.pi/agent/auth.json` with file mode `0600`, refreshes automatically, and saves each rotated token pair. Temporary network failures or HTTP 429/5xx responses allow reuse of the existing access token only until its exact expiry. Rejected or revoked refresh credentials require `/login litellm` again.
|
|
62
62
|
|
|
@@ -66,6 +66,41 @@ If `/sso/cli/start` returns 404 or 405, copy the token from the LiteLLM UI and p
|
|
|
66
66
|
|
|
67
67
|
Tokens are stored in Pi's local file, not an OS keychain. `/logout litellm` deletes the local credential only; logging out or signing in again does not revoke the previous session on the proxy because Pi has no provider revocation hook.
|
|
68
68
|
|
|
69
|
+
##### Direct OIDC login
|
|
70
|
+
|
|
71
|
+
For proxies running LiteLLM JWT auth (`enable_jwt_auth`) against your OpenID Connect identity provider (IdP), Pi can sign in with the IdP directly as a registered public client and present the IdP's `id_token` to the proxy as the bearer token. The proxy mints nothing and no virtual key is created; the IdP stays the session authority.
|
|
72
|
+
|
|
73
|
+
Prerequisites:
|
|
74
|
+
|
|
75
|
+
- The proxy's JWT auth trusts the IdP's issuer and accepts the client ID as an audience.
|
|
76
|
+
- The IdP has a public client (no client secret) registered for authorization code + PKCE with the loopback redirect `http://127.0.0.1:<port>/callback`.
|
|
77
|
+
- The browser runs on the same machine as Pi.
|
|
78
|
+
|
|
79
|
+
Configure the default `litellm` provider in the global `~/.pi/agent/settings.json`. Project settings are never read for this, so a cloned repository cannot point login at its own IdP.
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{ "litellm": { "providers": { "litellm": {
|
|
83
|
+
"baseUrl": "https://litellm.example.com",
|
|
84
|
+
"oidc": {
|
|
85
|
+
"issuer": "https://idp.example.com",
|
|
86
|
+
"clientId": "example-client-id",
|
|
87
|
+
"scope": "openid",
|
|
88
|
+
"redirectPorts": [8400, 8401]
|
|
89
|
+
}
|
|
90
|
+
} } } }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- `issuer` (required): absolute `https` URL with no credentials, query, or fragment.
|
|
94
|
+
- `clientId` (required): the public client's ID.
|
|
95
|
+
- `scope` (default `"openid"`): must include `openid`.
|
|
96
|
+
- `redirectPorts` (optional): ports tried in order, for IdPs that only accept exact pre-registered redirect URIs. Without it the OS assigns a free port.
|
|
97
|
+
|
|
98
|
+
With `oidc` set, `Sign in with LiteLLM SSO` confirms the proxy URL and then runs only this flow: Pi reads `<issuer>/.well-known/openid-configuration`, opens the IdP's authorization page, and exchanges the code for tokens. Login sends nothing to the proxy, and IdP requests never carry `LITELLM_HEADERS` or provider `headers`. An invalid `oidc` setting fails login with a message naming the field; it never falls back to the LiteLLM flows.
|
|
99
|
+
|
|
100
|
+
The `id_token` is the bearer token. Pi checks its issuer, audience, authorized party (when present), subject, expiry, and nonce but not its signature; the proxy verifies the signature against the IdP's keys. Pi refreshes it with the refresh token, so the IdP must return a new `id_token` from the refresh grant. Some IdPs only issue a refresh token when the scope includes `offline_access`; without one, run `/login litellm` again when the `id_token` expires.
|
|
101
|
+
|
|
102
|
+
Pi never derives the IdP from the proxy: LiteLLM's own `/.well-known/openid-configuration`, `oauth-authorization-server`, and `oauth-protected-resource` documents describe LiteLLM's authorization server, not the IdP that signs the JWTs it accepts. Zero-config IdP discovery from the proxy via RFC 9728 is out of scope until LiteLLM publishes it ([BerriAI/litellm#41135](https://github.com/BerriAI/litellm/issues/41135)).
|
|
103
|
+
|
|
69
104
|
### Option B — environment variables
|
|
70
105
|
|
|
71
106
|
```bash
|
|
@@ -123,13 +158,14 @@ Provider fields:
|
|
|
123
158
|
| `headers` | `$LITELLM_HEADERS` for `litellm`; unset for aliases | JSON string env reference or inline object of request headers |
|
|
124
159
|
| `displayName` | provider name | Label shown in Pi UI |
|
|
125
160
|
| `enabled` | `true` | Set `false` to skip an alias |
|
|
161
|
+
| `oidc` | unset | `litellm` only. Sign in directly with an OpenID Connect identity provider instead of the LiteLLM-hosted flows; see [Direct OIDC login](#direct-oidc-login) |
|
|
126
162
|
| `allowInsecureHttp` | `false` | Set `true` to permit plaintext HTTP for this provider, for example `http://host.docker.internal`. Credentials and request data will not be encrypted. Loopback HTTP works without this setting. |
|
|
127
163
|
|
|
128
164
|
`/login litellm` and Google ADC token auth remain scoped to the default `litellm` provider. Aliases use their configured `apiKey` or manually stored auth entries matching the alias name.
|
|
129
165
|
|
|
130
166
|
### Optional LiteLLM features
|
|
131
167
|
|
|
132
|
-
LiteLLM Skills and MCP integration are enabled by default. Disable either feature globally in `~/.pi/agent/settings.json`:
|
|
168
|
+
LiteLLM Skills and MCP integration are enabled by default. Disable either feature, or choose how MCP tools reach the model, globally in `~/.pi/agent/settings.json`:
|
|
133
169
|
|
|
134
170
|
```json
|
|
135
171
|
{
|
|
@@ -138,7 +174,12 @@ LiteLLM Skills and MCP integration are enabled by default. Disable either featur
|
|
|
138
174
|
"enabled": false
|
|
139
175
|
},
|
|
140
176
|
"mcp": {
|
|
141
|
-
"enabled":
|
|
177
|
+
"enabled": true,
|
|
178
|
+
"exposure": "deferred",
|
|
179
|
+
"toolExposure": {
|
|
180
|
+
"delete_*": "hidden",
|
|
181
|
+
"github/search_code": "direct"
|
|
182
|
+
}
|
|
142
183
|
}
|
|
143
184
|
}
|
|
144
185
|
}
|
|
@@ -146,6 +187,8 @@ LiteLLM Skills and MCP integration are enabled by default. Disable either featur
|
|
|
146
187
|
|
|
147
188
|
Setting `skills.enabled` to `false` disables the Skills Gateway management tools, skill fetching, and system-prompt injection. Setting `mcp.enabled` to `false` disables LiteLLM MCP discovery and tool registration. Restart Pi after changing these settings so previously registered tools are removed.
|
|
148
189
|
|
|
190
|
+
`mcp.exposure` and `mcp.toolExposure` take the values of a Pi 0.99 `mcp.json` server entry: `direct` (the default; every tool's name, description, and schema is sent with every request), `codemode` or `codemode-deferred` (callable only from Pi's `codemode` scripts; Pi 0.99.2 and later treat `codemode-deferred` as an alias of `codemode` for its own servers, while here the two stay distinct: `codemode` lists the tools in the codemode description and `codemode-deferred` does not), `deferred` (declared to the model only after Pi's `tool_search` loads it), or `hidden` (registered but unreachable). `toolExposure` keys are tool names as the server offers them, or `<server>/<tool>` (a key containing `/` is always read as `<server>/<tool>`, so a tool whose own name contains `/` must be addressed in that qualified form); `*` matches any characters, exact keys win over patterns, and the first matching pattern wins. When a discovery pass registers non-direct tools, the extension activates Pi's `codemode` or `tool_search` tool, as Pi's own MCP support does for its servers, and warns once when neither is available (for example under `--no-extensions`, which since Pi 0.99 also disables the built-in extensions). Pi before 0.99 ignores the setting; the extension says so once and the tools stay direct. Large shared catalogs are the reason to prefer `deferred` or `codemode`; an unusable value is reported and falls back to `direct`.
|
|
191
|
+
|
|
149
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.
|
|
150
193
|
|
|
151
194
|
## Use
|
|
@@ -154,6 +197,8 @@ Treat the configured LiteLLM proxy as trusted: Skills can add instructions to th
|
|
|
154
197
|
/model
|
|
155
198
|
```
|
|
156
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. With LiteLLM MCP enabled, the refresh also re-checks that provider's MCP tools.
|
|
201
|
+
|
|
157
202
|
Every request routed through a configured LiteLLM provider includes Pi's canonical session ID in the
|
|
158
203
|
`x-litellm-session-id` header. This groups Chat Completions, Responses, and native Messages requests in LiteLLM without
|
|
159
204
|
adding transport-specific fields to request bodies.
|
|
@@ -162,7 +207,7 @@ adding transport-specific fields to request bodies.
|
|
|
162
207
|
|
|
163
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 present, it must include `/v1/messages`; an explicit endpoint list without it keeps the group on Chat Completions. 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.
|
|
164
209
|
|
|
165
|
-
Native Messages authenticates with `x-api-key`; every transport carries the `x-litellm-session-id` header described above.
|
|
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.
|
|
166
211
|
|
|
167
212
|
## Optional environment variables
|
|
168
213
|
|
|
@@ -173,13 +218,17 @@ Native Messages authenticates with `x-api-key`; every transport carries the `x-l
|
|
|
173
218
|
| `LITELLM_GCLOUD_TOKEN_AUTH` | unset | If set to a non-empty value other than `0`, use Google Application Default Credentials as the LiteLLM bearer token source. This takes precedence over `LITELLM_API_KEY_HELPER` and `LITELLM_API_KEY` when no stored `/login litellm` credential exists. |
|
|
174
219
|
| `GOOGLE_APPLICATION_CREDENTIALS` | Google default ADC path | Optional path to an ADC JSON file used by `LITELLM_GCLOUD_TOKEN_AUTH`. If unset, the extension checks the default gcloud ADC locations. |
|
|
175
220
|
| `LITELLM_OFFLINE` | unset | If `1`, disable all model and MCP discovery, including post-login discovery; use cached models only when their stored canonical proxy root exactly matches the active credential root, including any path prefix. URL-standard host casing and default ports are canonicalized, but paths remain case-sensitive. |
|
|
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. |
|
|
176
222
|
| `LITELLM_DISCOVERY_TIMEOUT_MS` | `5000` | Background and explicit discovery fetch timeout in ms; `0` disables automatic discovery |
|
|
177
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 |
|
|
178
|
-
| `LITELLM_VERBOSE_DISCOVERY` | unset | If `1`, enable progress messages during model and MCP discovery (login, refresh, startup), including MCP prepared/registered/dropped counts. Progress messages are off by default; MCP safety diagnostics (see below) are always reported regardless of this setting |
|
|
224
|
+
| `LITELLM_VERBOSE_DISCOVERY` | unset | If `1`, enable progress messages during model and MCP discovery (login, refresh, startup), including startup skip reasons, defaulted `/model/info` context windows, and MCP prepared/registered/dropped counts. Progress messages are off by default; MCP safety diagnostics (see below) are always reported regardless of this setting |
|
|
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 |
|
|
179
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 |
|
|
180
227
|
|
|
181
228
|
Only use a trusted `LITELLM_API_KEY_HELPER` or `!command`. Prefer an absolute executable path, keep secrets out of command arguments, and print only the token to stdout without logging it to stderr.
|
|
182
229
|
|
|
230
|
+
With `LITELLM_VERBOSE_DISCOVERY=1`, `/model/info` discovery reports published routes whose context window uses the fallback assumption (including wildcard expansions) on one line with the selected value, a count, and a sample of escaped route names. Each route is reported once per process. Explicit or authoritative catalog limits do not trigger it, even when numerically equal to the default. Set `model_info.max_input_tokens` on every deployment in the route to provide real limits; this diagnostic does not change discovery limits or routing.
|
|
231
|
+
|
|
183
232
|
`LITELLM_DISCOVERY_TIMEOUT_MS=0` disables automatic and explicit refresh model discovery. It does not replace the base URL or API key settings required to send requests when you are not using `/login litellm`.
|
|
184
233
|
|
|
185
234
|
### Google ADC token auth
|
|
@@ -201,11 +250,13 @@ If your LiteLLM proxy exposes MCP REST endpoints, this extension discovers tools
|
|
|
201
250
|
- `GET /mcp-rest/tools/list`
|
|
202
251
|
- `POST /mcp-rest/tools/call`
|
|
203
252
|
|
|
204
|
-
An MCP access denial pauses discovery until a successful `/login litellm` (SSO or API key). The pause survives restarts in `~/.pi/agent/litellm-mcp-pauses/`, scoped to the proxy, login, and headers with salted credential fingerprints. A new API key or header configuration has its own scope. Each explicit login creates a fresh opaque `litellmMcpSession` identifier in Pi's stored credential, even when the key is unchanged; automatic OAuth refresh preserves that identifier and its pause. Older Pi processes cannot pause the new login. Pause files contain no credentials or proxy error text. Model discovery and chat continue normally. Grant MCP access on the proxy before signing in again. An explicit `litellm.mcp.enabled: false` setting remains disabled after login.
|
|
253
|
+
An MCP access denial pauses discovery until a successful `/login litellm` (SSO or API key); an alias provider, which has no login, stays paused until its credentials, base URL, or headers change. The pause survives restarts in `~/.pi/agent/litellm-mcp-pauses/`, scoped to the proxy, login, and headers with salted credential fingerprints. A new API key or header configuration has its own scope. Each explicit login creates a fresh opaque `litellmMcpSession` identifier in Pi's stored credential, even when the key is unchanged; automatic OAuth refresh preserves that identifier and its pause. Older Pi processes cannot pause the new login. Pause files contain no credentials or proxy error text. Model discovery and chat continue normally. Grant MCP access on the proxy before signing in again. An explicit `litellm.mcp.enabled: false` setting remains disabled after login.
|
|
254
|
+
|
|
255
|
+
Pi 0.99 ships its own MCP client, which reads `mcp.json` and names tools `mcp__<server>__<tool>`. This extension names its tools `mcp_<server>_<tool>_<hash>` and registers none of `/mcp`, `codemode`, or `tool_search`, so both load side by side without replacing each other. Each tool carries a `namespace` (`mcp_<server>_<hash>`, or `mcp_<alias>_<server>_<hash>` for an alias provider) built from the server name plus a 6-character hash of the server identity so distinct servers never share a group, with the server description LiteLLM reports, and the boolean MCP annotation hints the proxy passes through (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`), which permission extensions can read from `pi.getAllTools()`; other annotation fields are dropped. To let Pi's client talk to LiteLLM's streamable HTTP MCP endpoint instead, add its `/mcp` URL to `mcp.json` with the header LiteLLM documents for its MCP gateway and set `litellm.mcp.enabled` to `false`; that path uses a static key and Pi's own client rather than this extension's credentials, access-denied pauses, and schema guard.
|
|
205
256
|
|
|
206
|
-
Each discovered tool is registered as a native Pi tool named `mcp_<server>_<tool>_<hash>`, at most 64 characters, using only `[a-z0-9_]`. The trailing 10-character hash is derived from the tool's identity (`server_id`, `server_name`, `name`) and is always present, so a name depends only on that tool and never on which other tools happen to be in the same catalog — adding or removing a sibling never renames a survivor. When the readable prefix would overflow 64 characters it is truncated from the right, so a long server name can leave little or none of the tool name visible; the hash is what distinguishes such tools. Tools from different servers therefore never overwrite each other, and exact duplicate identities are registered once.
|
|
257
|
+
Each discovered tool is registered as a native Pi tool named `mcp_<server>_<tool>_<hash>`, at most 64 characters, using only `[a-z0-9_]`. The trailing 10-character hash is derived from the tool's identity (`server_id`, `server_name`, `name`) and is always present, so a name depends only on that tool and never on which other tools happen to be in the same catalog — adding or removing a sibling never renames a survivor. When the readable prefix would overflow 64 characters it is truncated from the right, so a long server name can leave little or none of the tool name visible; the hash is what distinguishes such tools. Tools from different servers therefore never overwrite each other, and exact duplicate identities are registered once. Each alias provider discovers its own catalog: its tools are named `mcp_<alias>_<server>_<tool>_<hash>`, with the alias name also folded into the hash, and are called with that alias's credentials, so two providers exposing the same server never replace each other's tools. The default `litellm` provider keeps unprefixed names, and diagnostics for an alias read `LiteLLM MCP ("<alias>"):`.
|
|
207
258
|
|
|
208
|
-
MCP discovery runs after Pi refreshes LiteLLM models
|
|
259
|
+
MCP discovery runs once for every configured provider before the first agent turn (so `-p` and other non-interactive modes get MCP tools), after Pi refreshes LiteLLM models, and after `/login litellm`; extension activation never waits for it. Discovery accepts at most a 5 MiB response and registers at most 512 tools. The response must be a JSON array of tools or an object with a `tools` array; an `unexpected_error` or unknown non-null `error` in the object envelope, an object without `tools`, and every other shape are reported as discovery failures rather than as empty catalogs. Failure reports use extension-owned text and never include LiteLLM's proxy-supplied tag or `message`. A `partial_failure` envelope keeps and registers the returned tools while reporting one bounded generated diagnostic. Each tool is isolated during normalization, so one bad tool never hides valid siblings. A tool is accepted only when its schema has a root of exactly `"type": "object"` (a bare `properties` object with no `type`, or a type union such as `["object","null"]`, is refused), plain-object `properties`, at most 16 levels of JSON nesting (about eight nested schema objects when each level uses `properties`), and a serialized size of at most 64 KiB. Descriptions are limited to 4 KiB, and tool labels and result metadata to 256 bytes, each with a truncation marker.
|
|
209
260
|
|
|
210
261
|
### Which schema a tool ends up with
|
|
211
262
|
|
|
@@ -241,9 +292,9 @@ Pi registers a tool synchronously, replacing any existing tool of the same name,
|
|
|
241
292
|
|
|
242
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.
|
|
243
294
|
|
|
244
|
-
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
|
|
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.
|
|
245
296
|
|
|
246
|
-
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.
|
|
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).
|
|
247
298
|
|
|
248
299
|
A passed-through schema's `format` keyword is evaluated, using the validator's own built-in expressions rather than anything the proxy supplies.
|
|
249
300
|
|
|
@@ -257,6 +308,8 @@ If your LiteLLM proxy exposes `/claude-code/marketplace.json`, enabled skills ar
|
|
|
257
308
|
- `litellm_skill_create`
|
|
258
309
|
- `litellm_skill_delete`
|
|
259
310
|
|
|
311
|
+
`litellm_skill_create` needs Skill Hub `sourceJson` metadata and a proxy that exposes `/claude-code/plugins`. It does not create skills from code: LiteLLM's `POST /v1/skills` is Anthropic's multipart Skills API.
|
|
312
|
+
|
|
260
313
|
## Mocked LiteLLM smoke workflow
|
|
261
314
|
|
|
262
315
|
The `LiteLLM Smoke` GitHub Actions workflow starts VidaiMock and a real LiteLLM proxy on the runner. LiteLLM exposes route-distinct Chat, Responses, native Messages, and mixed-deployment models whose upstreams are served by VidaiMock. The smoke runner discovers those models through LiteLLM, asserts each model's expected API, exercises `/v1/chat/completions`, `/v1/responses`, and `/v1/messages`, verifies the expected `x-litellm-response-cost` behavior, and proves endpoint coverage from captured LiteLLM request logs rather than response text.
|
|
@@ -296,7 +349,9 @@ Dynamic catalogs are persisted by Pi in `~/.pi/agent/models-store.json`. Credent
|
|
|
296
349
|
|
|
297
350
|
LiteLLM's `/model/info` is the metadata authority, so models.dev enrichment is an opt-in escape hatch. With `LITELLM_MODELS_DEV=1`, for every genuine `/model/info` row, including deployment details fetched through `/health`, the extension requests `https://models.dev/api.json` for enrichment and caches the result for 28 days in `~/.pi/agent/litellm-models-dev.json`. Without it, discovery neither requests models.dev nor reads that cache; Pi's own catalog still applies. Health-only entries without deployment details use the bounded Pi catalog lookup without models.dev enrichment. `PI_OFFLINE` suppresses activation-time discovery and the models.dev request. `LITELLM_OFFLINE=1` also disables LiteLLM discovery; direct discovery callers use only an existing models.dev cache and do not refresh it.
|
|
298
351
|
|
|
299
|
-
Opening `/model` refreshes configured provider catalogs in the background using Pi's native model lifecycle.
|
|
352
|
+
Opening `/model` refreshes configured provider catalogs in the background using Pi's native model lifecycle when network discovery is enabled.
|
|
353
|
+
|
|
354
|
+
Pi documents `PI_OFFLINE` and `--offline` as disabling automatic network activity, including model catalog refreshes. Pi 0.87.1 adopted that wording to resolve [earendil-works/pi#8684](https://github.com/earendil-works/pi/issues/8684), without changing the behavior. Pi still checks only whether the variable is set, so `PI_OFFLINE=0` and an empty value also disable model refreshes. With `LITELLM_VERBOSE_DISCOVERY=1`, disabled startup discovery reports its reason, for example `startup discovery skipped (PI_OFFLINE)`. To discover models while keeping `PI_OFFLINE`, run `/litellm-refresh`. To allow all discovery for one invocation, use `env -u PI_OFFLINE pi` and do not pass `--offline`. If you only want to disable Pi's version check, use `PI_SKIP_VERSION_CHECK=1` instead; it does not apply offline mode's other network restrictions. See [#155](https://github.com/balcsida/pi-provider-litellm/issues/155).
|
|
300
355
|
|
|
301
356
|
### Model host enforcement
|
|
302
357
|
|
|
@@ -319,7 +374,7 @@ Each distinct availability diagnostic is written once per session on stderr. `LI
|
|
|
319
374
|
|
|
320
375
|
### Protocols and prompt caching
|
|
321
376
|
|
|
322
|
-
Discovery selects native Messages for proven Claude groups under the [Model transport](#model-transport) rules, Responses for OpenAI-family backends, and Chat Completions for other OpenAI-compatible backends.
|
|
377
|
+
Discovery selects native Messages for proven Claude groups under the [Model transport](#model-transport) rules, Responses for OpenAI-family backends, and Chat Completions for other OpenAI-compatible backends. Azure deployments stay on Chat Completions unless an explicit Responses mode or a `supported_endpoints` list containing `/v1/responses` authorizes Responses; `api_version` alone does not prove support. Azure AI (`azure_ai/` prefix or `custom_llm_provider: azure_ai`) deployments additionally depend on the proxy version. On LiteLLM before v1.103.0, or when the version is unknown, they ignore a `supported_endpoints` list that contains `/v1/responses` and need an explicit `mode: "responses"`: those releases have no native Responses config for that provider, so they bridge `/v1/responses` through Chat Completions, and the bridge fails streaming with `list index out of range` on Azure's empty-choices chunks (BerriAI/litellm#34455) while the endpoint list, copied from the model cost map, still advertises `/v1/responses`. From v1.103.0 the list is honored again. Discovery reads the version from the `x-litellm-version` header of one extra request, `GET /v1/responses/resp_version_probe`, sent only when such a deployment is published; LiteLLM answers it with an error and calls no model. A pre-release counts as older than the release it names, and a proxy that omits the header keeps these deployments on Chat Completions. Azure is detected from the adapter field, an `azure/` or `azure_ai/` model prefix, or the reported provider; other adapters follow the backend family because LiteLLM bridges `/v1/responses` to Chat Completions when the provider has no native Responses config (`litellm/responses/main.py`, `_bridges_to_chat_completions`). A route whose model prefix and `custom_llm_provider` name different providers is treated as unidentified and stays on Chat Completions, so it also does not get the local 128-tool preflight. When `/model/info` supplies `supported_endpoints`, that list takes precedence, and `mode: "responses"` remains an explicit Responses signal. An evidence-free fallback entry—a bare `/v1/models` id or a `/health` entry with no detail row—takes its protocol from the Pi catalog entry for its id; both evidence-free fallback paths also inherit presentation metadata from the bounded catalog lookup. An unknown id stays on Chat Completions. The route name alone authorizes nothing. A concrete id expanded from wildcard `/model/info` routes inherits the deployment evidence of the wildcard route LiteLLM would select for it, with the requested id substituted into the row's `model` the way LiteLLM serves it; the id is omitted when that selected route is not a published chat-style route. Chat Completions and Responses use `<root>/v1`; native Messages uses the proxy root directly. All three protocols pass through the provider's host guard. When an evidence-free cached `(no metadata)` entry can be enriched from the Pi catalog, it is restored on Responses only for a catalog Responses model and otherwise on Chat Completions, never Anthropic Messages. A model supplied only through `models.json` whose `api` this provider does not implement takes Pi's global API fallback rather than this provider's host guard; the credential-root pin (see [Model host enforcement](#model-host-enforcement)) still keeps the credential on the active proxy.
|
|
323
378
|
|
|
324
379
|
`cacheControlFormat: "anthropic"` applies only to Chat Completions, where Pi adds Anthropic `cache_control` markers. The Responses transport has a different compatibility type and uses native `prompt_cache_key` (plus supported retention fields), so Responses models must not receive `cacheControlFormat`. Freshly discovered OpenAI-family models forced onto Chat Completions are rejected locally when a request contains more than 128 tools; route them through Responses or reduce enabled extensions. Moonshot request-side reasoning suppression follows LiteLLM's routing provider, not `model_info.base_model`; response-side `<think>` normalization follows discovered Kimi-family evidence. Opaque aliases retain display normalization without sending Moonshot-only parameters to Azure- or Bedrock-hosted Kimi models. Strict tool-message repair requires every deployment to identify Kimi; Gemini effort normalization likewise requires consistent backend-family evidence.
|
|
325
380
|
|
|
@@ -331,7 +386,7 @@ LiteLLM may load-balance one public `model_name` across deployments with differe
|
|
|
331
386
|
|
|
332
387
|
- Responses is selected only when every deployment supports it under the protocol rules above; a deployment that requires Chat keeps the group on Chat.
|
|
333
388
|
- Vision and reasoning are advertised only when every routable deployment resolves them as supported. An explicit `supports_reasoning: false` suppresses all thinking controls, even when catalog or router effort metadata exists. Catalog thinking controls are intersected per level: a level is exposed only when every deployment maps it to the same value, while disagreement or absence becomes an explicit denial. Catalog maps are not complete effort lists, so absence has one exception: an omitted `minimal`, `low`, `medium` or `high` counts as Pi's default spelling of that level. An omitted `off`, `xhigh` or `max` still differs from an explicit mapping. Reasoning selectors additionally require an accepted wire carrier on every deployment. Public effort lists are intersected, explicit LiteLLM denials win, and `xhigh`/`max` need unanimous explicit support. Null or absent standard-effort flags have no opinion. A deployment that declares `model_info.reasoning_effort_levels` answers every level from that list, ahead of its per-level flags, as LiteLLM itself reads it.
|
|
334
|
-
- Context and output limits use the minimum resolved value across deployments.
|
|
389
|
+
- Context and output limits use the minimum resolved value across deployments. Pi catalogue lookup prefers the deployment's provider-specific entry before falling back to the generic vendor entry; Azure and Azure AI therefore retain Azure-specific limits rather than being capped by a different OpenAI catalogue entry. When models.dev supplies a sparse vendor record, missing fields still use the provider-specific Pi catalogue.
|
|
335
390
|
- Each displayed base-price field uses the maximum only when every deployment resolves that field; unresolved fields remain zero and the model name is suffixed with ` (incomplete metadata)`.
|
|
336
391
|
- When complete base pricing comes from the catalog under unanimous catalog authority, tiered pricing is the conservative worst-case envelope. Its usable finite non-negative thresholds are the sorted union of every deployment's thresholds, and each price field at each interval is the maximum applicable rate across all deployments, floored by each deployment's base rate. Differing tier breakpoints are therefore preserved rather than treated as incompatible. An explicit `/model/info` base-price field replaces catalog pricing for that field at every threshold; catalog tiers remain for unaffected fields and are omitted only when every base-price field is explicit.
|
|
337
392
|
- Reduced route groups omit tier ladders when complete base-pricing or catalog authority is unavailable. Wildcard expansion retains every known tier from matching groups even when another match has incomplete metadata: suppressing a complete sibling's higher tier could understate the known worst-case rate. The ` (incomplete metadata)` marker remains because the resulting envelope is only as complete as the available ladders.
|
|
@@ -359,6 +414,7 @@ The development probe runs against minimized snapshots with `npm run probe:proxy
|
|
|
359
414
|
| Symptom | Likely cause |
|
|
360
415
|
|---|---|
|
|
361
416
|
| "no credentials" warning at startup | Env vars not set and no OAuth credential — run `/login litellm` |
|
|
417
|
+
| No models, or no new routes, while `PI_OFFLINE` is set | Pi skips every model network refresh, including the one `/model` starts. Run `/litellm-refresh`, or unset `PI_OFFLINE` (`PI_OFFLINE=0` is not enough) |
|
|
362
418
|
| `No models available` at startup, gone after a restart | A Pi startup race, not discovery — see [`No models available` at startup](#no-models-available-at-startup) |
|
|
363
419
|
| "discovered no models" | Proxy returned an empty list — check pi's startup log and verify `/model/info`, `/v1/models`, or `/health` responds |
|
|
364
420
|
| `/model/info` returning 401/403/404 | Expected behavior with virtual keys — extension falls back to `/v1/models` |
|
|
@@ -367,6 +423,7 @@ The development probe runs against minimized snapshots with `npm run probe:proxy
|
|
|
367
423
|
| A configured or restored model fails to complete but never leaks the credential | Its `api` is absent from the current provider catalog, so Pi used global API fallback with the credential root pinned. Open `/model` against the active proxy to repopulate the protocol — see [Model host enforcement](#model-host-enforcement) |
|
|
368
424
|
| `LiteLLM discovery: ... route group(s) have missing or conflicting deployment provider evidence` | One or more deployments lack resolvable backend authority or resolve to different providers or concrete catalog models. Add consistent `litellm_params.model`, `model_info.base_model`, or adapter metadata; catalog-derived limits, pricing, and reasoning metadata are withheld meanwhile. This may be intentional for cross-host routes such as Vertex/Bedrock Claude. |
|
|
369
425
|
| `LiteLLM discovery: ... route group(s) mix chat-style and explicitly incompatible deployment modes` | A public route can select both a chat-style deployment and an incompatible deployment such as an embedding model. Give the embedding deployment its own `model_name`. |
|
|
426
|
+
| `LiteLLM (...): a fallback served "<route>"` | A successful HTTP response reports a LiteLLM router fallback (`x-litellm-attempted-fallbacks`). `/model/info` does not expose fallbacks, so protocol and model handling follow the requested route's own deployments. If the primary and fallback groups support Chat Completions, set `model_info.supported_endpoints: ["/v1/chat/completions"]` on the primary to avoid Responses-to-Chat bridging. This warning does not change routing or fix upstream bridging defects. Warned once per provider and route until the extension reloads; shown in the UI, or stderr without a UI. |
|
|
370
427
|
| A model is marked ` (incomplete metadata)` | `/model/info` or `/health` did not provide enough authoritative metadata. Explicit fields remain usable, but unknown cost fields are shown as zero and route-name cache enrichment stays disabled. |
|
|
371
428
|
| `401 Token expired` | Set `LITELLM_API_KEY_HELPER`. |
|
|
372
429
|
| 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 |
|
|
@@ -375,6 +432,7 @@ The development probe runs against minimized snapshots with `npm run probe:proxy
|
|
|
375
432
|
| 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 |
|
|
376
433
|
| Enterprise SSO token prompt fails with "SSO token is required" | The token field was left empty — paste the token copied from the LiteLLM UI |
|
|
377
434
|
| MCP tools not showing | Verify the proxy exposes `/mcp-rest/tools/list` and open `/model` after fixing the proxy |
|
|
435
|
+
| MCP tools registered but the model cannot call them | Check `litellm.mcp.exposure`: `codemode` and `codemode-deferred` tools need Pi's `codemode` tool and `deferred` tools need `tool_search`, both built into Pi 0.99 and disabled by `--no-extensions`. Use `direct` to declare the tools on every request |
|
|
378
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 |
|
|
379
437
|
| Skills not affecting prompts | Verify the proxy exposes `/claude-code/marketplace.json` or `/v1/skills` and returns enabled skills |
|
|
380
438
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-provider-litellm",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "LiteLLM proxy provider extension for Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"extensions": [
|
|
30
30
|
"./src/index.ts"
|
|
31
31
|
],
|
|
32
|
-
"image": "https://raw.githubusercontent.com/balcsida/pi-provider-litellm/refs/heads/main/assets/pi_litellm_gallery.
|
|
32
|
+
"image": "https://raw.githubusercontent.com/balcsida/pi-provider-litellm/refs/heads/main/assets/pi_litellm_gallery.svg"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"clean": "rm -rf dist",
|
|
@@ -63,18 +63,18 @@
|
|
|
63
63
|
"basic-ftp": "6.0.1",
|
|
64
64
|
"nanoid": "3.3.18",
|
|
65
65
|
"protobufjs": "8.7.1",
|
|
66
|
-
"undici": "8.
|
|
66
|
+
"undici": "8.10.2"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@biomejs/biome": "^2.4.15",
|
|
70
|
-
"@earendil-works/pi-ai": "^0.
|
|
71
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
70
|
+
"@earendil-works/pi-ai": "^1.0.0",
|
|
71
|
+
"@earendil-works/pi-coding-agent": "^1.0.0",
|
|
72
72
|
"@kitlangton/terminal-control": "1.2.1",
|
|
73
73
|
"@types/node": "^26.0.0",
|
|
74
74
|
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
75
75
|
"es-module-lexer": "^3.0.2",
|
|
76
76
|
"tsx": "^4.21.0",
|
|
77
|
-
"typebox": "1.3.
|
|
77
|
+
"typebox": "1.3.34",
|
|
78
78
|
"vitest": "^5.0.0"
|
|
79
79
|
}
|
|
80
80
|
}
|
package/src/backend-identity.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const LITELLM_DISCOVERY_VERSION =
|
|
1
|
+
export const LITELLM_DISCOVERY_VERSION = 3 as const;
|
|
2
2
|
|
|
3
3
|
export type BackendFamily = "claude" | "deepseek" | "gemini" | "kimi" | "openai";
|
|
4
4
|
|
|
@@ -29,6 +29,10 @@ const PROVIDER_FAMILIES: Readonly<Record<string, BackendFamily>> = {
|
|
|
29
29
|
// Prefixes whose keyword match is trustworthy across the whole "prefix/modelId" string because
|
|
30
30
|
// the prefix itself names a vendor, not just some unrelated string that happens to contain one.
|
|
31
31
|
const KNOWN_VENDOR_PREFIXES = new Set([...Object.keys(PROVIDER_FAMILIES), "openai"]);
|
|
32
|
+
// First segments that are known to be model path, never a provider: Fireworks' account-scoped
|
|
33
|
+
// ids ("accounts/fireworks/models/x"). An allowlist fails closed: any other differing prefix,
|
|
34
|
+
// recognized provider or not, stays a conflict.
|
|
35
|
+
const MODEL_PATH_SEGMENTS = new Set(["accounts"]);
|
|
32
36
|
// `o\d` (OpenAI's o1/o3/o4-mini reasoning models) is only trustworthy at the start of the id or
|
|
33
37
|
// right after a provider path segment — an interior "-o1-" is as likely to be an unrelated
|
|
34
38
|
// product's own version marker (e.g. "custom-o1-clone").
|
|
@@ -62,14 +66,20 @@ export function resolveBackendIdentity(row: BackendIdentityRow): BackendIdentity
|
|
|
62
66
|
wireString(row.model_info?.base_model) ?? wireString(row.litellm_params?.model) ?? wireString(row.model_name);
|
|
63
67
|
if (!raw) return undefined;
|
|
64
68
|
|
|
65
|
-
|
|
69
|
+
let slash = raw.indexOf("/");
|
|
66
70
|
const prefix = slash > 0 ? raw.slice(0, slash).trim().toLowerCase() : undefined;
|
|
67
|
-
|
|
71
|
+
let prefixProvider = prefix && !GENERIC_ADAPTERS.has(prefix) ? prefix : undefined;
|
|
68
72
|
// `custom_llm_provider` is how LiteLLM routes an unprefixed model. It is provider evidence
|
|
69
|
-
// in its own right, so a prefix
|
|
73
|
+
// in its own right, so a differing prefix is a conflict, not a tiebreak. The exception is a
|
|
74
|
+
// known model-path segment: LiteLLM routes by custom_llm_provider, so Fireworks'
|
|
75
|
+
// "accounts/fireworks/models/x" is not a provider named "accounts".
|
|
70
76
|
const custom = wireString(row.litellm_params?.custom_llm_provider)?.toLowerCase();
|
|
71
77
|
const customProvider = custom && !GENERIC_ADAPTERS.has(custom) ? custom : undefined;
|
|
72
|
-
if (prefixProvider && customProvider && prefixProvider !== customProvider)
|
|
78
|
+
if (prefixProvider && customProvider && prefixProvider !== customProvider) {
|
|
79
|
+
if (!MODEL_PATH_SEGMENTS.has(prefixProvider)) return undefined;
|
|
80
|
+
prefixProvider = undefined;
|
|
81
|
+
slash = -1;
|
|
82
|
+
}
|
|
73
83
|
const provider = prefixProvider ?? customProvider;
|
|
74
84
|
const modelId = slash > 0 ? raw.slice(slash + 1) : raw;
|
|
75
85
|
// Scan the full "prefix/modelId" string only when the prefix is itself a whole known vendor
|
|
@@ -98,3 +108,19 @@ export function resolveCatalogProvider(
|
|
|
98
108
|
}
|
|
99
109
|
return identity?.provider ?? reported ?? custom;
|
|
100
110
|
}
|
|
111
|
+
|
|
112
|
+
// LiteLLM routes a deployment by `custom_llm_provider` and the `litellm_params.model` prefix.
|
|
113
|
+
// `model_info.litellm_provider` is a cost-map lookup (of `base_model` when one is set), so it is
|
|
114
|
+
// not routing evidence. With no declared signal, LiteLLM picks the provider from its model lists,
|
|
115
|
+
// where an unprefixed Bedrock id still routes to Bedrock.
|
|
116
|
+
export function routesOnlyThrough(row: BackendIdentityRow, providers: ReadonlySet<string>): boolean {
|
|
117
|
+
const model = wireString(row.litellm_params?.model);
|
|
118
|
+
const slash = model?.indexOf("/") ?? -1;
|
|
119
|
+
const routing = [
|
|
120
|
+
wireString(row.litellm_params?.custom_llm_provider),
|
|
121
|
+
model && slash > 0 ? model.slice(0, slash) : undefined,
|
|
122
|
+
]
|
|
123
|
+
.map((provider) => provider?.trim().toLowerCase())
|
|
124
|
+
.filter((provider): provider is string => Boolean(provider));
|
|
125
|
+
return routing.length > 0 && routing.every((provider) => providers.has(provider));
|
|
126
|
+
}
|