pi-provider-litellm 1.2.7 → 1.2.9
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 +80 -10
- package/dist/cost.d.ts +5 -1
- package/dist/cost.js +0 -0
- package/dist/discover.d.ts +5 -0
- package/dist/discover.js +71 -29
- package/dist/index.js +720 -171
- package/dist/mcp-tools.d.ts +3 -3
- package/dist/mcp-tools.js +76 -64
- package/dist/skills.d.ts +7 -6
- package/dist/skills.js +112 -32
- package/dist/types.d.ts +10 -0
- package/package.json +6 -8
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
LiteLLM proxy provider extension for [Pi](https://pi.dev).
|
|
4
4
|
|
|
5
|
-
Discovers models from
|
|
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-refresh`, 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
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -44,6 +44,21 @@ You can also run `/login`, select `Use a subscription`, then select `LiteLLM`.
|
|
|
44
44
|
|
|
45
45
|
You'll be prompted for the base URL and API key. Credentials are persisted to `~/.pi/agent/auth.json`.
|
|
46
46
|
|
|
47
|
+
#### Enterprise SSO login
|
|
48
|
+
|
|
49
|
+
If your LiteLLM proxy requires SSO/OAuth authentication (enterprise deployments), you can authenticate via a browser SSO flow and optionally pair the resulting JWT with a stable virtual key:
|
|
50
|
+
|
|
51
|
+
1. Run `/login litellm` inside pi
|
|
52
|
+
2. Enter the proxy URL
|
|
53
|
+
3. At the login method prompt, enter `2` for SSO / Enterprise JWT
|
|
54
|
+
4. Your default browser opens the LiteLLM SSO login URL (e.g. `https://litellm.your-domain.com/sso/key/generate`) automatically — the URL is also displayed in case it can't be opened. Authenticate via SSO
|
|
55
|
+
5. Copy your token from the LiteLLM UI and paste it at the prompt (copying a full `Bearer ...` header value is fine — the prefix is stripped automatically)
|
|
56
|
+
6. When prompted to generate a virtual key, press Enter to accept (recommended) or enter `n` to use the JWT directly
|
|
57
|
+
|
|
58
|
+
When you generate a virtual key, the resulting `sk-...` key is stored as your credential and used for all API requests. If the proxy's key policy attaches an expiry to the generated key, Pi will prompt you to re-authenticate when it nears expiry; otherwise the key is treated as permanent until revoked in LiteLLM.
|
|
59
|
+
|
|
60
|
+
When using a JWT directly, the extension reads its `exp` claim and Pi will prompt you to re-authenticate when the token nears expiry. Run `/login litellm` again to refresh.
|
|
61
|
+
|
|
47
62
|
### Option B — environment variables
|
|
48
63
|
|
|
49
64
|
```bash
|
|
@@ -53,6 +68,57 @@ export LITELLM_API_KEY="sk-..."
|
|
|
53
68
|
|
|
54
69
|
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.
|
|
55
70
|
|
|
71
|
+
### Multiple LiteLLM provider aliases
|
|
72
|
+
|
|
73
|
+
Add alias providers in `~/.pi/agent/settings.json` under `litellm.providers`. Each alias is registered as a separate Pi provider name, so models appear as `litellm/model-id` and `litellm-anthropic/model-id`.
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"litellm": {
|
|
78
|
+
"providers": {
|
|
79
|
+
"litellm-anthropic": {
|
|
80
|
+
"baseUrl": "https://litellm.your-domain.com",
|
|
81
|
+
"apiKey": "$LITELLM_CLAUDE_KEY",
|
|
82
|
+
"headers": "$LITELLM_HEADERS"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
You can also override the default provider through the same shape:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"litellm": {
|
|
94
|
+
"providers": {
|
|
95
|
+
"litellm": {
|
|
96
|
+
"baseUrl": "https://litellm.your-domain.com",
|
|
97
|
+
"apiKey": "$LITELLM_API_KEY",
|
|
98
|
+
"headers": "$LITELLM_HEADERS"
|
|
99
|
+
},
|
|
100
|
+
"litellm-anthropic": {
|
|
101
|
+
"baseUrl": "https://litellm.your-domain.com",
|
|
102
|
+
"apiKey": "$LITELLM_CLAUDE_KEY",
|
|
103
|
+
"headers": "$LITELLM_HEADERS"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Provider fields:
|
|
111
|
+
|
|
112
|
+
| Field | Default | Effect |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| `baseUrl` | `LITELLM_BASE_URL` for `litellm`; required for aliases | LiteLLM proxy URL, with or without `/v1` |
|
|
115
|
+
| `apiKey` | `LITELLM_API_KEY_HELPER`/`LITELLM_API_KEY` for `litellm`; required for aliases | Pi config value for this provider's key. Use `$ENV_VAR`, `${ENV_VAR}`, `!command`, or a literal key. Escape a literal `$` as `$$`. |
|
|
116
|
+
| `headers` | `$LITELLM_HEADERS` for `litellm`; unset for aliases | JSON string env reference or inline object of request headers |
|
|
117
|
+
| `displayName` | provider name | Label shown in Pi UI |
|
|
118
|
+
| `enabled` | `true` | Set `false` to skip an alias |
|
|
119
|
+
|
|
120
|
+
`/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.
|
|
121
|
+
|
|
56
122
|
## Use
|
|
57
123
|
|
|
58
124
|
```
|
|
@@ -64,7 +130,8 @@ Stored pi credentials for `litellm` take precedence over `LITELLM_API_KEY`; the
|
|
|
64
130
|
| Variable | Default | Effect |
|
|
65
131
|
|---|---|---|
|
|
66
132
|
| `LITELLM_API_KEY_HELPER` | unset | Command that prints a fresh LiteLLM bearer token. Takes precedence over `LITELLM_API_KEY`. Registered as a `!command` provider key; Pi re-runs it on every request (the per-request auth path is uncached), so rotating/short-lived tokens stay fresh. |
|
|
67
|
-
| `
|
|
133
|
+
| `LITELLM_HEADERS` | unset | JSON object of extra headers sent to LiteLLM provider, discovery, MCP, and Skills Gateway requests. Provider aliases can use it with `"headers": "$LITELLM_HEADERS"`. |
|
|
134
|
+
| `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. |
|
|
68
135
|
| `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. |
|
|
69
136
|
| `LITELLM_OFFLINE` | unset | If `1`, skip discovery on this start; use cache only |
|
|
70
137
|
| `LITELLM_DISCOVERY_TIMEOUT_MS` | `5000` | Discovery fetch timeout in ms; `0` to skip discovery |
|
|
@@ -81,7 +148,7 @@ export LITELLM_BASE_URL="https://litellm.your-domain.com"
|
|
|
81
148
|
export LITELLM_GCLOUD_TOKEN_AUTH=1
|
|
82
149
|
```
|
|
83
150
|
|
|
84
|
-
Only `authorized_user` ADC files are supported. Service account JSON files are rejected with a warning. Tokens are cached in memory for 50 minutes and
|
|
151
|
+
Only `authorized_user` ADC files are supported. Service account JSON files are rejected with a warning. Tokens are cached in memory for 50 minutes and the registered provider key is a Pi `!command`, so request-time auth resolves a fresh token when Pi sends model requests.
|
|
85
152
|
|
|
86
153
|
## LiteLLM MCP tools
|
|
87
154
|
|
|
@@ -90,11 +157,11 @@ If your LiteLLM proxy exposes MCP REST endpoints, this extension discovers tools
|
|
|
90
157
|
- `GET /mcp-rest/tools/list`
|
|
91
158
|
- `POST /mcp-rest/tools/call`
|
|
92
159
|
|
|
93
|
-
Each discovered tool is registered as a native Pi tool named `mcp_<server>_<tool>`, with simple JSON Schema parameters mapped to Pi/TypeBox parameters. Complex schemas fall back to a single `args` object.
|
|
160
|
+
Each discovered tool is registered as a native Pi tool named `mcp_<server>_<tool>`, with simple JSON Schema parameters mapped to Pi/TypeBox parameters. Complex schemas fall back to a single `args` object. MCP discovery runs after successful live model discovery, `/login litellm`, or `/litellm-refresh`; it does not force network or helper-token access when a fresh model cache is used. MCP tools run in Pi's parallel tool mode and retry transient failures once.
|
|
94
161
|
|
|
95
|
-
## LiteLLM
|
|
162
|
+
## LiteLLM Skill Hub
|
|
96
163
|
|
|
97
|
-
If your LiteLLM proxy exposes `/
|
|
164
|
+
If your LiteLLM proxy exposes `/claude-code/marketplace.json`, enabled skills are fetched before each agent turn and appended to the system prompt as a `litellm_skills` section. The extension falls back to the legacy `/v1/skills` Skills Gateway path when Skill Hub is unavailable. It also registers Pi tools for basic skill management:
|
|
98
165
|
|
|
99
166
|
- `litellm_skill_list`
|
|
100
167
|
- `litellm_skill_create`
|
|
@@ -104,7 +171,7 @@ If your LiteLLM proxy exposes `/v1/skills`, enabled skills are fetched before ea
|
|
|
104
171
|
|
|
105
172
|
The `LiteLLM Smoke` GitHub Actions workflow starts VidaiMock and a real LiteLLM proxy on the runner. LiteLLM exposes OpenAI-compatible and Anthropic routes whose upstreams are served by VidaiMock, then this extension's smoke runner discovers those models through LiteLLM and sends `/v1/chat/completions` requests through the proxy.
|
|
106
173
|
|
|
107
|
-
This keeps the LiteLLM integration path under test but does not call real LLM APIs. No provider API keys or GitHub Models permission are required. The workflow also runs a non-interactive Pi CLI smoke with `--list-models` and `-p` so extension loading, model discovery, and
|
|
174
|
+
This keeps the LiteLLM integration path under test but does not call real LLM APIs. No provider API keys or GitHub Models permission are required. The smoke runner also asserts that discovery came from `/model/info` (`LITELLM_SMOKE_EXPECT_SOURCE`) so a silent fallback to `/v1/models` fails the run. The workflow also runs auth checks plus optional Postgres-backed auth checks when `LITELLM_LICENSE` is configured for virtual-key and admin-route behavior, then runs a non-interactive Pi CLI smoke with `--list-models` and `-p` against both the OpenAI-compatible and Anthropic-backed routes, so extension loading, model discovery, and real completion paths are covered without opening the TUI.
|
|
108
175
|
|
|
109
176
|
## Development
|
|
110
177
|
|
|
@@ -135,11 +202,12 @@ Before tagging a release, keep `package.json` and `package-lock.json` versions i
|
|
|
135
202
|
|
|
136
203
|
## Slash commands
|
|
137
204
|
|
|
138
|
-
- `/litellm-refresh` — force re-fetch the model list, ignoring cache
|
|
205
|
+
- `/litellm-refresh` — force re-fetch the model list for all configured LiteLLM providers, ignoring cache
|
|
206
|
+
- `/litellm-refresh <provider>` — refresh one configured provider alias, for example `/litellm-refresh litellm-anthropic`
|
|
139
207
|
|
|
140
208
|
## Cache
|
|
141
209
|
|
|
142
|
-
The model list is cached at `~/.pi/agent/litellm-models.json` with the base URL
|
|
210
|
+
The default provider model list is cached at `~/.pi/agent/litellm-models.json` with a keyed fingerprint of the base URL + API key. Alias provider caches use `~/.pi/agent/litellm-models-<provider>.json`. Changing the base URL or key invalidates that provider's cache automatically.
|
|
143
211
|
|
|
144
212
|
If the cache is older than 24 hours, the extension refreshes it in the background on session start (non-blocking). Failures are silent — the cached models remain in use. Run `/litellm-refresh` to force an immediate refresh.
|
|
145
213
|
|
|
@@ -153,8 +221,10 @@ If the cache is older than 24 hours, the extension refreshes it in the backgroun
|
|
|
153
221
|
| Discovery times out | Increase `LITELLM_DISCOVERY_TIMEOUT_MS` or set `LITELLM_OFFLINE=1` to fall back on cached models |
|
|
154
222
|
| `401 Token expired` | Set `LITELLM_API_KEY_HELPER`. |
|
|
155
223
|
| 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 |
|
|
224
|
+
| 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 |
|
|
225
|
+
| Enterprise SSO token prompt fails with "SSO token is required" | The token field was left empty — paste the token copied from the LiteLLM UI |
|
|
156
226
|
| MCP tools not showing | Verify the proxy exposes `/mcp-rest/tools/list` and run `/litellm-refresh` after fixing the proxy |
|
|
157
|
-
| Skills not affecting prompts | Verify the proxy exposes `/v1/skills` and returns enabled skills |
|
|
227
|
+
| Skills not affecting prompts | Verify the proxy exposes `/claude-code/marketplace.json` or `/v1/skills` and returns enabled skills |
|
|
158
228
|
|
|
159
229
|
## License
|
|
160
230
|
|
package/dist/cost.d.ts
CHANGED
|
@@ -5,4 +5,8 @@ export interface ModelCostInfo {
|
|
|
5
5
|
cacheReadCostPerToken: number;
|
|
6
6
|
cacheWriteCostPerToken: number;
|
|
7
7
|
}
|
|
8
|
-
export
|
|
8
|
+
export interface ProviderModels {
|
|
9
|
+
provider: string;
|
|
10
|
+
models: ProviderModelConfig[];
|
|
11
|
+
}
|
|
12
|
+
export declare function setupLiteLLMCostTracking(pi: ExtensionAPI, providerModels: ProviderModels[]): (providerModels: ProviderModels[]) => void;
|
package/dist/cost.js
CHANGED
|
Binary file
|
package/dist/discover.d.ts
CHANGED
|
@@ -2,6 +2,11 @@ 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
4
|
export declare function isMoonshotModel(modelId: string): boolean;
|
|
5
|
+
export declare function isGpt55Model(modelId: string): boolean;
|
|
5
6
|
export declare function shouldSuppressReasoningContent(modelId: string): boolean;
|
|
6
7
|
export declare function buildCompat(modelId: string): ProviderModelConfig["compat"];
|
|
8
|
+
export declare function withTimeout(timeoutMs: number, signal?: AbortSignal): {
|
|
9
|
+
signal: AbortSignal;
|
|
10
|
+
cancel: () => void;
|
|
11
|
+
};
|
|
7
12
|
export declare function discoverModels(baseUrl: string, apiKey: string, options?: DiscoveryOptions): Promise<DiscoveryResult>;
|
package/dist/discover.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getModels, getProviders } from "@earendil-works/pi-ai";
|
|
1
|
+
import { getModels, getProviders } from "@earendil-works/pi-ai/compat";
|
|
2
2
|
const DEFAULT_TIMEOUT_MS = 5000;
|
|
3
3
|
const DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
4
4
|
const DEFAULT_MAX_TOKENS = 16_384;
|
|
@@ -8,6 +8,13 @@ let modelsDevCatalog;
|
|
|
8
8
|
export function normalizeBaseUrl(input) {
|
|
9
9
|
return input.replace(/\/+$/, "").replace(/\/v1\/?$/i, "");
|
|
10
10
|
}
|
|
11
|
+
const RESPONSES_MODE_PATTERN = /^responses?$/i;
|
|
12
|
+
function isResponsesMode(mode) {
|
|
13
|
+
return mode !== undefined && RESPONSES_MODE_PATTERN.test(mode);
|
|
14
|
+
}
|
|
15
|
+
function isChatStyleMode(mode) {
|
|
16
|
+
return mode === undefined || mode === "chat" || isResponsesMode(mode);
|
|
17
|
+
}
|
|
11
18
|
// Matches both the conventional `anthropic/...` prefix and aliases that
|
|
12
19
|
// LiteLLM deployments commonly assign to Anthropic-backed routes (e.g.
|
|
13
20
|
// `google/claude-sonnet-4-6`, `opus-4.7`, `sonnet-4.6`, `haiku-4.5`). Without
|
|
@@ -16,9 +23,16 @@ export function normalizeBaseUrl(input) {
|
|
|
16
23
|
const ANTHROPIC_MODEL_PATTERN = /(?:^|[-_/.:])(?:anthropic\/|(?:claude|opus|sonnet|haiku)(?=$|[-_/.:]))/i;
|
|
17
24
|
const MOONSHOT_MODEL_PATTERN = /^(moonshotai\/|moonshot\/|kimi[-/])/i;
|
|
18
25
|
const FORCED_THINKING_MODEL_PATTERN = /(?:^|[-/])thinking(?:[-/]|$)/i;
|
|
26
|
+
// Deployments expose the gpt-5.5 route under varying names (`llm-gateway/gpt-5.5`,
|
|
27
|
+
// bare `gpt-5.5`, dated ids like `gpt-5.5-20260504143601`); match them all so the
|
|
28
|
+
// tool+reasoning workaround survives route renames.
|
|
29
|
+
const GPT55_MODEL_PATTERN = /(?:^|\/)gpt-5\.5(?:$|[-.])/i;
|
|
19
30
|
export function isMoonshotModel(modelId) {
|
|
20
31
|
return MOONSHOT_MODEL_PATTERN.test(modelId);
|
|
21
32
|
}
|
|
33
|
+
export function isGpt55Model(modelId) {
|
|
34
|
+
return GPT55_MODEL_PATTERN.test(modelId);
|
|
35
|
+
}
|
|
22
36
|
export function shouldSuppressReasoningContent(modelId) {
|
|
23
37
|
return isMoonshotModel(modelId) && !FORCED_THINKING_MODEL_PATTERN.test(modelId);
|
|
24
38
|
}
|
|
@@ -45,22 +59,53 @@ function toKnownProvider(provider) {
|
|
|
45
59
|
}
|
|
46
60
|
function findCatalogModel(id, ownedBy) {
|
|
47
61
|
const prefixProvider = toKnownProvider(id.split("/")[0]);
|
|
48
|
-
const
|
|
62
|
+
const lookupIds = catalogLookupIds(id);
|
|
63
|
+
const candidates = [toKnownProvider(ownedBy), prefixProvider, lookupIds.length > 1 ? "anthropic" : undefined].filter((provider) => provider !== undefined);
|
|
49
64
|
for (const provider of candidates) {
|
|
50
|
-
const
|
|
51
|
-
if (
|
|
52
|
-
return
|
|
53
|
-
const providerQualified = getModels(provider).find((model) => model.id === `${provider}/${id}`);
|
|
54
|
-
if (providerQualified)
|
|
55
|
-
return providerQualified;
|
|
65
|
+
const match = findCatalogModelInProvider(provider, lookupIds);
|
|
66
|
+
if (match)
|
|
67
|
+
return match;
|
|
56
68
|
}
|
|
57
69
|
for (const provider of getProviders()) {
|
|
58
|
-
const
|
|
70
|
+
const match = findCatalogModelInProvider(provider, lookupIds);
|
|
71
|
+
if (match)
|
|
72
|
+
return match;
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
function catalogLookupIds(id) {
|
|
77
|
+
const lookupIds = new Set([id]);
|
|
78
|
+
const unprefixed = id.includes("/") ? id.slice(id.indexOf("/") + 1) : id;
|
|
79
|
+
lookupIds.add(unprefixed);
|
|
80
|
+
const anthropicAlias = unprefixed.toLowerCase().replaceAll(".", "-");
|
|
81
|
+
const match = /^(?:claude-)?(opus|sonnet|haiku)-(\d+)-(\d+)$/.exec(anthropicAlias);
|
|
82
|
+
if (match)
|
|
83
|
+
lookupIds.add(`claude-${match[1]}-${match[2]}-${match[3]}`);
|
|
84
|
+
return [...lookupIds];
|
|
85
|
+
}
|
|
86
|
+
function findCatalogModelInProvider(provider, lookupIds) {
|
|
87
|
+
for (const lookupId of lookupIds) {
|
|
88
|
+
const exact = getModels(provider).find((model) => model.id === lookupId);
|
|
59
89
|
if (exact)
|
|
60
90
|
return exact;
|
|
91
|
+
const providerQualified = getModels(provider).find((model) => model.id === `${provider}/${lookupId}`);
|
|
92
|
+
if (providerQualified)
|
|
93
|
+
return providerQualified;
|
|
61
94
|
}
|
|
62
95
|
return undefined;
|
|
63
96
|
}
|
|
97
|
+
function mapModelInfoCost(info, fallback) {
|
|
98
|
+
return {
|
|
99
|
+
input: info.input_cost_per_token !== undefined ? info.input_cost_per_token * 1_000_000 : (fallback?.input ?? 0),
|
|
100
|
+
output: info.output_cost_per_token !== undefined ? info.output_cost_per_token * 1_000_000 : (fallback?.output ?? 0),
|
|
101
|
+
cacheRead: info.cache_read_input_token_cost !== undefined
|
|
102
|
+
? info.cache_read_input_token_cost * 1_000_000
|
|
103
|
+
: (fallback?.cacheRead ?? 0),
|
|
104
|
+
cacheWrite: info.cache_creation_input_token_cost !== undefined
|
|
105
|
+
? info.cache_creation_input_token_cost * 1_000_000
|
|
106
|
+
: (fallback?.cacheWrite ?? 0),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
64
109
|
function getFallbackProviderAndModel(id, ownedBy) {
|
|
65
110
|
const [prefix, ...rest] = id.split("/");
|
|
66
111
|
const prefixProvider = toKnownProvider(prefix);
|
|
@@ -75,7 +120,7 @@ function findModelsDevModel(catalog, id, ownedBy) {
|
|
|
75
120
|
return undefined;
|
|
76
121
|
return catalog?.[provider]?.models?.[modelId];
|
|
77
122
|
}
|
|
78
|
-
function withTimeout(timeoutMs, signal) {
|
|
123
|
+
export function withTimeout(timeoutMs, signal) {
|
|
79
124
|
const controller = new AbortController();
|
|
80
125
|
const onAbort = () => controller.abort(signal?.reason);
|
|
81
126
|
if (signal) {
|
|
@@ -98,7 +143,7 @@ async function fetchJson(url, apiKey, options) {
|
|
|
98
143
|
const { signal, cancel } = withTimeout(timeoutMs, options.signal);
|
|
99
144
|
try {
|
|
100
145
|
const response = await fetch(url, {
|
|
101
|
-
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
|
|
146
|
+
headers: { ...options.headers, Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
|
|
102
147
|
signal,
|
|
103
148
|
});
|
|
104
149
|
if (!response.ok)
|
|
@@ -168,44 +213,41 @@ function mapFromModelInfo(entry) {
|
|
|
168
213
|
if (!id)
|
|
169
214
|
return undefined;
|
|
170
215
|
const info = entry.model_info ?? {};
|
|
171
|
-
if (info.mode
|
|
216
|
+
if (!isChatStyleMode(info.mode))
|
|
172
217
|
return undefined;
|
|
218
|
+
const responsesMode = isResponsesMode(info.mode);
|
|
219
|
+
const catalogModel = findCatalogModel(id);
|
|
173
220
|
return {
|
|
174
221
|
id,
|
|
175
222
|
name: id,
|
|
176
223
|
reasoning: info.supports_reasoning ?? false,
|
|
177
224
|
input: info.supports_vision ? ["text", "image"] : ["text"],
|
|
178
|
-
cost:
|
|
179
|
-
input: (info.input_cost_per_token ?? 0) * 1_000_000,
|
|
180
|
-
output: (info.output_cost_per_token ?? 0) * 1_000_000,
|
|
181
|
-
cacheRead: (info.cache_read_input_token_cost ?? 0) * 1_000_000,
|
|
182
|
-
cacheWrite: (info.cache_creation_input_token_cost ?? 0) * 1_000_000,
|
|
183
|
-
},
|
|
225
|
+
cost: mapModelInfoCost(info, catalogModel?.cost),
|
|
184
226
|
contextWindow: info.max_input_tokens ?? DEFAULT_CONTEXT_WINDOW,
|
|
185
227
|
maxTokens: info.max_output_tokens ?? DEFAULT_MAX_TOKENS,
|
|
186
228
|
compat: buildCompat(id),
|
|
229
|
+
...(responsesMode ? { api: "openai-responses" } : {}),
|
|
187
230
|
};
|
|
188
231
|
}
|
|
189
232
|
function mapFromHealthModelInfo(entry, fallbackId) {
|
|
190
233
|
const model = mapFromModelInfo(entry);
|
|
191
234
|
if (model || !fallbackId)
|
|
192
235
|
return model;
|
|
193
|
-
if (entry.model_info?.mode
|
|
236
|
+
if (!isChatStyleMode(entry.model_info?.mode))
|
|
194
237
|
return undefined;
|
|
238
|
+
const info = entry.model_info ?? {};
|
|
239
|
+
const responsesMode = isResponsesMode(info.mode);
|
|
240
|
+
const catalogModel = findCatalogModel(fallbackId);
|
|
195
241
|
return {
|
|
196
242
|
id: fallbackId,
|
|
197
243
|
name: fallbackId,
|
|
198
|
-
reasoning:
|
|
199
|
-
input:
|
|
200
|
-
cost:
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
cacheRead: (entry.model_info?.cache_read_input_token_cost ?? 0) * 1_000_000,
|
|
204
|
-
cacheWrite: (entry.model_info?.cache_creation_input_token_cost ?? 0) * 1_000_000,
|
|
205
|
-
},
|
|
206
|
-
contextWindow: entry.model_info?.max_input_tokens ?? DEFAULT_CONTEXT_WINDOW,
|
|
207
|
-
maxTokens: entry.model_info?.max_output_tokens ?? DEFAULT_MAX_TOKENS,
|
|
244
|
+
reasoning: info.supports_reasoning ?? false,
|
|
245
|
+
input: info.supports_vision ? ["text", "image"] : ["text"],
|
|
246
|
+
cost: mapModelInfoCost(info, catalogModel?.cost),
|
|
247
|
+
contextWindow: info.max_input_tokens ?? DEFAULT_CONTEXT_WINDOW,
|
|
248
|
+
maxTokens: info.max_output_tokens ?? DEFAULT_MAX_TOKENS,
|
|
208
249
|
compat: buildCompat(fallbackId),
|
|
250
|
+
...(responsesMode ? { api: "openai-responses" } : {}),
|
|
209
251
|
};
|
|
210
252
|
}
|
|
211
253
|
function mapFromHealthEndpoint(entry) {
|