@staticeng/opencode-litellm 0.2.2 → 0.2.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 CHANGED
@@ -48,12 +48,15 @@ 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.
@@ -98,9 +101,12 @@ The built-in mode matrix is:
98
101
  | GPT-5.5 | `none`, `low`, `medium`, `high`, `xhigh` | `medium` |
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
- | Qwen3.8 | `off`, `low`, `medium`, `xhigh` | `xhigh` |
104
+ | Qwen3.8 27B refusal dial | `off`, `low`, `medium`, `xhigh` | `xhigh` |
105
+ | Qwen3.8 Flash Next | `off`, `low` | `low` |
106
+
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.
102
108
 
103
- 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 default `xhigh` effort before serialization. The `variantDefault` compatibility marker is UI-only and is stripped before requests for exact built-in aliases.
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.
104
110
 
105
111
  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.
106
112
 
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);
@@ -274,12 +282,23 @@ function contractDefaults(name, tombstones = false) {
274
282
  const disabled = entry.family === "DeepSeek V4 Flash"
275
283
  ? ["medium", "xhigh"]
276
284
  : entry.family === "Qwen3.8"
277
- ? ["none", "minimal", "high", "max"]
285
+ ? ["none", "minimal", "low", "medium", "high", "xhigh", "max", ...(entry.metadata ? ["ultra"] : [])].filter((effort) => !entry.modes.some((mode) => mode.id === effort))
278
286
  : [];
279
287
  for (const effort of disabled)
280
288
  variants[effort] = { disabled: true };
281
289
  }
282
290
  return {
291
+ ...(entry.metadata
292
+ ? {
293
+ modalities: {
294
+ input: [...entry.metadata.modalities.input],
295
+ output: [...entry.metadata.modalities.output],
296
+ },
297
+ attachment: entry.metadata.attachment,
298
+ tool_call: entry.metadata.tool_call,
299
+ limit: { ...entry.metadata.limit },
300
+ }
301
+ : {}),
283
302
  reasoning: true,
284
303
  options: { ...defaultMode.legacy, variantDefault: entry.variantDefault },
285
304
  variants,
@@ -320,7 +339,9 @@ function generatedModel(model, tombstones = false) {
320
339
  ...(supportsReasoning(model) && reasoningVariants(model) ? { variants: reasoningVariants(model) } : {}),
321
340
  ...(model.model_info?.supports_function_calling || model.model_info?.supports_tool_choice ? { tool_call: true } : {}),
322
341
  };
323
- return deepMerge(generated, contractDefaults(name, tombstones));
342
+ const defaults = contractDefaults(name, tombstones);
343
+ const merged = deepMerge(generated, defaults);
344
+ return defaults?.limit ? { ...merged, limit: { ...defaults.limit } } : merged;
324
345
  }
325
346
  function finalizedModel(model, override) {
326
347
  const input = supportedModalities(model.modalities?.input ?? [TEXT_MODALITY]);
@@ -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) {
15
+ function contract(family, aliases, modes, defaultMode, metadata) {
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) {
22
22
  defaultMode,
23
23
  reasoning: true,
24
24
  variantDefault: false,
25
+ ...(metadata ? { metadata } : {}),
25
26
  });
26
27
  }
27
28
  const GPT_STANDARD_MODES = effortModes(["none", "low", "medium", "high", "xhigh"]);
@@ -43,6 +44,16 @@ const QWEN_MODES = Object.freeze([
43
44
  Object.freeze({ id: "medium", legacy: freezeJson({ reasoningEffort: "medium" }), v2Body: freezeJson({ reasoning_effort: "medium" }) }),
44
45
  Object.freeze({ id: "xhigh", legacy: freezeJson({ reasoningEffort: "xhigh" }), v2Body: freezeJson({ reasoning_effort: "xhigh" }) }),
45
46
  ]);
47
+ const QWEN_FLASH_NEXT_MODES = Object.freeze(QWEN_MODES.slice(0, 2));
48
+ const QWEN_FLASH_NEXT_METADATA = Object.freeze({
49
+ modalities: Object.freeze({
50
+ input: Object.freeze(["text", "image"]),
51
+ output: Object.freeze(["text"]),
52
+ }),
53
+ attachment: true,
54
+ tool_call: true,
55
+ limit: Object.freeze({ context: 262144, output: 0 }),
56
+ });
46
57
  const chatGptAliases = (model) => Object.freeze([
47
58
  model,
48
59
  `chatgpt/${model}`,
@@ -58,6 +69,7 @@ export const MODEL_CONTRACTS = Object.freeze([
58
69
  contract("GPT-5.6 Terra", chatGptAliases("gpt-5.6-terra"), GPT_MAX_MODES, "medium"),
59
70
  contract("DeepSeek V4 Flash", ["deepseek-v4-flash-fp8-mtp", "deepseek-v4-flash-fp8-mtp-norefusal"], DEEPSEEK_MODES, "max"),
60
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),
61
73
  ]);
62
74
  export const RETIRED_MODEL_ALIASES = Object.freeze([
63
75
  ...chatGptAliases("gpt-5.3-codex"),
package/dist/types.d.ts CHANGED
@@ -4,6 +4,18 @@ export type ModelContractMode = Readonly<{
4
4
  legacy: Readonly<JsonObject>;
5
5
  v2Body: Readonly<JsonObject>;
6
6
  }>;
7
+ export type ModelContractMetadata = Readonly<{
8
+ modalities: Readonly<{
9
+ input: readonly string[];
10
+ output: readonly string[];
11
+ }>;
12
+ attachment: boolean;
13
+ tool_call: boolean;
14
+ limit: Readonly<{
15
+ context: number;
16
+ output: number;
17
+ }>;
18
+ }>;
7
19
  export type ModelContract = Readonly<{
8
20
  family: string;
9
21
  aliases: readonly string[];
@@ -11,6 +23,7 @@ export type ModelContract = Readonly<{
11
23
  defaultMode: string;
12
24
  reasoning: true;
13
25
  variantDefault: false;
26
+ metadata?: ModelContractMetadata;
14
27
  }>;
15
28
  export type OpenCodeModel = JsonObject & {
16
29
  id?: string;
@@ -110,6 +123,8 @@ export type LiteLLMPluginOptions = {
110
123
  includeModes?: string[];
111
124
  excludeModels?: string[];
112
125
  includeModels?: string[];
126
+ /** Exact model_info.backend matches, unioned with includeModels pins. */
127
+ includeBackends?: string[];
113
128
  timeoutMs?: number;
114
129
  strict?: boolean;
115
130
  };
@@ -121,6 +136,7 @@ export type LiteLLMModelInfo = JsonObject & {
121
136
  model_info?: JsonObject & {
122
137
  id?: string;
123
138
  base_model?: string;
139
+ backend?: unknown;
124
140
  mode?: string;
125
141
  input_cost_per_token?: number | string;
126
142
  output_cost_per_token?: number | string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staticeng/opencode-litellm",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "OpenCode plugin that discovers LiteLLM models at runtime.",
5
5
  "type": "module",
6
6
  "license": "MIT",