@staticeng/opencode-litellm 0.2.4 → 0.2.6
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 +28 -3
- package/dist/mapping.js +50 -14
- package/dist/model-contracts.js +3 -3
- package/dist/types.d.ts +5 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -61,7 +61,7 @@ When `includeBackends` is present, models are candidates when `model_info.backen
|
|
|
61
61
|
|
|
62
62
|
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, DeepSeek V4 Flash, and Qwen3.8 aliases. Use `overrides` to customize those defaults or to add metadata for unknown models.
|
|
63
63
|
|
|
64
|
-
When LiteLLM publishes `model_info.supported_reasoning_efforts`, the plugin maps that list to OpenCode variants automatically
|
|
64
|
+
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. Currently only `qwen3.8-flash-next` closes discovery so unverified efforts cannot become active. No substring, suffix, provider namespace, or base-model inference is used.
|
|
65
65
|
|
|
66
66
|
```json
|
|
67
67
|
{
|
|
@@ -93,6 +93,31 @@ When LiteLLM publishes `model_info.supported_reasoning_efforts`, the plugin maps
|
|
|
93
93
|
|
|
94
94
|
Overrides win over built-in contracts and generated values through a recursive deep merge. Model overrides are applied first and provider overrides last. Arrays and scalar values are replaced rather than concatenated.
|
|
95
95
|
|
|
96
|
+
### GPT Transport
|
|
97
|
+
|
|
98
|
+
GPT models use `@ai-sdk/openai` and the Responses API through the configured
|
|
99
|
+
LiteLLM base URL (for example, `/v1/responses`). Other models retain
|
|
100
|
+
`@ai-sdk/openai-compatible` and Chat Completions. No OpenAI OAuth endpoint or
|
|
101
|
+
direct upstream URL is introduced.
|
|
102
|
+
|
|
103
|
+
Selection matches a final `gpt-` plus digit segment (case-insensitive), including
|
|
104
|
+
namespaced aliases, in the routable model name, `base_model`, or
|
|
105
|
+
`litellm_params.model`. Opaque deployment IDs do not override a named alias.
|
|
106
|
+
The routable ID is preserved. O-series and non-GPT models are not migrated merely
|
|
107
|
+
because discovery reports `mode: responses`. Explicit model `provider.npm`
|
|
108
|
+
overrides still win; V2 uses the corresponding `api.package`.
|
|
109
|
+
|
|
110
|
+
Legacy reasoning options remain `reasoningEffort` (OpenCode places them under
|
|
111
|
+
`providerOptions.openai` for this SDK). V2 GPT request defaults and variants use
|
|
112
|
+
Responses wire bodies: `reasoning: { effort }`, not `reasoning_effort`.
|
|
113
|
+
Use an OpenCode release with AI SDK 5 or newer, where the OpenAI SDK's
|
|
114
|
+
`languageModel()` defaults to Responses. Tests pin the SDK versions used by
|
|
115
|
+
OpenCode at qualification time; these are development dependencies only.
|
|
116
|
+
|
|
117
|
+
After upgrading, pin the exact plugin version and restart only when active
|
|
118
|
+
sessions can safely stop. Existing processes retain their loaded plugin; do not
|
|
119
|
+
clear global caches to upgrade one plugin.
|
|
120
|
+
|
|
96
121
|
The built-in mode matrix is:
|
|
97
122
|
|
|
98
123
|
| Contract | Modes in order | Default |
|
|
@@ -102,9 +127,9 @@ The built-in mode matrix is:
|
|
|
102
127
|
| GPT-5.6 Luna / Sol / Terra | `none`, `low`, `medium`, `high`, `xhigh`, `max` | `medium` |
|
|
103
128
|
| DeepSeek V4 Flash | `off`, `low`, `high`, `max` | `max` |
|
|
104
129
|
| Qwen3.8 27B refusal dial | `off`, `low`, `medium`, `xhigh` | `xhigh` |
|
|
105
|
-
| Qwen3.8 Flash Next | `off`, `low` | `
|
|
130
|
+
| Qwen3.8 Flash Next | `off`, `low`, `medium`, `xhigh` | `xhigh` |
|
|
106
131
|
|
|
107
|
-
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. For requests without an explicit named variant, the plugin supplies the row's contract default through model options; an explicit variant is merged afterward and wins. Qwen's explicit `off` cleanup removes the model's default effort before serialization. The `variantDefault` compatibility marker is UI-only and is stripped before requests for exact built-in aliases.
|
|
132
|
+
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. For requests without an explicit named variant, the plugin supplies the row's contract default through model options; an explicit variant is merged afterward and wins. Qwen's explicit `off` cleanup removes the model's default effort before serialization. The `variantDefault` compatibility marker is UI-only and is stripped before requests for exact built-in aliases.
|
|
108
133
|
|
|
109
134
|
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.
|
|
110
135
|
|
package/dist/mapping.js
CHANGED
|
@@ -111,6 +111,10 @@ function enrichMetadata(existing, incoming, key) {
|
|
|
111
111
|
function modelName(model) {
|
|
112
112
|
return asString(model.model_name) ?? asString(model.litellm_params?.model) ?? asString(model.model_info?.id);
|
|
113
113
|
}
|
|
114
|
+
function usesGPTResponses(model) {
|
|
115
|
+
// Match a GPT family segment, not an incidental substring or an opaque deployment ID.
|
|
116
|
+
return [modelName(model), model.model_info?.base_model, model.litellm_params?.model].some((name) => typeof name === "string" && /(?:^|\/)gpt-\d[^/]*$/i.test(name));
|
|
117
|
+
}
|
|
114
118
|
export function mergeModelInfo(deployments, groups) {
|
|
115
119
|
const byName = new Map();
|
|
116
120
|
for (const model of [...deployments, ...groups]) {
|
|
@@ -327,16 +331,19 @@ function generatedModel(model, tombstones = false) {
|
|
|
327
331
|
if (!name)
|
|
328
332
|
return undefined;
|
|
329
333
|
const mappedModalities = modalities(model);
|
|
334
|
+
const discoveredVariants = supportsReasoning(model) ? reasoningVariants(model) : undefined;
|
|
335
|
+
const suppressDiscoveredVariants = modelContract(name)?.suppressDiscoveredVariants === true;
|
|
330
336
|
const generated = {
|
|
331
337
|
id: name,
|
|
332
338
|
name,
|
|
339
|
+
...(usesGPTResponses(model) ? { provider: { npm: "@ai-sdk/openai" } } : {}),
|
|
333
340
|
...(asString(model.model_info?.base_model) ? { family: asString(model.model_info?.base_model) } : {}),
|
|
334
341
|
...(cost(model) ? { cost: cost(model) } : {}),
|
|
335
342
|
...(limit(model) ? { limit: limit(model) } : {}),
|
|
336
343
|
modalities: mappedModalities,
|
|
337
344
|
...(mappedModalities.input.includes("image") || mappedModalities.input.includes("pdf") ? { attachment: true } : {}),
|
|
338
345
|
...(supportsReasoning(model) ? { reasoning: true } : {}),
|
|
339
|
-
...(
|
|
346
|
+
...(discoveredVariants && !suppressDiscoveredVariants ? { variants: discoveredVariants } : {}),
|
|
340
347
|
...(model.model_info?.supports_function_calling || model.model_info?.supports_tool_choice ? { tool_call: true } : {}),
|
|
341
348
|
};
|
|
342
349
|
const defaults = contractDefaults(name, tombstones);
|
|
@@ -368,20 +375,26 @@ function v2Variants(model) {
|
|
|
368
375
|
return undefined;
|
|
369
376
|
return Object.entries(variants).map(([id, value]) => ({ id, body: v2VariantBody(value) }));
|
|
370
377
|
}
|
|
371
|
-
function contractV2Variants(name, override) {
|
|
378
|
+
function contractV2Variants(name, model, override) {
|
|
372
379
|
const entry = modelContract(name);
|
|
373
380
|
if (!entry)
|
|
374
381
|
return undefined;
|
|
375
382
|
const variants = entry.modes.map((mode) => ({ id: mode.id, body: { ...mode.v2Body } }));
|
|
376
|
-
if (!isRecord(override?.variants))
|
|
377
|
-
return variants;
|
|
378
383
|
const byID = new Map(variants.map((variant) => [variant.id, variant]));
|
|
379
|
-
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
id,
|
|
383
|
-
|
|
384
|
-
|
|
384
|
+
if (isRecord(override?.variants)) {
|
|
385
|
+
for (const [id, value] of Object.entries(override.variants)) {
|
|
386
|
+
const current = byID.get(id);
|
|
387
|
+
byID.set(id, {
|
|
388
|
+
id,
|
|
389
|
+
body: deepMerge(current?.body ?? {}, v2VariantBody(value)),
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (!entry.suppressDiscoveredVariants && isRecord(model.variants)) {
|
|
394
|
+
for (const [id, value] of Object.entries(model.variants)) {
|
|
395
|
+
if (!byID.has(id))
|
|
396
|
+
byID.set(id, { id, body: v2VariantBody(value) });
|
|
397
|
+
}
|
|
385
398
|
}
|
|
386
399
|
return Array.from(byID.values());
|
|
387
400
|
}
|
|
@@ -395,8 +408,8 @@ function v2Model(model, baseURL) {
|
|
|
395
408
|
api: {
|
|
396
409
|
...(id ? { id } : {}),
|
|
397
410
|
type: "aisdk",
|
|
398
|
-
package: "@ai-sdk/openai-compatible",
|
|
399
|
-
...(baseURL ? { url: baseURL } : {}),
|
|
411
|
+
package: model.provider?.npm ?? "@ai-sdk/openai-compatible",
|
|
412
|
+
...(model.provider?.api ?? baseURL ? { url: model.provider?.api ?? baseURL } : {}),
|
|
400
413
|
settings: {},
|
|
401
414
|
},
|
|
402
415
|
capabilities: {
|
|
@@ -449,10 +462,33 @@ export function mapModelsV2(models, options = {}) {
|
|
|
449
462
|
const legacy = mapModels(models, options);
|
|
450
463
|
return Object.fromEntries(Object.entries(legacy).map(([name, model]) => {
|
|
451
464
|
const mapped = v2Model(model, options.baseURL);
|
|
452
|
-
const variants = contractV2Variants(name, options.overrides?.[name]);
|
|
453
|
-
|
|
465
|
+
const variants = contractV2Variants(name, model, options.overrides?.[name]);
|
|
466
|
+
const result = variants ? { ...mapped, variants } : mapped;
|
|
467
|
+
if (mapped.api?.package === "@ai-sdk/openai") {
|
|
468
|
+
const body = responsesBody(isRecord(model.options) ? model.options : {});
|
|
469
|
+
result.request = { body };
|
|
470
|
+
result.variants = result.variants?.map((variant) => ({ ...variant, body: responsesBody(variant.body ?? {}) }));
|
|
471
|
+
}
|
|
472
|
+
return [name, result];
|
|
454
473
|
}));
|
|
455
474
|
}
|
|
475
|
+
function responsesBody(options) {
|
|
476
|
+
const body = { ...options };
|
|
477
|
+
const effort = body.reasoningEffort ?? body.reasoning_effort;
|
|
478
|
+
const summary = body.reasoningSummary;
|
|
479
|
+
delete body.reasoningEffort;
|
|
480
|
+
delete body.reasoning_effort;
|
|
481
|
+
delete body.reasoningSummary;
|
|
482
|
+
delete body.variantDefault;
|
|
483
|
+
if (effort !== undefined || summary !== undefined) {
|
|
484
|
+
body.reasoning = {
|
|
485
|
+
...(isRecord(body.reasoning) ? body.reasoning : {}),
|
|
486
|
+
...(effort !== undefined ? { effort } : {}),
|
|
487
|
+
...(summary !== undefined ? { summary } : {}),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
return body;
|
|
491
|
+
}
|
|
456
492
|
export function buildProviderV2(models, options = {}) {
|
|
457
493
|
return deepMerge({
|
|
458
494
|
name: options.providerName ?? "LiteLLM",
|
package/dist/model-contracts.js
CHANGED
|
@@ -12,7 +12,7 @@ function effortModes(ids) {
|
|
|
12
12
|
v2Body: freezeJson({ reasoning_effort: id }),
|
|
13
13
|
})));
|
|
14
14
|
}
|
|
15
|
-
function contract(family, aliases, modes, defaultMode, metadata) {
|
|
15
|
+
function contract(family, aliases, modes, defaultMode, metadata, suppressDiscoveredVariants = false) {
|
|
16
16
|
if (!modes.some((mode) => mode.id === defaultMode))
|
|
17
17
|
throw new Error(`Model contract ${family} has an invalid default`);
|
|
18
18
|
return Object.freeze({
|
|
@@ -22,6 +22,7 @@ function contract(family, aliases, modes, defaultMode, metadata) {
|
|
|
22
22
|
defaultMode,
|
|
23
23
|
reasoning: true,
|
|
24
24
|
variantDefault: false,
|
|
25
|
+
...(suppressDiscoveredVariants ? { suppressDiscoveredVariants: true } : {}),
|
|
25
26
|
...(metadata ? { metadata } : {}),
|
|
26
27
|
});
|
|
27
28
|
}
|
|
@@ -44,7 +45,6 @@ const QWEN_MODES = Object.freeze([
|
|
|
44
45
|
Object.freeze({ id: "medium", legacy: freezeJson({ reasoningEffort: "medium" }), v2Body: freezeJson({ reasoning_effort: "medium" }) }),
|
|
45
46
|
Object.freeze({ id: "xhigh", legacy: freezeJson({ reasoningEffort: "xhigh" }), v2Body: freezeJson({ reasoning_effort: "xhigh" }) }),
|
|
46
47
|
]);
|
|
47
|
-
const QWEN_FLASH_NEXT_MODES = Object.freeze(QWEN_MODES.slice(0, 2));
|
|
48
48
|
const QWEN_FLASH_NEXT_METADATA = Object.freeze({
|
|
49
49
|
modalities: Object.freeze({
|
|
50
50
|
input: Object.freeze(["text", "image"]),
|
|
@@ -69,7 +69,7 @@ export const MODEL_CONTRACTS = Object.freeze([
|
|
|
69
69
|
contract("GPT-5.6 Terra", chatGptAliases("gpt-5.6-terra"), GPT_MAX_MODES, "medium"),
|
|
70
70
|
contract("DeepSeek V4 Flash", ["deepseek-v4-flash-fp8-mtp", "deepseek-v4-flash-fp8-mtp-norefusal"], DEEPSEEK_MODES, "max"),
|
|
71
71
|
contract("Qwen3.8", ["qwen3.8-27b-refusal-dial"], QWEN_MODES, "xhigh"),
|
|
72
|
-
contract("Qwen3.8", ["qwen3.8-flash-next"],
|
|
72
|
+
contract("Qwen3.8", ["qwen3.8-flash-next"], QWEN_MODES, "xhigh", QWEN_FLASH_NEXT_METADATA, true),
|
|
73
73
|
]);
|
|
74
74
|
export const RETIRED_MODEL_ALIASES = Object.freeze([
|
|
75
75
|
...chatGptAliases("gpt-5.3-codex"),
|
package/dist/types.d.ts
CHANGED
|
@@ -23,9 +23,14 @@ export type ModelContract = Readonly<{
|
|
|
23
23
|
defaultMode: string;
|
|
24
24
|
reasoning: true;
|
|
25
25
|
variantDefault: false;
|
|
26
|
+
suppressDiscoveredVariants?: true;
|
|
26
27
|
metadata?: ModelContractMetadata;
|
|
27
28
|
}>;
|
|
28
29
|
export type OpenCodeModel = JsonObject & {
|
|
30
|
+
provider?: {
|
|
31
|
+
npm?: string;
|
|
32
|
+
api?: string;
|
|
33
|
+
};
|
|
29
34
|
id?: string;
|
|
30
35
|
name?: string;
|
|
31
36
|
cost?: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@staticeng/opencode-litellm",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "OpenCode plugin that discovers LiteLLM models at runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
"@opencode-ai/plugin": "*"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
+
"@ai-sdk/openai": "3.0.84",
|
|
34
|
+
"@ai-sdk/openai-compatible": "2.0.41",
|
|
33
35
|
"@opencode-ai/plugin": "latest",
|
|
34
36
|
"@types/node": "^24.10.1",
|
|
35
37
|
"typescript": "^5.9.3"
|