@staticeng/opencode-litellm 0.2.3 → 0.2.5

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/LICENSE CHANGED
File without changes
package/README.md CHANGED
@@ -48,17 +48,20 @@ Shared or synchronized OpenCode configuration should use the published package n
48
48
  | `providerOverrides` | `{}` | Provider-level deep-merge overrides. |
49
49
  | `includeModes` | `chat`, `responses` | LiteLLM modes to include. |
50
50
  | `includeModels` | all included modes | Optional allowlist by model name. |
51
+ | `includeBackends` | unset | Optional exact allowlist by `model_info.backend`, unioned with `includeModels` pins. |
51
52
  | `excludeModels` | none | Optional denylist by model name. |
52
53
  | `timeoutMs` | `30000` | Per-request discovery timeout. |
53
54
  | `strict` | `false` | Throw on fetch/mapping failures instead of warning. |
54
55
 
55
56
  The LiteLLM key must have permission to call `GET /model/info` and root `GET /model_group/info`.
56
57
 
58
+ 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.
59
+
57
60
  ## Overrides
58
61
 
59
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.
60
63
 
61
- When LiteLLM publishes `model_info.supported_reasoning_efforts`, the plugin maps that list to OpenCode variants automatically for unknown models. Known contracts use exact alias membership and win over absent, incomplete, or conflicting discovery metadata. No substring, suffix, provider namespace, or base-model inference is used.
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.
62
65
 
63
66
  ```json
64
67
  {
@@ -99,9 +102,9 @@ The built-in mode matrix is:
99
102
  | GPT-5.6 Luna / Sol / Terra | `none`, `low`, `medium`, `high`, `xhigh`, `max` | `medium` |
100
103
  | DeepSeek V4 Flash | `off`, `low`, `high`, `max` | `max` |
101
104
  | Qwen3.8 27B refusal dial | `off`, `low`, `medium`, `xhigh` | `xhigh` |
102
- | Qwen3.8 Flash Next | `off`, `low` | `low` |
105
+ | Qwen3.8 Flash Next | `off`, `low`, `medium`, `xhigh` | `xhigh` |
103
106
 
104
- 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.
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. 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.
105
108
 
106
109
  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.
107
110
 
package/dist/helpers.d.ts CHANGED
File without changes
package/dist/helpers.js CHANGED
File without changes
package/dist/index.d.ts CHANGED
File without changes
package/dist/index.js CHANGED
File without changes
package/dist/litellm.d.ts CHANGED
File without changes
package/dist/litellm.js CHANGED
File without changes
package/dist/mapping.d.ts CHANGED
File without changes
package/dist/mapping.js CHANGED
@@ -169,8 +169,16 @@ function allowedByFilters(model, options) {
169
169
  return false;
170
170
  if (isRetiredModelAlias(name))
171
171
  return false;
172
- if (options.includeModels?.length && !options.includeModels.includes(name))
172
+ const pinned = options.includeModels?.includes(name) ?? false;
173
+ if (options.includeBackends !== undefined) {
174
+ const backend = model.model_info?.backend;
175
+ const backendMatched = typeof backend === "string" && options.includeBackends.includes(backend);
176
+ if (!backendMatched && !pinned)
177
+ return false;
178
+ }
179
+ else if (options.includeModels?.length && !pinned) {
173
180
  return false;
181
+ }
174
182
  if (options.excludeModels?.includes(name))
175
183
  return false;
176
184
  const modelMode = mode(model);
@@ -319,6 +327,8 @@ function generatedModel(model, tombstones = false) {
319
327
  if (!name)
320
328
  return undefined;
321
329
  const mappedModalities = modalities(model);
330
+ const discoveredVariants = supportsReasoning(model) ? reasoningVariants(model) : undefined;
331
+ const suppressDiscoveredVariants = modelContract(name)?.suppressDiscoveredVariants === true;
322
332
  const generated = {
323
333
  id: name,
324
334
  name,
@@ -328,7 +338,7 @@ function generatedModel(model, tombstones = false) {
328
338
  modalities: mappedModalities,
329
339
  ...(mappedModalities.input.includes("image") || mappedModalities.input.includes("pdf") ? { attachment: true } : {}),
330
340
  ...(supportsReasoning(model) ? { reasoning: true } : {}),
331
- ...(supportsReasoning(model) && reasoningVariants(model) ? { variants: reasoningVariants(model) } : {}),
341
+ ...(discoveredVariants && !suppressDiscoveredVariants ? { variants: discoveredVariants } : {}),
332
342
  ...(model.model_info?.supports_function_calling || model.model_info?.supports_tool_choice ? { tool_call: true } : {}),
333
343
  };
334
344
  const defaults = contractDefaults(name, tombstones);
@@ -360,20 +370,26 @@ function v2Variants(model) {
360
370
  return undefined;
361
371
  return Object.entries(variants).map(([id, value]) => ({ id, body: v2VariantBody(value) }));
362
372
  }
363
- function contractV2Variants(name, override) {
373
+ function contractV2Variants(name, model, override) {
364
374
  const entry = modelContract(name);
365
375
  if (!entry)
366
376
  return undefined;
367
377
  const variants = entry.modes.map((mode) => ({ id: mode.id, body: { ...mode.v2Body } }));
368
- if (!isRecord(override?.variants))
369
- return variants;
370
378
  const byID = new Map(variants.map((variant) => [variant.id, variant]));
371
- for (const [id, value] of Object.entries(override.variants)) {
372
- const current = byID.get(id);
373
- byID.set(id, {
374
- id,
375
- body: deepMerge(current?.body ?? {}, v2VariantBody(value)),
376
- });
379
+ if (isRecord(override?.variants)) {
380
+ for (const [id, value] of Object.entries(override.variants)) {
381
+ const current = byID.get(id);
382
+ byID.set(id, {
383
+ id,
384
+ body: deepMerge(current?.body ?? {}, v2VariantBody(value)),
385
+ });
386
+ }
387
+ }
388
+ if (!entry.suppressDiscoveredVariants && isRecord(model.variants)) {
389
+ for (const [id, value] of Object.entries(model.variants)) {
390
+ if (!byID.has(id))
391
+ byID.set(id, { id, body: v2VariantBody(value) });
392
+ }
377
393
  }
378
394
  return Array.from(byID.values());
379
395
  }
@@ -441,7 +457,7 @@ export function mapModelsV2(models, options = {}) {
441
457
  const legacy = mapModels(models, options);
442
458
  return Object.fromEntries(Object.entries(legacy).map(([name, model]) => {
443
459
  const mapped = v2Model(model, options.baseURL);
444
- const variants = contractV2Variants(name, options.overrides?.[name]);
460
+ const variants = contractV2Variants(name, model, options.overrides?.[name]);
445
461
  return [name, variants ? { ...mapped, variants } : mapped];
446
462
  }));
447
463
  }
package/dist/merge.d.ts CHANGED
File without changes
package/dist/merge.js CHANGED
File without changes
File without changes
@@ -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"], QWEN_FLASH_NEXT_MODES, "low", QWEN_FLASH_NEXT_METADATA),
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/options.d.ts CHANGED
File without changes
package/dist/options.js CHANGED
File without changes
package/dist/types.d.ts CHANGED
@@ -23,6 +23,7 @@ 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 & {
@@ -123,6 +124,8 @@ export type LiteLLMPluginOptions = {
123
124
  includeModes?: string[];
124
125
  excludeModels?: string[];
125
126
  includeModels?: string[];
127
+ /** Exact model_info.backend matches, unioned with includeModels pins. */
128
+ includeBackends?: string[];
126
129
  timeoutMs?: number;
127
130
  strict?: boolean;
128
131
  };
@@ -134,6 +137,7 @@ export type LiteLLMModelInfo = JsonObject & {
134
137
  model_info?: JsonObject & {
135
138
  id?: string;
136
139
  base_model?: string;
140
+ backend?: unknown;
137
141
  mode?: string;
138
142
  input_cost_per_token?: number | string;
139
143
  output_cost_per_token?: number | string;
package/dist/types.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staticeng/opencode-litellm",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "OpenCode plugin that discovers LiteLLM models at runtime.",
5
5
  "type": "module",
6
6
  "license": "MIT",