@staticeng/opencode-litellm 0.3.2 → 0.3.4
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 +65 -36
- package/dist/discovery.d.ts +5 -0
- package/dist/discovery.js +33 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/litellm.d.ts +2 -2
- package/dist/litellm.js +18 -8
- package/dist/mapping.d.ts +3 -2
- package/dist/mapping.js +30 -13
- package/dist/model-contracts.js +14 -1
- package/dist/options.d.ts +1 -1
- package/dist/options.js +2 -0
- package/dist/types.d.ts +6 -0
- package/dist/v2.d.ts +1 -1
- package/dist/v2.js +79 -47
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,20 +4,23 @@ Runtime LiteLLM model autodiscovery for OpenCode. The plugin fetches LiteLLM dep
|
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
Install the package by npm name, then add it to OpenCode `plugin` config:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @staticeng/opencode-litellm
|
|
11
|
+
```
|
|
9
12
|
|
|
10
13
|
```json
|
|
11
14
|
{
|
|
12
15
|
"$schema": "https://opencode.ai/config.json",
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
|
|
16
|
+
"plugin": [
|
|
17
|
+
[
|
|
18
|
+
"@staticeng/opencode-litellm",
|
|
19
|
+
{
|
|
17
20
|
"baseURL": "https://litellm.example.com/v1",
|
|
18
21
|
"apiKeyEnv": "LITELLM_API_KEY"
|
|
19
22
|
}
|
|
20
|
-
|
|
23
|
+
]
|
|
21
24
|
]
|
|
22
25
|
}
|
|
23
26
|
```
|
|
@@ -32,15 +35,27 @@ Do not put real API keys in `opencode.json`. If you pass `apiKey` directly, the
|
|
|
32
35
|
|
|
33
36
|
Shared or synchronized OpenCode configuration should use the published package name as shown above. Do not use a local path or `file://` plugin reference for shared configuration.
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
package
|
|
38
|
-
|
|
38
|
+
### OpenCode V2
|
|
39
|
+
|
|
40
|
+
OpenCode V2 loads plugins from `plugins` (plural). Use the package name as the entrypoint; its default export supports the V2 loader. The `./v2` export is also available for direct imports. The current API is tested against OpenCode V2 2.0.16 and `@opencode/plugin` 2.0.16; the transitional `@opencode-ai/plugin` catalog API remains supported. The dual V1/V2 default entrypoint supports V1 1.18.29 and newer; older V1 releases that require a default-exported function need an earlier package version.
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"plugins": [
|
|
45
|
+
{
|
|
46
|
+
"package": "@staticeng/opencode-litellm",
|
|
47
|
+
"options": {
|
|
48
|
+
"baseURL": "https://litellm.example.com/v1",
|
|
49
|
+
"apiKeyEnv": "LITELLM_API_KEY"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
V2 adds the provider and models through in-memory transforms. It uses the same options and environment variables as V1.
|
|
39
57
|
|
|
40
|
-
|
|
41
|
-
process, then use `/models` to select a model under `LiteLLM`. For synchronized
|
|
42
|
-
setups, edit the authoritative configuration once and wait for Syncthing to
|
|
43
|
-
deliver it to each peer. Each host installs its own npm package cache.
|
|
58
|
+
Current V2 uses the drivers bundled in the OpenCode executable: `@opencode/ai/providers/openai-compatible` for Chat Completions and `@opencode/ai/providers/openai/responses` for Responses. GPT names and LiteLLM `mode: responses` metadata select Responses, including opaque aliases. Models marked `use_responses_lite: true` receive `reasoning.context: all_turns`. Explicit model overrides still win. OpenCode 2.0.16 does not bundle the SDK's `openai-compatible/responses` entrypoint; the plugin normalizes that override to the bundled Responses driver as well.
|
|
44
59
|
|
|
45
60
|
## Options
|
|
46
61
|
|
|
@@ -49,7 +64,7 @@ deliver it to each peer. Each host installs its own npm package cache.
|
|
|
49
64
|
| `baseURL` | `LITELLM_BASE_URL` | LiteLLM proxy base URL. `/v1` URLs are supported. |
|
|
50
65
|
| `apiKey` | env value | Bearer token for LiteLLM. |
|
|
51
66
|
| `apiKeyEnv` | `LITELLM_API_KEY` | Environment variable used when `apiKey` is not set. |
|
|
52
|
-
| `providerKey` | `LiteLLM` | Provider ID
|
|
67
|
+
| `providerKey` | `LiteLLM` | Provider ID in the V1 runtime config or V2 catalog. |
|
|
53
68
|
| `providerName` | `LiteLLM` | Human provider name. |
|
|
54
69
|
| `overrides` | `{}` | Per-model deep-merge overrides. |
|
|
55
70
|
| `providerOverrides` | `{}` | Provider-level deep-merge overrides. |
|
|
@@ -58,40 +73,49 @@ deliver it to each peer. Each host installs its own npm package cache.
|
|
|
58
73
|
| `includeBackends` | unset | Optional exact allowlist by `model_info.backend`, unioned with `includeModels` pins. |
|
|
59
74
|
| `excludeModels` | none | Optional denylist by model name. |
|
|
60
75
|
| `timeoutMs` | `30000` | Per-request discovery timeout. |
|
|
76
|
+
| `startupRetryMs` | `120000` | Startup grace period for transient DNS/network, timeout and HTTP 408/429/5xx failures. Uses exponential backoff up to 10 seconds. Set `0` to make one attempt. |
|
|
77
|
+
| `recoveryIntervalMs` | `30000` | Current V2 makes one background discovery attempt per interval after startup grace expires, then reloads the provider catalog when it recovers. Retries until success or plugin unload. Set `0` to disable. |
|
|
61
78
|
| `strict` | `false` | Throw on fetch/mapping failures instead of warning. |
|
|
62
79
|
|
|
63
80
|
The LiteLLM key must have permission to call `GET /model/info` and root `GET /model_group/info`.
|
|
64
81
|
|
|
82
|
+
### Starting before the network is ready
|
|
83
|
+
|
|
84
|
+
At boot, a user service can start before DNS or the LiteLLM endpoint is reachable,
|
|
85
|
+
even with `After=network-online.target`. Transient failures now retry during the
|
|
86
|
+
startup grace period. In current OpenCode V2, non-strict mode also recovers after
|
|
87
|
+
that period without restarting OpenCode or OpenChamber: it registers the provider
|
|
88
|
+
and reloads the live catalog once discovery succeeds. Recovery timers and in-flight
|
|
89
|
+
requests stop when the plugin unloads. No generated model data is written to your
|
|
90
|
+
configuration. Authentication errors and invalid metadata fail immediately.
|
|
91
|
+
|
|
92
|
+
Strict mode throws if discovery still fails after startup grace. Legacy and
|
|
93
|
+
transitional APIs use startup retries; recovery after that grace period requires
|
|
94
|
+
the current V2 provider reload API.
|
|
95
|
+
|
|
65
96
|
When `includeBackends` is present, models are candidates when `model_info.backend` exactly and case-sensitively matches an entry or their exact model name is pinned by `includeModels`. Backend values are not trimmed, normalized, or partially matched; missing and non-string values do not match. An empty `includeBackends` matches no backends, so without model pins it includes no models. `excludeModels` always wins over backend matches and pins, and candidates must still pass `includeModes` (or the default `chat` and `responses` modes). When `includeBackends` is omitted, `includeModels` retains its historical behavior: a non-empty array is a model-name allowlist, while an omitted or empty array imposes no model-name allowlist.
|
|
66
97
|
|
|
67
98
|
## Overrides
|
|
68
99
|
|
|
69
100
|
LiteLLM metadata does not expose every OpenCode model field. The plugin includes exact built-in contracts for the currently approved GPT-5.4, GPT-5.5, GPT-5.6, GPT-6 Luna and Sol, DeepSeek V4 Flash, and Qwen3.8 aliases. Use `overrides` to customize those defaults or to add metadata for unknown models.
|
|
70
101
|
|
|
71
|
-
When LiteLLM publishes `model_info.supported_reasoning_efforts`, the plugin maps that list to OpenCode variants automatically. Known contracts contribute their approved defaults; discovered extra efforts remain available unless that exact contract explicitly closes discovery.
|
|
102
|
+
When LiteLLM publishes `model_info.supported_reasoning_efforts`, the plugin maps that list to OpenCode variants automatically. Known contracts contribute their approved defaults; discovered extra efforts remain available unless that exact contract explicitly closes discovery. `qwen3.8-flash-next` and `gpt-6.1-sol` close discovery so unsupported efforts cannot become active. No substring, suffix, provider namespace, or base-model inference is used.
|
|
72
103
|
|
|
73
104
|
```json
|
|
74
105
|
{
|
|
75
|
-
"
|
|
76
|
-
|
|
77
|
-
"
|
|
78
|
-
|
|
106
|
+
"plugin": [
|
|
107
|
+
[
|
|
108
|
+
"@staticeng/opencode-litellm",
|
|
109
|
+
{
|
|
79
110
|
"baseURL": "https://litellm.example.com/v1",
|
|
80
111
|
"apiKeyEnv": "LITELLM_API_KEY",
|
|
81
112
|
"overrides": {
|
|
82
113
|
"chatgpt/gpt-5.5": {
|
|
83
|
-
"limit": {
|
|
84
|
-
"context": 262144,
|
|
85
|
-
"output": 128000
|
|
86
|
-
},
|
|
114
|
+
"limit": { "context": 262144, "output": 128000 },
|
|
87
115
|
"variants": {
|
|
88
|
-
"high": {
|
|
89
|
-
"reasoningEffort": "high"
|
|
90
|
-
}
|
|
116
|
+
"high": { "reasoningEffort": "high" }
|
|
91
117
|
},
|
|
92
|
-
"interleaved": {
|
|
93
|
-
"field": "reasoning_details"
|
|
94
|
-
}
|
|
118
|
+
"interleaved": { "field": "reasoning_details" }
|
|
95
119
|
}
|
|
96
120
|
},
|
|
97
121
|
"providerOverrides": {
|
|
@@ -100,7 +124,7 @@ When LiteLLM publishes `model_info.supported_reasoning_efforts`, the plugin maps
|
|
|
100
124
|
}
|
|
101
125
|
}
|
|
102
126
|
}
|
|
103
|
-
|
|
127
|
+
]
|
|
104
128
|
]
|
|
105
129
|
}
|
|
106
130
|
```
|
|
@@ -140,25 +164,28 @@ The built-in mode matrix is:
|
|
|
140
164
|
| GPT-5.5 | `none`, `low`, `medium`, `high`, `xhigh` | `medium` |
|
|
141
165
|
| GPT-5.6 Luna / Sol / Terra | `none`, `low`, `medium`, `high`, `xhigh`, `max` | `medium` |
|
|
142
166
|
| DeepSeek V4 Flash | `off`, `low`, `high`, `max` | `max` |
|
|
167
|
+
| DeepSeek V4.1 Flash 1017 | `off`, `low`, `high`, `max` | `high` |
|
|
143
168
|
| Qwen3.8 27B refusal dial | `off`, `low`, `medium`, `xhigh` | `xhigh` |
|
|
144
169
|
| Qwen3.8 Flash Next | `off`, `low`, `medium`, `xhigh` | `xhigh` |
|
|
145
170
|
|
|
146
|
-
DeepSeek `off` sends canonical `none`. Qwen `off` sends only `chat_template_kwargs.enable_thinking=false`, with no reasoning effort. `ultra` is not generated by a built-in contract. Flash Next activates exactly `off`, `low`, `medium`, and `xhigh`: discovered extras such as `ultra` are suppressed, V2 omits `minimal`, `high`, and `max`, and the legacy OpenCode surface disables unsupported generic choices.
|
|
171
|
+
DeepSeek `off` sends canonical `none`. Qwen `off` sends only `chat_template_kwargs.enable_thinking=false`, with no reasoning effort. `ultra` is not generated by a built-in contract. Flash Next activates exactly `off`, `low`, `medium`, and `xhigh`: discovered extras such as `ultra` are suppressed, V2 omits `minimal`, `high`, and `max`, and the legacy OpenCode surface disables unsupported generic choices. V1 supplies the contract default through model options and removes the default effort in its `chat.params` hook for Qwen `off`. Transitional V2 selects the default with `model.request.variant`. Current V2 applies the selected default variant's body and headers to outbound requests when no explicit variant is selected; an explicit `off` variant therefore has no inherited default effort. A request-level reasoning effort takes precedence. The `variantDefault` compatibility marker applies only to V1 and is stripped before requests for exact built-in aliases.
|
|
147
172
|
|
|
148
173
|
The exact dynamically discovered alias `qwen3.8-flash-next` also receives verified static metadata that overrides incomplete or conflicting LiteLLM group metadata: text and image input, text output, attachment, reasoning, tool calling, and a 262144-token context. Its output limit remains unknown and is represented by the package's `0` sentinel; pricing and other modalities are not supplied. The contract is still discovery-gated and never creates the alias when LiteLLM omits it.
|
|
149
174
|
|
|
175
|
+
The exact alias `deepseek-v4.1-flash-1017` receives verified static metadata from the official model card and deployed LiteLLM metadata: text and image input, text output, tool calling, a 1048576-token context, and a 262144-token output limit. Pricing remains discovery-owned. The V4.1 encoder documents `chat` and `thinking` modes with `low`, `high`, and `max` effort aliases; the plugin exposes `off` as the LiteLLM/vLLM `reasoning_effort=none` compatibility mode. Only `off`, `low`, `high`, and `max` are active, defaulting to `high`; extra discovery efforts are suppressed. The contract remains discovery-gated.
|
|
176
|
+
|
|
150
177
|
Normal GPT-5.3 Codex and GPT-5.3 Codex Spark aliases are retired and filtered even if stale discovery metadata still reports them. Unlisted aliases such as `defend/gpt-5.5` do not inherit a contract.
|
|
151
178
|
|
|
152
179
|
## Runtime Behavior
|
|
153
180
|
|
|
154
181
|
- Fetches `${baseURL}/model/info`, then root `/model_group/info`, with separate authenticated and timed requests.
|
|
155
182
|
- Preserves `/v1` for model-info discovery but resolves model-group discovery from the LiteLLM origin.
|
|
156
|
-
-
|
|
183
|
+
- V1 updates `cfg.provider[providerKey]` and the migration-era `cfg.providers[providerKey]` in memory; transitional V2 registers a catalog transform, while current V2 registers a provider transform and scoped session hooks.
|
|
157
184
|
- Keeps namespaced deployment IDs and augments them with exact bare model-group IDs; exact collisions are enriched deterministically.
|
|
158
185
|
- Skips malformed group rows without a nonblank `model_group` and exposes reasoning capability on legacy and V2 model metadata.
|
|
159
186
|
- Applies exact built-in model contracts as defaults, then applies explicit model and provider overrides last.
|
|
160
187
|
- Filters retired normal and Spark GPT-5.3 Codex aliases without redirecting them.
|
|
161
|
-
- Warns safely and keeps deployment models when group discovery fails in non-strict mode; strict mode
|
|
188
|
+
- Warns safely and keeps deployment models when group discovery fails in non-strict mode; strict mode skips registration or config mutation on failure.
|
|
162
189
|
- Does not write `opencode.json`, cache model metadata, or persist generated provider data.
|
|
163
190
|
- Emits the required `limit.output` as `0` when LiteLLM reports a context/input limit but no output
|
|
164
191
|
limit. The zero sentinel retains known metadata without inventing a positive output allowance.
|
|
@@ -176,9 +203,11 @@ npm test
|
|
|
176
203
|
npm pack --dry-run
|
|
177
204
|
```
|
|
178
205
|
|
|
206
|
+
With the OpenCode 2.0.16 executable installed, run `OPENCODE_V2_CLI_TEST=1 node --test test/v2-cli.test.mjs` after building. Set `OPENCODE_V2_CLI` to override the executable path. This isolated test uses mock HTTP endpoints and temporary configuration to check the binary's provider loading, Responses aliases, and Qwen reasoning modes without real credentials.
|
|
207
|
+
|
|
179
208
|
Confirm package contents before publishing and keep secrets out of fixtures, docs, logs, and config examples.
|
|
180
209
|
|
|
181
|
-
|
|
210
|
+
Publishing uses npm trusted publishing from the `npm-production` GitHub environment. Provenance
|
|
182
211
|
requires this GitHub repository to remain public. Before a release, configure npm's trusted
|
|
183
212
|
publisher for package `@staticeng/opencode-litellm` with this repository, workflow
|
|
184
213
|
`publish.yml`, and environment `npm-production`; no npm token is used by the workflow.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ResolvedOptions } from "./options.js";
|
|
2
|
+
export declare function isTransient(error: unknown): boolean;
|
|
3
|
+
export declare function waitForRetry(ms: number, signal: AbortSignal): Promise<void>;
|
|
4
|
+
/** Retry only transient failures; the grace deadline includes requests and backoff. */
|
|
5
|
+
export declare function discoverWithRetry<T>(load: (options: ResolvedOptions, signal: AbortSignal) => Promise<T>, options: ResolvedOptions, signal?: AbortSignal): Promise<T>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function isTransient(error) {
|
|
2
|
+
return error instanceof Error && "retryable" in error && error.retryable === true;
|
|
3
|
+
}
|
|
4
|
+
export function waitForRetry(ms, signal) {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
if (signal.aborted)
|
|
7
|
+
return reject(new Error("LiteLLM discovery cancelled"));
|
|
8
|
+
const abort = () => { clearTimeout(timer); reject(new Error("LiteLLM discovery cancelled")); };
|
|
9
|
+
const timer = setTimeout(() => { signal.removeEventListener("abort", abort); resolve(); }, ms);
|
|
10
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
/** Retry only transient failures; the grace deadline includes requests and backoff. */
|
|
14
|
+
export async function discoverWithRetry(load, options, signal = new AbortController().signal) {
|
|
15
|
+
const grace = Number.isFinite(options.startupRetryMs) ? Math.max(0, options.startupRetryMs) : 120_000;
|
|
16
|
+
const deadline = Date.now() + grace;
|
|
17
|
+
let delay = 1000;
|
|
18
|
+
for (;;) {
|
|
19
|
+
if (signal.aborted)
|
|
20
|
+
throw new Error("LiteLLM discovery cancelled");
|
|
21
|
+
try {
|
|
22
|
+
const remaining = deadline - Date.now();
|
|
23
|
+
return await load({ ...options, timeoutMs: grace > 0 ? Math.max(1, Math.min(options.timeoutMs, remaining)) : options.timeoutMs }, signal);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
const remaining = deadline - Date.now();
|
|
27
|
+
if (!isTransient(error) || signal.aborted || remaining <= 0)
|
|
28
|
+
throw error;
|
|
29
|
+
await waitForRetry(Math.min(delay, remaining), signal);
|
|
30
|
+
delay = Math.min(delay * 2, 10_000);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -34,6 +34,6 @@ declare const _default: {
|
|
|
34
34
|
options: Record<string, any>;
|
|
35
35
|
}) => Promise<void>;
|
|
36
36
|
}>;
|
|
37
|
-
setup: (ctx: import("@opencode/plugin/promise/plugin").Context) => Promise<void>;
|
|
37
|
+
setup: (ctx: import("@opencode/plugin/promise/plugin").Context) => Promise<() => void>;
|
|
38
38
|
};
|
|
39
39
|
export default _default;
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { fetchModelGroupInfo, fetchModelInfo } from "./litellm.js";
|
|
|
2
2
|
import { buildProvider, buildProviderV2, isContractReasoningModel, isQwen38ReasoningModel, mergeModelInfo, } from "./mapping.js";
|
|
3
3
|
import { resolveOptions } from "./options.js";
|
|
4
4
|
import V2Plugin from "./v2.js";
|
|
5
|
+
import { discoverWithRetry } from "./discovery.js";
|
|
5
6
|
async function log(ctx, level, message, extra = {}) {
|
|
6
7
|
await ctx.client?.app?.log?.({ body: { service: "opencode-litellm", level, message, extra } });
|
|
7
8
|
}
|
|
@@ -10,7 +11,7 @@ export const LiteLLMPlugin = async (ctx, options) => {
|
|
|
10
11
|
return {
|
|
11
12
|
config: async (cfg) => {
|
|
12
13
|
try {
|
|
13
|
-
const deployments = await fetchModelInfo(resolved);
|
|
14
|
+
const deployments = await discoverWithRetry((attempt, signal) => fetchModelInfo(attempt, fetch, signal), resolved);
|
|
14
15
|
let groups = [];
|
|
15
16
|
try {
|
|
16
17
|
groups = await fetchModelGroupInfo(resolved);
|
package/dist/litellm.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ResolvedOptions } from "./options.js";
|
|
2
2
|
import type { LiteLLMModelInfo } from "./types.js";
|
|
3
|
-
export declare function fetchModelInfo(options: ResolvedOptions, fetchImpl?: typeof fetch): Promise<LiteLLMModelInfo[]>;
|
|
3
|
+
export declare function fetchModelInfo(options: ResolvedOptions, fetchImpl?: typeof fetch, signal?: AbortSignal): Promise<LiteLLMModelInfo[]>;
|
|
4
4
|
export declare function normalizeModelGroupResponse(response: unknown): LiteLLMModelInfo[];
|
|
5
|
-
export declare function fetchModelGroupInfo(options: ResolvedOptions, fetchImpl?: typeof fetch): Promise<LiteLLMModelInfo[]>;
|
|
5
|
+
export declare function fetchModelGroupInfo(options: ResolvedOptions, fetchImpl?: typeof fetch, signal?: AbortSignal): Promise<LiteLLMModelInfo[]>;
|
package/dist/litellm.js
CHANGED
|
@@ -6,8 +6,12 @@ function requireCredentials(options) {
|
|
|
6
6
|
if (!options.apiKey)
|
|
7
7
|
throw new Error("opencode-litellm requires options.apiKey or the configured apiKeyEnv");
|
|
8
8
|
}
|
|
9
|
-
async function fetchJSON(options, endpoint, url, fetchImpl) {
|
|
9
|
+
async function fetchJSON(options, endpoint, url, fetchImpl, signal) {
|
|
10
10
|
const controller = new AbortController();
|
|
11
|
+
const abort = () => controller.abort();
|
|
12
|
+
if (signal?.aborted)
|
|
13
|
+
abort();
|
|
14
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
11
15
|
const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
12
16
|
try {
|
|
13
17
|
try {
|
|
@@ -20,24 +24,30 @@ async function fetchJSON(options, endpoint, url, fetchImpl) {
|
|
|
20
24
|
signal: controller.signal,
|
|
21
25
|
});
|
|
22
26
|
if (!response.ok)
|
|
23
|
-
throw new Error(`LiteLLM ${endpoint} returned HTTP ${response.status}`)
|
|
27
|
+
throw Object.assign(new Error(`LiteLLM ${endpoint} returned HTTP ${response.status}`), {
|
|
28
|
+
retryable: response.status === 408 || response.status === 429 || response.status >= 500,
|
|
29
|
+
});
|
|
24
30
|
return await response.json();
|
|
25
31
|
}
|
|
26
32
|
catch (error) {
|
|
27
33
|
if (error instanceof Error && error.message.startsWith(`LiteLLM ${endpoint}`))
|
|
28
34
|
throw error;
|
|
29
35
|
if (controller.signal.aborted)
|
|
30
|
-
throw new Error(`LiteLLM ${endpoint} timed out after ${options.timeoutMs}ms`);
|
|
31
|
-
throw new Error(`LiteLLM ${endpoint} request failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
36
|
+
throw Object.assign(new Error(`LiteLLM ${endpoint} timed out after ${options.timeoutMs}ms`), { retryable: !signal?.aborted });
|
|
37
|
+
throw Object.assign(new Error(`LiteLLM ${endpoint} request failed: ${error instanceof Error ? error.message : String(error)}`), {
|
|
38
|
+
// A JSON/parser failure is a catalog error, not a network outage.
|
|
39
|
+
retryable: !(error instanceof SyntaxError),
|
|
40
|
+
});
|
|
32
41
|
}
|
|
33
42
|
}
|
|
34
43
|
finally {
|
|
35
44
|
clearTimeout(timeout);
|
|
45
|
+
signal?.removeEventListener("abort", abort);
|
|
36
46
|
}
|
|
37
47
|
}
|
|
38
|
-
export async function fetchModelInfo(options, fetchImpl = fetch) {
|
|
48
|
+
export async function fetchModelInfo(options, fetchImpl = fetch, signal) {
|
|
39
49
|
requireCredentials(options);
|
|
40
|
-
return normalizeModelListResponse(await fetchJSON(options, "/model/info", modelInfoURL(options.baseURL), fetchImpl));
|
|
50
|
+
return normalizeModelListResponse(await fetchJSON(options, "/model/info", modelInfoURL(options.baseURL), fetchImpl, signal));
|
|
41
51
|
}
|
|
42
52
|
export function normalizeModelGroupResponse(response) {
|
|
43
53
|
const data = typeof response === "object" && response !== null && !Array.isArray(response)
|
|
@@ -63,7 +73,7 @@ export function normalizeModelGroupResponse(response) {
|
|
|
63
73
|
},
|
|
64
74
|
}));
|
|
65
75
|
}
|
|
66
|
-
export async function fetchModelGroupInfo(options, fetchImpl = fetch) {
|
|
76
|
+
export async function fetchModelGroupInfo(options, fetchImpl = fetch, signal) {
|
|
67
77
|
requireCredentials(options);
|
|
68
|
-
return normalizeModelGroupResponse(await fetchJSON(options, "/model_group/info", modelGroupInfoURL(options.baseURL), fetchImpl));
|
|
78
|
+
return normalizeModelGroupResponse(await fetchJSON(options, "/model_group/info", modelGroupInfoURL(options.baseURL), fetchImpl, signal));
|
|
69
79
|
}
|
package/dist/mapping.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export declare function mapModels(response: unknown, options?: LiteLLMPluginOpti
|
|
|
18
18
|
[k: string]: OpenCodeModel;
|
|
19
19
|
};
|
|
20
20
|
export declare function buildProvider(models: unknown, options?: LiteLLMPluginOptions): OpenCodeProvider;
|
|
21
|
-
export declare function mapModelsV2(models: unknown, options?: LiteLLMPluginOptions): {
|
|
21
|
+
export declare function mapModelsV2(models: unknown, options?: LiteLLMPluginOptions, nativeImages?: boolean): {
|
|
22
22
|
[k: string]: OpenCodeModelV2 | {
|
|
23
23
|
variants: {
|
|
24
24
|
id: string;
|
|
@@ -26,6 +26,7 @@ export declare function mapModelsV2(models: unknown, options?: LiteLLMPluginOpti
|
|
|
26
26
|
[x: string]: unknown;
|
|
27
27
|
};
|
|
28
28
|
}[];
|
|
29
|
+
type?: "image";
|
|
29
30
|
family?: string;
|
|
30
31
|
name?: string;
|
|
31
32
|
api?: {
|
|
@@ -63,4 +64,4 @@ export declare function mapModelsV2(models: unknown, options?: LiteLLMPluginOpti
|
|
|
63
64
|
};
|
|
64
65
|
};
|
|
65
66
|
};
|
|
66
|
-
export declare function buildProviderV2(models: unknown, options?: LiteLLMPluginOptions): OpenCodeProviderV2;
|
|
67
|
+
export declare function buildProviderV2(models: unknown, options?: LiteLLMPluginOptions, nativeImages?: boolean): OpenCodeProviderV2;
|
package/dist/mapping.js
CHANGED
|
@@ -167,7 +167,7 @@ export function normalizeModelListResponse(response) {
|
|
|
167
167
|
function mode(model) {
|
|
168
168
|
return asString(model.model_info?.mode) ?? "chat";
|
|
169
169
|
}
|
|
170
|
-
function allowedByFilters(model, options) {
|
|
170
|
+
function allowedByFilters(model, options, nativeImages = false) {
|
|
171
171
|
const name = modelName(model);
|
|
172
172
|
if (!name)
|
|
173
173
|
return false;
|
|
@@ -186,8 +186,13 @@ function allowedByFilters(model, options) {
|
|
|
186
186
|
if (options.excludeModels?.includes(name))
|
|
187
187
|
return false;
|
|
188
188
|
const modelMode = mode(model);
|
|
189
|
+
// Images require the typed catalog and media resolver introduced in current V2.
|
|
190
|
+
if (modelMode === "image_generation" && !nativeImages)
|
|
191
|
+
return false;
|
|
189
192
|
if (options.includeModes?.length)
|
|
190
193
|
return options.includeModes.includes(modelMode);
|
|
194
|
+
if (modelMode === "image_generation")
|
|
195
|
+
return nativeImages;
|
|
191
196
|
return DEFAULT_MODES.has(modelMode) && !NON_CHAT_MODES.has(modelMode);
|
|
192
197
|
}
|
|
193
198
|
function cost(model) {
|
|
@@ -283,11 +288,13 @@ function contractDefaults(name, tombstones = false) {
|
|
|
283
288
|
throw new Error(`Model contract ${entry.family} has no default mode body`);
|
|
284
289
|
const variants = Object.fromEntries(entry.modes.map((mode) => [mode.id, { ...mode.legacy }]));
|
|
285
290
|
if (tombstones) {
|
|
286
|
-
const disabled = entry.family === "DeepSeek V4 Flash"
|
|
287
|
-
?
|
|
288
|
-
: entry.family === "
|
|
289
|
-
? ["
|
|
290
|
-
:
|
|
291
|
+
const disabled = entry.family === "DeepSeek V4.1 Flash"
|
|
292
|
+
? REASONING_EFFORTS.filter((effort) => !entry.modes.some((mode) => mode.id === effort))
|
|
293
|
+
: entry.family === "DeepSeek V4 Flash"
|
|
294
|
+
? ["medium", "xhigh"]
|
|
295
|
+
: entry.family === "Qwen3.8"
|
|
296
|
+
? ["none", "minimal", "low", "medium", "high", "xhigh", "max", ...(entry.metadata ? ["ultra"] : [])].filter((effort) => !entry.modes.some((mode) => mode.id === effort))
|
|
297
|
+
: [];
|
|
291
298
|
for (const effort of disabled)
|
|
292
299
|
variants[effort] = { disabled: true };
|
|
293
300
|
}
|
|
@@ -330,6 +337,15 @@ function generatedModel(model, tombstones = false) {
|
|
|
330
337
|
const name = modelName(model);
|
|
331
338
|
if (!name)
|
|
332
339
|
return undefined;
|
|
340
|
+
if (mode(model) === "image_generation")
|
|
341
|
+
return {
|
|
342
|
+
id: name,
|
|
343
|
+
name,
|
|
344
|
+
type: "image",
|
|
345
|
+
provider: { npm: "@opencode/ai/providers/openai-compatible/images" },
|
|
346
|
+
modalities: { input: supportedModalities(["text", ...(model.model_info?.supported_modalities ?? [])]), output: ["image"] },
|
|
347
|
+
tool_call: false,
|
|
348
|
+
};
|
|
333
349
|
const mappedModalities = modalities(model);
|
|
334
350
|
const discoveredVariants = supportsReasoning(model) ? reasoningVariants(model) : undefined;
|
|
335
351
|
const suppressDiscoveredVariants = modelContract(name)?.suppressDiscoveredVariants === true;
|
|
@@ -403,6 +419,7 @@ function v2Model(model, baseURL) {
|
|
|
403
419
|
const name = asString(model.name);
|
|
404
420
|
const family = asString(model.family);
|
|
405
421
|
return {
|
|
422
|
+
...(model.type === "image" ? { type: "image" } : {}),
|
|
406
423
|
...(family ? { family } : {}),
|
|
407
424
|
...(name ? { name } : {}),
|
|
408
425
|
api: {
|
|
@@ -425,10 +442,10 @@ function v2Model(model, baseURL) {
|
|
|
425
442
|
...(v2Variants(model) ? { variants: v2Variants(model) } : {}),
|
|
426
443
|
};
|
|
427
444
|
}
|
|
428
|
-
function mapModelsInternal(response, options, tombstones) {
|
|
445
|
+
function mapModelsInternal(response, options, tombstones, nativeImages = false) {
|
|
429
446
|
const models = normalizeModelListResponse(response);
|
|
430
447
|
return Object.fromEntries(models
|
|
431
|
-
.filter((model) => allowedByFilters(model, options))
|
|
448
|
+
.filter((model) => allowedByFilters(model, options, nativeImages))
|
|
432
449
|
.map((model) => {
|
|
433
450
|
const name = modelName(model);
|
|
434
451
|
const generated = generatedModel(model, tombstones);
|
|
@@ -458,8 +475,8 @@ export function buildProvider(models, options = {}) {
|
|
|
458
475
|
models: mapModelsInternal(models, options, true),
|
|
459
476
|
}, options.providerOverrides);
|
|
460
477
|
}
|
|
461
|
-
export function mapModelsV2(models, options = {}) {
|
|
462
|
-
const legacy =
|
|
478
|
+
export function mapModelsV2(models, options = {}, nativeImages = false) {
|
|
479
|
+
const legacy = mapModelsInternal(models, options, false, nativeImages);
|
|
463
480
|
return Object.fromEntries(Object.entries(legacy).map(([name, model]) => {
|
|
464
481
|
const mapped = v2Model(model, options.baseURL);
|
|
465
482
|
const variants = contractV2Variants(name, model, options.overrides?.[name]);
|
|
@@ -471,7 +488,7 @@ export function mapModelsV2(models, options = {}) {
|
|
|
471
488
|
const overridesEffort = isRecord(overriddenOptions) && ("reasoningEffort" in overriddenOptions ||
|
|
472
489
|
"reasoning_effort" in overriddenOptions ||
|
|
473
490
|
"chat_template_kwargs" in overriddenOptions);
|
|
474
|
-
// V2 applies the selected variant after request.body, so Qwen off
|
|
491
|
+
// V2 applies the selected variant after request.body, so the Qwen off mode cannot inherit a default effort.
|
|
475
492
|
if (contract && mapped.api?.package !== "@ai-sdk/openai" && !overridesEffort) {
|
|
476
493
|
delete defaultBody.reasoning_effort;
|
|
477
494
|
delete defaultBody.reasoningEffort;
|
|
@@ -504,7 +521,7 @@ function responsesBody(options) {
|
|
|
504
521
|
}
|
|
505
522
|
return body;
|
|
506
523
|
}
|
|
507
|
-
export function buildProviderV2(models, options = {}) {
|
|
524
|
+
export function buildProviderV2(models, options = {}, nativeImages = false) {
|
|
508
525
|
return deepMerge({
|
|
509
526
|
name: options.providerName ?? "LiteLLM",
|
|
510
527
|
api: {
|
|
@@ -518,6 +535,6 @@ export function buildProviderV2(models, options = {}) {
|
|
|
518
535
|
...(options.apiKey ? { apiKey: options.apiKey } : {}),
|
|
519
536
|
},
|
|
520
537
|
},
|
|
521
|
-
models: mapModelsV2(models, options),
|
|
538
|
+
models: mapModelsV2(models, options, nativeImages),
|
|
522
539
|
}, options.providerOverrides);
|
|
523
540
|
}
|
package/dist/model-contracts.js
CHANGED
|
@@ -28,12 +28,24 @@ function contract(family, aliases, modes, defaultMode, metadata, suppressDiscove
|
|
|
28
28
|
}
|
|
29
29
|
const GPT_STANDARD_MODES = effortModes(["none", "low", "medium", "high", "xhigh"]);
|
|
30
30
|
const GPT_MAX_MODES = effortModes(["none", "low", "medium", "high", "xhigh", "max"]);
|
|
31
|
+
const GPT_61_SOL_MODES = effortModes(["low", "medium", "high", "xhigh", "max"]);
|
|
31
32
|
const DEEPSEEK_MODES = Object.freeze([
|
|
32
33
|
Object.freeze({ id: "off", legacy: freezeJson({ reasoningEffort: "none" }), v2Body: freezeJson({ reasoning_effort: "none" }) }),
|
|
33
34
|
Object.freeze({ id: "low", legacy: freezeJson({ reasoningEffort: "low" }), v2Body: freezeJson({ reasoning_effort: "low" }) }),
|
|
34
35
|
Object.freeze({ id: "high", legacy: freezeJson({ reasoningEffort: "high" }), v2Body: freezeJson({ reasoning_effort: "high" }) }),
|
|
35
36
|
Object.freeze({ id: "max", legacy: freezeJson({ reasoningEffort: "max" }), v2Body: freezeJson({ reasoning_effort: "max" }) }),
|
|
36
37
|
]);
|
|
38
|
+
// V4.1's official encoder accepts chat mode plus low/high/max effort aliases;
|
|
39
|
+
// the plugin keeps the existing off -> none wire contract for LiteLLM/vLLM.
|
|
40
|
+
const DEEPSEEK_V41_METADATA = Object.freeze({
|
|
41
|
+
modalities: Object.freeze({
|
|
42
|
+
input: Object.freeze(["text", "image"]),
|
|
43
|
+
output: Object.freeze(["text"]),
|
|
44
|
+
}),
|
|
45
|
+
attachment: true,
|
|
46
|
+
tool_call: true,
|
|
47
|
+
limit: Object.freeze({ context: 1048576, output: 262144 }),
|
|
48
|
+
});
|
|
37
49
|
const qwenOff = Object.freeze({ enable_thinking: false });
|
|
38
50
|
const QWEN_MODES = Object.freeze([
|
|
39
51
|
Object.freeze({
|
|
@@ -78,8 +90,9 @@ export const MODEL_CONTRACTS = Object.freeze([
|
|
|
78
90
|
contract("GPT-5.6 Sol", chatGptAliases("gpt-5.6-sol"), GPT_MAX_MODES, "medium"),
|
|
79
91
|
contract("GPT-5.6 Terra", chatGptAliases("gpt-5.6-terra"), GPT_MAX_MODES, "medium"),
|
|
80
92
|
contract("GPT-6 Luna", chatGptAliases("gpt-6-luna"), GPT_MAX_MODES, "medium"),
|
|
81
|
-
contract("GPT-6 Sol", chatGptAliases("gpt-6-sol"),
|
|
93
|
+
contract("GPT-6.1 Sol", chatGptAliases("gpt-6.1-sol"), GPT_61_SOL_MODES, "medium", undefined, true),
|
|
82
94
|
contract("DeepSeek V4 Flash", ["deepseek-v4-flash-fp8-mtp", "deepseek-v4-flash-fp8-mtp-norefusal"], DEEPSEEK_MODES, "max"),
|
|
95
|
+
contract("DeepSeek V4.1 Flash", ["deepseek-v4.1-flash-1017"], DEEPSEEK_MODES, "high", DEEPSEEK_V41_METADATA, true),
|
|
83
96
|
contract("Qwen3.8", ["qwen3.8-27b-refusal-dial"], QWEN_MODES, "xhigh"),
|
|
84
97
|
contract("Qwen3.8", ["qwen3.8-flash-next"], QWEN_MODES, "xhigh", QWEN_FLASH_NEXT_METADATA, true),
|
|
85
98
|
contract("Qwen3.8 Strata", ["5090-local-model", "5090-local-model-codex"], STRATA_QWEN_MODES, "xhigh", STRATA_QWEN_METADATA, true),
|
package/dist/options.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LiteLLMPluginOptions } from "./types.js";
|
|
2
|
-
export type ResolvedOptions = Required<Pick<LiteLLMPluginOptions, "providerKey" | "providerName" | "timeoutMs" | "strict">> & Omit<LiteLLMPluginOptions, "providerKey" | "providerName" | "timeoutMs" | "strict">;
|
|
2
|
+
export type ResolvedOptions = Required<Pick<LiteLLMPluginOptions, "providerKey" | "providerName" | "timeoutMs" | "strict" | "startupRetryMs" | "recoveryIntervalMs">> & Omit<LiteLLMPluginOptions, "providerKey" | "providerName" | "timeoutMs" | "strict" | "startupRetryMs" | "recoveryIntervalMs">;
|
|
3
3
|
export declare function resolveOptions(options?: LiteLLMPluginOptions): ResolvedOptions;
|
|
4
4
|
export declare function modelInfoURL(baseURL: string): string;
|
|
5
5
|
export declare function modelGroupInfoURL(baseURL: string): string;
|
package/dist/options.js
CHANGED
|
@@ -12,6 +12,8 @@ export function resolveOptions(options = {}) {
|
|
|
12
12
|
providerKey: options.providerKey ?? "LiteLLM",
|
|
13
13
|
providerName: options.providerName ?? "LiteLLM",
|
|
14
14
|
timeoutMs: options.timeoutMs ?? 30_000,
|
|
15
|
+
startupRetryMs: options.startupRetryMs ?? 120_000,
|
|
16
|
+
recoveryIntervalMs: options.recoveryIntervalMs ?? 30_000,
|
|
15
17
|
strict: options.strict ?? false,
|
|
16
18
|
};
|
|
17
19
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export type ModelContract = Readonly<{
|
|
|
27
27
|
metadata?: ModelContractMetadata;
|
|
28
28
|
}>;
|
|
29
29
|
export type OpenCodeModel = JsonObject & {
|
|
30
|
+
type?: "image";
|
|
30
31
|
provider?: {
|
|
31
32
|
npm?: string;
|
|
32
33
|
api?: string;
|
|
@@ -54,6 +55,7 @@ export type OpenCodeModel = JsonObject & {
|
|
|
54
55
|
variants?: Record<string, JsonObject>;
|
|
55
56
|
};
|
|
56
57
|
export type OpenCodeModelV2 = JsonObject & {
|
|
58
|
+
type?: "image";
|
|
57
59
|
family?: string;
|
|
58
60
|
name?: string;
|
|
59
61
|
api?: {
|
|
@@ -131,6 +133,10 @@ export type LiteLLMPluginOptions = {
|
|
|
131
133
|
/** Exact model_info.backend matches, unioned with includeModels pins. */
|
|
132
134
|
includeBackends?: string[];
|
|
133
135
|
timeoutMs?: number;
|
|
136
|
+
/** Total startup grace period for transient discovery failures. Zero disables retries. */
|
|
137
|
+
startupRetryMs?: number;
|
|
138
|
+
/** V2 recovery interval after startup grace expires. Zero disables background recovery. */
|
|
139
|
+
recoveryIntervalMs?: number;
|
|
134
140
|
strict?: boolean;
|
|
135
141
|
};
|
|
136
142
|
export type LiteLLMModelInfo = JsonObject & {
|
package/dist/v2.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ import type { Plugin as CurrentPlugin } from "@opencode/plugin";
|
|
|
2
2
|
type CurrentContext = Parameters<CurrentPlugin.Plugin["setup"]>[0];
|
|
3
3
|
declare const LiteLLMPluginV2: {
|
|
4
4
|
id: string;
|
|
5
|
-
setup: (ctx: CurrentContext) => Promise<void>;
|
|
5
|
+
setup: (ctx: CurrentContext) => Promise<() => void>;
|
|
6
6
|
};
|
|
7
7
|
export default LiteLLMPluginV2;
|
package/dist/v2.js
CHANGED
|
@@ -2,9 +2,17 @@ import { fetchModelGroupInfo, fetchModelInfo } from "./litellm.js";
|
|
|
2
2
|
import { buildProviderV2, mergeModelInfo } from "./mapping.js";
|
|
3
3
|
import { deepMerge } from "./merge.js";
|
|
4
4
|
import { resolveOptions } from "./options.js";
|
|
5
|
+
import { discoverWithRetry, isTransient } from "./discovery.js";
|
|
5
6
|
const CHAT_PACKAGE = "@opencode/ai/providers/openai-compatible";
|
|
6
7
|
// The 2.0.16 binary bundles this driver, but not openai-compatible/responses.
|
|
7
8
|
const RESPONSES_PACKAGE = "@opencode/ai/providers/openai/responses";
|
|
9
|
+
/** Image catalog types and their resolver are available in OpenCode 2.2+. */
|
|
10
|
+
function supportsNativeImages(ctx) {
|
|
11
|
+
const version = ctx.app?.version?.match(/^v?(\d+)\.(\d+)\.(\d+)(?:$|[-+])/);
|
|
12
|
+
if (!version)
|
|
13
|
+
return false;
|
|
14
|
+
return Number(version[1]) > 2 || (Number(version[1]) === 2 && Number(version[2]) >= 2);
|
|
15
|
+
}
|
|
8
16
|
function packageName(name) {
|
|
9
17
|
if (name === "@ai-sdk/openai" || name === "@opencode/ai/providers/openai-compatible/responses")
|
|
10
18
|
return RESPONSES_PACKAGE;
|
|
@@ -49,6 +57,7 @@ function currentModel(providerID, id, config, providerPackage) {
|
|
|
49
57
|
}
|
|
50
58
|
return {
|
|
51
59
|
id,
|
|
60
|
+
...(config.type === "image" ? { type: "image" } : {}),
|
|
52
61
|
modelID: config.api?.id ?? id,
|
|
53
62
|
providerID,
|
|
54
63
|
name: config.name ?? id,
|
|
@@ -226,62 +235,85 @@ const LiteLLMPluginV2 = {
|
|
|
226
235
|
id: "opencode-litellm",
|
|
227
236
|
setup: async (ctx) => {
|
|
228
237
|
const options = resolveOptions(ctx.options);
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
238
|
+
const controller = new AbortController();
|
|
239
|
+
let timer;
|
|
240
|
+
let recovered = false;
|
|
241
|
+
const cleanup = () => { controller.abort(); if (timer)
|
|
242
|
+
clearTimeout(timer); };
|
|
243
|
+
const load = async () => {
|
|
244
|
+
let provider;
|
|
233
245
|
try {
|
|
234
|
-
|
|
246
|
+
const deployments = await discoverWithRetry((attempt, signal) => fetchModelInfo(attempt, fetch, signal), recovered ? { ...options, startupRetryMs: 0 } : options, controller.signal);
|
|
247
|
+
let groups = [];
|
|
248
|
+
try {
|
|
249
|
+
groups = await fetchModelGroupInfo(options, fetch, controller.signal);
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
if (options.strict)
|
|
253
|
+
throw error;
|
|
254
|
+
console.warn("opencode-litellm: failed to load LiteLLM model group metadata", error instanceof Error ? error.message : String(error));
|
|
255
|
+
}
|
|
256
|
+
const models = mergeModelInfo(deployments, groups);
|
|
257
|
+
if (ctx.provider?.transform) {
|
|
258
|
+
const overrides = { ...options.overrides };
|
|
259
|
+
for (const model of models) {
|
|
260
|
+
if (model.model_info?.mode !== "responses")
|
|
261
|
+
continue;
|
|
262
|
+
const id = model.model_name ?? model.litellm_params?.model ?? model.model_info?.id;
|
|
263
|
+
if (!id)
|
|
264
|
+
continue;
|
|
265
|
+
// Opaque Responses aliases need the same transport as GPT-named models.
|
|
266
|
+
overrides[id] = deepMerge({ provider: { npm: "@ai-sdk/openai" } }, overrides[id]);
|
|
267
|
+
}
|
|
268
|
+
provider = buildProviderV2(models, { ...options, overrides }, supportsNativeImages(ctx));
|
|
269
|
+
for (const model of models) {
|
|
270
|
+
if (model.model_info?.use_responses_lite !== true)
|
|
271
|
+
continue;
|
|
272
|
+
const id = model.model_name ?? model.litellm_params?.model ?? model.model_info?.id;
|
|
273
|
+
const mapped = id ? provider.models[id] : undefined;
|
|
274
|
+
if (!mapped || currentModelPackage(mapped, currentProviderPackage(provider)) !== RESPONSES_PACKAGE)
|
|
275
|
+
continue;
|
|
276
|
+
mapped.request ??= {};
|
|
277
|
+
mapped.request.body = deepMerge({ reasoning: { context: "all_turns" } }, mapped.request.body);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
provider = buildProviderV2(models, options);
|
|
282
|
+
}
|
|
235
283
|
}
|
|
236
284
|
catch (error) {
|
|
285
|
+
if (controller.signal.aborted)
|
|
286
|
+
return;
|
|
237
287
|
if (options.strict)
|
|
238
288
|
throw error;
|
|
239
|
-
console.warn("opencode-litellm: failed to load LiteLLM
|
|
289
|
+
console.warn("opencode-litellm: failed to load LiteLLM provider metadata", error instanceof Error ? error.message : String(error));
|
|
290
|
+
const interval = options.recoveryIntervalMs;
|
|
291
|
+
if (isTransient(error) && Number.isFinite(interval) && interval > 0 && ctx.provider?.reload) {
|
|
292
|
+
timer = setTimeout(() => {
|
|
293
|
+
recovered = true;
|
|
294
|
+
void load().catch((failure) => console.warn("opencode-litellm: catalog recovery failed", failure instanceof Error ? failure.message : String(failure)));
|
|
295
|
+
}, interval);
|
|
296
|
+
timer.unref?.();
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
240
299
|
}
|
|
241
|
-
|
|
300
|
+
if (controller.signal.aborted)
|
|
301
|
+
return;
|
|
242
302
|
if (ctx.provider?.transform) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const id = model.model_name ?? model.litellm_params?.model ?? model.model_info?.id;
|
|
248
|
-
if (!id)
|
|
249
|
-
continue;
|
|
250
|
-
// Opaque Responses aliases need the same transport as GPT-named models.
|
|
251
|
-
overrides[id] = deepMerge({ provider: { npm: "@ai-sdk/openai" } }, overrides[id]);
|
|
252
|
-
}
|
|
253
|
-
provider = buildProviderV2(models, { ...options, overrides });
|
|
254
|
-
for (const model of models) {
|
|
255
|
-
if (model.model_info?.use_responses_lite !== true)
|
|
256
|
-
continue;
|
|
257
|
-
const id = model.model_name ?? model.litellm_params?.model ?? model.model_info?.id;
|
|
258
|
-
const mapped = id ? provider.models[id] : undefined;
|
|
259
|
-
if (!mapped || currentModelPackage(mapped, currentProviderPackage(provider)) !== RESPONSES_PACKAGE)
|
|
260
|
-
continue;
|
|
261
|
-
mapped.request ??= {};
|
|
262
|
-
mapped.request.body = deepMerge({ reasoning: { context: "all_turns" } }, mapped.request.body);
|
|
263
|
-
}
|
|
303
|
+
await registerCurrent(ctx, options.providerKey, provider);
|
|
304
|
+
if (recovered)
|
|
305
|
+
await ctx.provider.reload();
|
|
306
|
+
return;
|
|
264
307
|
}
|
|
265
|
-
|
|
266
|
-
|
|
308
|
+
const transitional = ctx;
|
|
309
|
+
if (transitional.catalog?.transform) {
|
|
310
|
+
await transitional.catalog.transform((catalog) => applyTransitional(catalog, options.providerKey, provider));
|
|
311
|
+
return;
|
|
267
312
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
console.warn("opencode-litellm: failed to load LiteLLM provider metadata", error instanceof Error ? error.message : String(error));
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
if (ctx.provider?.transform) {
|
|
276
|
-
await registerCurrent(ctx, options.providerKey, provider);
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
const transitional = ctx;
|
|
280
|
-
if (transitional.catalog?.transform) {
|
|
281
|
-
await transitional.catalog.transform((catalog) => applyTransitional(catalog, options.providerKey, provider));
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
throw new Error("opencode-litellm requires an OpenCode V2 provider or catalog transform API");
|
|
313
|
+
throw new Error("opencode-litellm requires an OpenCode V2 provider or catalog transform API");
|
|
314
|
+
};
|
|
315
|
+
await load();
|
|
316
|
+
return cleanup;
|
|
285
317
|
},
|
|
286
318
|
};
|
|
287
319
|
export default LiteLLMPluginV2;
|