@staticeng/opencode-litellm 0.1.6 → 0.1.8

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
@@ -1,6 +1,6 @@
1
1
  # opencode-litellm
2
2
 
3
- Runtime LiteLLM model autodiscovery for OpenCode. The plugin fetches LiteLLM `/model/info` at OpenCode startup and adds an in-memory provider named `LiteLLM` by default.
3
+ Runtime LiteLLM model autodiscovery for OpenCode. The plugin fetches LiteLLM deployment and model-group metadata at startup and adds an in-memory provider named `LiteLLM` by default.
4
4
 
5
5
  ## Install
6
6
 
@@ -40,17 +40,17 @@ Do not put real API keys in `opencode.json`. If you pass `apiKey` directly, the
40
40
  | `baseURL` | `LITELLM_BASE_URL` | LiteLLM proxy base URL. `/v1` URLs are supported. |
41
41
  | `apiKey` | env value | Bearer token for LiteLLM. |
42
42
  | `apiKeyEnv` | `LITELLM_API_KEY` | Environment variable used when `apiKey` is not set. |
43
- | `providerKey` | `LiteLLM` | Key written to `cfg.provider[providerKey]`. |
43
+ | `providerKey` | `LiteLLM` | Key written to legacy `cfg.provider` and V2 `cfg.providers`. |
44
44
  | `providerName` | `LiteLLM` | Human provider name. |
45
45
  | `overrides` | `{}` | Per-model deep-merge overrides. |
46
46
  | `providerOverrides` | `{}` | Provider-level deep-merge overrides. |
47
47
  | `includeModes` | `chat`, `responses` | LiteLLM modes to include. |
48
48
  | `includeModels` | all included modes | Optional allowlist by model name. |
49
49
  | `excludeModels` | none | Optional denylist by model name. |
50
- | `timeoutMs` | `30000` | `/model/info` request timeout. |
50
+ | `timeoutMs` | `30000` | Per-request discovery timeout. |
51
51
  | `strict` | `false` | Throw on fetch/mapping failures instead of warning. |
52
52
 
53
- The LiteLLM key must have permission to call `GET /model/info`.
53
+ The LiteLLM key must have permission to call `GET /model/info` and root `GET /model_group/info`.
54
54
 
55
55
  ## Overrides
56
56
 
@@ -88,9 +88,12 @@ Overrides win over generated values through a recursive deep merge. Arrays and s
88
88
 
89
89
  ## Runtime Behavior
90
90
 
91
- - Fetches `${baseURL}/model/info` with `Authorization: Bearer <token>`.
92
- - Handles base URLs with or without a trailing slash, including `/v1`.
93
- - Adds or replaces `cfg.provider[providerKey]` during OpenCode's plugin config hook.
91
+ - Fetches `${baseURL}/model/info`, then root `/model_group/info`, with separate authenticated and timed requests.
92
+ - Preserves `/v1` for model-info discovery but resolves model-group discovery from the LiteLLM origin.
93
+ - Adds or replaces both legacy `cfg.provider[providerKey]` and V2 `cfg.providers[providerKey]` during the config hook.
94
+ - Keeps namespaced deployment IDs and augments them with exact bare model-group IDs; exact collisions are enriched deterministically.
95
+ - Skips malformed group rows without a nonblank `model_group` and exposes reasoning capability on legacy and V2 model metadata.
96
+ - Warns safely and keeps deployment models when group discovery fails in non-strict mode; strict mode mutates neither config surface on failure.
94
97
  - Does not write `opencode.json`, cache model metadata, or persist generated provider data.
95
98
  - Includes `chat` and `responses` modes by default; non-chat modes such as `embedding` and `rerank` are excluded unless explicitly included.
96
99
  - No HuggingFace fallback is included in v1.
package/dist/helpers.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- export { buildProvider, buildProviderV2, mapModels, mapModelsV2 } from "./mapping.js";
2
- export { fetchModelInfo } from "./litellm.js";
1
+ export { buildProvider, buildProviderV2, mapModels, mapModelsV2, mergeModelInfo, normalizeModelListResponse, modelListResponseShape, } from "./mapping.js";
2
+ export { fetchModelGroupInfo, fetchModelInfo, normalizeModelGroupResponse } from "./litellm.js";
3
+ export { modelGroupInfoURL, modelInfoURL } from "./options.js";
3
4
  export { deepMerge } from "./merge.js";
4
- export type { LiteLLMModelInfo, LiteLLMPluginOptions, OpenCodeModel, OpenCodeModelV2, OpenCodeProvider, OpenCodeProviderV2, } from "./types.js";
5
+ export type { LiteLLMPluginOptions, LiteLLMModelGroup, LiteLLMModelInfo, OpenCodeProvider, OpenCodeProviderV2, OpenCodeModel, OpenCodeModelV2, } from "./types.js";
package/dist/helpers.js CHANGED
@@ -1,3 +1,4 @@
1
- export { buildProvider, buildProviderV2, mapModels, mapModelsV2 } from "./mapping.js";
2
- export { fetchModelInfo } from "./litellm.js";
1
+ export { buildProvider, buildProviderV2, mapModels, mapModelsV2, mergeModelInfo, normalizeModelListResponse, modelListResponseShape, } from "./mapping.js";
2
+ export { fetchModelGroupInfo, fetchModelInfo, normalizeModelGroupResponse } from "./litellm.js";
3
+ export { modelGroupInfoURL, modelInfoURL } from "./options.js";
3
4
  export { deepMerge } from "./merge.js";
package/dist/index.d.ts CHANGED
File without changes
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { fetchModelInfo } from "./litellm.js";
2
- import { buildProvider, buildProviderV2 } from "./mapping.js";
1
+ import { fetchModelGroupInfo, fetchModelInfo } from "./litellm.js";
2
+ import { buildProvider, buildProviderV2, mergeModelInfo } from "./mapping.js";
3
3
  import { resolveOptions } from "./options.js";
4
4
  async function log(ctx, level, message, extra = {}) {
5
5
  await ctx.client?.app?.log?.({ body: { service: "opencode-litellm", level, message, extra } });
@@ -9,7 +9,22 @@ const LiteLLMPlugin = async (ctx, options) => {
9
9
  return {
10
10
  config: async (cfg) => {
11
11
  try {
12
- const models = await fetchModelInfo(resolved);
12
+ const deployments = await fetchModelInfo(resolved);
13
+ let groups = [];
14
+ try {
15
+ groups = await fetchModelGroupInfo(resolved);
16
+ }
17
+ catch (error) {
18
+ const errorShape = error instanceof Error && "responseShape" in error ? error.responseShape : undefined;
19
+ await log(ctx, resolved.strict ? "error" : "warn", "failed to load LiteLLM model group metadata", {
20
+ providerKey: resolved.providerKey,
21
+ error: error instanceof Error ? error.message : String(error),
22
+ ...(errorShape ? { responseShape: errorShape } : {}),
23
+ });
24
+ if (resolved.strict)
25
+ throw error;
26
+ }
27
+ const models = mergeModelInfo(deployments, groups);
13
28
  const provider = buildProvider(models, resolved);
14
29
  const config = cfg;
15
30
  const providerV2 = buildProviderV2(models, resolved);
@@ -28,9 +43,11 @@ const LiteLLMPlugin = async (ctx, options) => {
28
43
  });
29
44
  }
30
45
  catch (error) {
46
+ const errorShape = error instanceof Error && "responseShape" in error ? error.responseShape : undefined;
31
47
  await log(ctx, resolved.strict ? "error" : "warn", "failed to load LiteLLM provider metadata", {
32
48
  providerKey: resolved.providerKey,
33
49
  error: error instanceof Error ? error.message : String(error),
50
+ ...(errorShape ? { responseShape: errorShape } : {}),
34
51
  });
35
52
  if (resolved.strict)
36
53
  throw error;
package/dist/litellm.d.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  import { type ResolvedOptions } from "./options.js";
2
2
  import type { LiteLLMModelInfo } from "./types.js";
3
3
  export declare function fetchModelInfo(options: ResolvedOptions, fetchImpl?: typeof fetch): Promise<LiteLLMModelInfo[]>;
4
+ export declare function normalizeModelGroupResponse(response: unknown): LiteLLMModelInfo[];
5
+ export declare function fetchModelGroupInfo(options: ResolvedOptions, fetchImpl?: typeof fetch): Promise<LiteLLMModelInfo[]>;
package/dist/litellm.js CHANGED
@@ -1,33 +1,69 @@
1
- import { modelInfoURL } from "./options.js";
2
- function isModelInfoArray(value) {
3
- return Array.isArray(value);
4
- }
5
- export async function fetchModelInfo(options, fetchImpl = fetch) {
1
+ import { modelListResponseShape, normalizeModelListResponse } from "./mapping.js";
2
+ import { modelGroupInfoURL, modelInfoURL } from "./options.js";
3
+ function requireCredentials(options) {
6
4
  if (!options.baseURL)
7
5
  throw new Error("opencode-litellm requires options.baseURL or LITELLM_BASE_URL");
8
6
  if (!options.apiKey)
9
7
  throw new Error("opencode-litellm requires options.apiKey or the configured apiKeyEnv");
8
+ }
9
+ async function fetchJSON(options, endpoint, url, fetchImpl) {
10
10
  const controller = new AbortController();
11
11
  const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
12
12
  try {
13
- const response = await fetchImpl(modelInfoURL(options.baseURL), {
14
- method: "GET",
15
- headers: {
16
- Authorization: `Bearer ${options.apiKey}`,
17
- Accept: "application/json",
18
- },
19
- signal: controller.signal,
20
- });
21
- if (!response.ok)
22
- throw new Error(`LiteLLM /model/info returned HTTP ${response.status}`);
23
- const body = (await response.json());
24
- if (isModelInfoArray(body))
25
- return body;
26
- if (isModelInfoArray(body.data))
27
- return body.data;
28
- throw new Error("LiteLLM /model/info response did not contain a data array");
13
+ try {
14
+ const response = await fetchImpl(url, {
15
+ method: "GET",
16
+ headers: {
17
+ Authorization: `Bearer ${options.apiKey}`,
18
+ Accept: "application/json",
19
+ },
20
+ signal: controller.signal,
21
+ });
22
+ if (!response.ok)
23
+ throw new Error(`LiteLLM ${endpoint} returned HTTP ${response.status}`);
24
+ return await response.json();
25
+ }
26
+ catch (error) {
27
+ if (error instanceof Error && error.message.startsWith(`LiteLLM ${endpoint}`))
28
+ throw error;
29
+ 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)}`);
32
+ }
29
33
  }
30
34
  finally {
31
35
  clearTimeout(timeout);
32
36
  }
33
37
  }
38
+ export async function fetchModelInfo(options, fetchImpl = fetch) {
39
+ requireCredentials(options);
40
+ return normalizeModelListResponse(await fetchJSON(options, "/model/info", modelInfoURL(options.baseURL), fetchImpl));
41
+ }
42
+ export function normalizeModelGroupResponse(response) {
43
+ const data = typeof response === "object" && response !== null && !Array.isArray(response)
44
+ ? response.data
45
+ : undefined;
46
+ if (!Array.isArray(data)) {
47
+ throw Object.assign(new Error("LiteLLM /model_group/info response did not contain a data array"), {
48
+ name: "LiteLLMModelGroupResponseError",
49
+ responseShape: modelListResponseShape(response),
50
+ });
51
+ }
52
+ return data
53
+ .filter((group) => typeof group === "object" &&
54
+ group !== null &&
55
+ !Array.isArray(group) &&
56
+ typeof group.model_group === "string" &&
57
+ Boolean(group.model_group?.trim()))
58
+ .map(({ model_group, supported_openai_params, ...metadata }) => ({
59
+ model_name: model_group,
60
+ model_info: {
61
+ ...metadata,
62
+ ...(supported_openai_params === undefined ? {} : { supported_params: supported_openai_params }),
63
+ },
64
+ }));
65
+ }
66
+ export async function fetchModelGroupInfo(options, fetchImpl = fetch) {
67
+ requireCredentials(options);
68
+ return normalizeModelGroupResponse(await fetchJSON(options, "/model_group/info", modelGroupInfoURL(options.baseURL), fetchImpl));
69
+ }
package/dist/mapping.d.ts CHANGED
@@ -1,9 +1,20 @@
1
1
  import type { LiteLLMModelInfo, LiteLLMPluginOptions, OpenCodeModel, OpenCodeModelV2, OpenCodeProvider, OpenCodeProviderV2 } from "./types.js";
2
- export declare function mapModels(models: LiteLLMModelInfo[], options?: LiteLLMPluginOptions): {
2
+ export type ModelListResponseShape = {
3
+ type: string;
4
+ length?: number;
5
+ keyCount?: number;
6
+ arrayFields?: string[];
7
+ objectValueCount?: number;
8
+ scalarValueCount?: number;
9
+ };
10
+ export declare function modelListResponseShape(value: unknown): ModelListResponseShape;
11
+ export declare function mergeModelInfo(deployments: LiteLLMModelInfo[], groups: LiteLLMModelInfo[]): LiteLLMModelInfo[];
12
+ export declare function normalizeModelListResponse(response: unknown): LiteLLMModelInfo[];
13
+ export declare function mapModels(response: unknown, options?: LiteLLMPluginOptions): {
3
14
  [k: string]: OpenCodeModel;
4
15
  };
5
- export declare function buildProvider(models: LiteLLMModelInfo[], options?: LiteLLMPluginOptions): OpenCodeProvider;
6
- export declare function mapModelsV2(models: LiteLLMModelInfo[], options?: LiteLLMPluginOptions): {
16
+ export declare function buildProvider(models: unknown, options?: LiteLLMPluginOptions): OpenCodeProvider;
17
+ export declare function mapModelsV2(models: unknown, options?: LiteLLMPluginOptions): {
7
18
  [k: string]: OpenCodeModelV2;
8
19
  };
9
- export declare function buildProviderV2(models: LiteLLMModelInfo[], options?: LiteLLMPluginOptions): OpenCodeProviderV2;
20
+ export declare function buildProviderV2(models: unknown, options?: LiteLLMPluginOptions): OpenCodeProviderV2;
package/dist/mapping.js CHANGED
@@ -2,7 +2,65 @@ import { deepMerge } from "./merge.js";
2
2
  const DEFAULT_MODES = new Set(["chat", "responses"]);
3
3
  const NON_CHAT_MODES = new Set(["embedding", "rerank"]);
4
4
  const TEXT_MODALITY = "text";
5
+ const OPENCODE_MODALITIES = new Set(["text", "audio", "image", "video", "pdf"]);
5
6
  const TEXT_ONLY_MODELS = new Set(["minimax-m2.7", "minimax_m2.7"]);
7
+ const NUMERIC_MODEL_INFO_FIELDS = new Set([
8
+ "input_cost_per_token",
9
+ "output_cost_per_token",
10
+ "cache_creation_input_token_cost",
11
+ "cache_read_input_token_cost",
12
+ "max_tokens",
13
+ "max_input_tokens",
14
+ "max_output_tokens",
15
+ ]);
16
+ const MODEL_LIST_RESPONSE_ERROR_MESSAGE = "LiteLLM model list response did not contain a model array";
17
+ const MODEL_ARRAY_FIELDS = ["data", "models", "model_info"];
18
+ const MODEL_INFO_FIELDS = new Set([
19
+ "id",
20
+ "base_model",
21
+ "mode",
22
+ "input_cost_per_token",
23
+ "output_cost_per_token",
24
+ "max_tokens",
25
+ "max_input_tokens",
26
+ "max_output_tokens",
27
+ "supports_function_calling",
28
+ "supports_tool_choice",
29
+ "supports_vision",
30
+ "supports_pdf_input",
31
+ "supports_reasoning",
32
+ "supported_modalities",
33
+ "output_modalities",
34
+ "supported_params",
35
+ ]);
36
+ function supportedModalities(values) {
37
+ return unique(values).filter((modality) => OPENCODE_MODALITIES.has(modality));
38
+ }
39
+ function isRecord(value) {
40
+ return typeof value === "object" && value !== null && !Array.isArray(value);
41
+ }
42
+ export function modelListResponseShape(value) {
43
+ if (Array.isArray(value))
44
+ return { type: "array", length: value.length };
45
+ if (value === null)
46
+ return { type: "null" };
47
+ if (!isRecord(value))
48
+ return { type: typeof value };
49
+ const values = Object.values(value);
50
+ return {
51
+ type: "object",
52
+ keyCount: Object.keys(value).length,
53
+ arrayFields: MODEL_ARRAY_FIELDS.filter((field) => Array.isArray(value[field])),
54
+ objectValueCount: values.filter(isRecord).length,
55
+ scalarValueCount: values.filter((entry) => !isRecord(entry) && !Array.isArray(entry)).length,
56
+ };
57
+ }
58
+ function modelListResponseError(response) {
59
+ return Object.assign(new Error(MODEL_LIST_RESPONSE_ERROR_MESSAGE), {
60
+ name: "LiteLLMModelListResponseError",
61
+ responseShape: modelListResponseShape(response),
62
+ });
63
+ }
6
64
  function asString(value) {
7
65
  return typeof value === "string" && value.trim() ? value : undefined;
8
66
  }
@@ -17,9 +75,85 @@ function perMillion(value) {
17
75
  function unique(values) {
18
76
  return Array.from(new Set(values));
19
77
  }
78
+ function isValidMetadata(value, key) {
79
+ if (value === undefined || value === null || value === "")
80
+ return false;
81
+ if (key && NUMERIC_MODEL_INFO_FIELDS.has(key))
82
+ return asNumber(value) !== undefined;
83
+ if (typeof value === "number")
84
+ return Number.isFinite(value) && value > 0;
85
+ if (Array.isArray(value))
86
+ return value.length > 0;
87
+ return true;
88
+ }
89
+ function enrichMetadata(existing, incoming, key) {
90
+ if (Array.isArray(existing) && Array.isArray(incoming))
91
+ return unique([...existing, ...incoming].filter(asString));
92
+ if (isRecord(existing) && isRecord(incoming)) {
93
+ const merged = { ...existing };
94
+ for (const [nestedKey, value] of Object.entries(incoming)) {
95
+ merged[nestedKey] = enrichMetadata(merged[nestedKey], value, nestedKey);
96
+ }
97
+ return merged;
98
+ }
99
+ if (typeof existing === "boolean" && typeof incoming === "boolean")
100
+ return existing || incoming;
101
+ if (isValidMetadata(existing, key))
102
+ return existing;
103
+ return isValidMetadata(incoming, key) ? incoming : existing;
104
+ }
20
105
  function modelName(model) {
21
106
  return asString(model.model_name) ?? asString(model.litellm_params?.model) ?? asString(model.model_info?.id);
22
107
  }
108
+ export function mergeModelInfo(deployments, groups) {
109
+ const byName = new Map();
110
+ for (const model of [...deployments, ...groups]) {
111
+ const name = modelName(model);
112
+ if (!name)
113
+ continue;
114
+ const existing = byName.get(name);
115
+ byName.set(name, existing ? enrichMetadata(existing, model) : model);
116
+ }
117
+ return Array.from(byName.values());
118
+ }
119
+ function hasModelInfoFields(value) {
120
+ return Object.keys(value).some((key) => MODEL_INFO_FIELDS.has(key));
121
+ }
122
+ function isPlausibleMappedModel(value) {
123
+ if (isRecord(value.model_info) && hasModelInfoFields(value.model_info))
124
+ return true;
125
+ if (isRecord(value.litellm_params) && typeof value.litellm_params.model === "string")
126
+ return true;
127
+ return hasModelInfoFields(value);
128
+ }
129
+ function normalizeMappedModel(name, value) {
130
+ if (!isRecord(value))
131
+ return undefined;
132
+ const model = value;
133
+ if (modelName(model))
134
+ return model;
135
+ if (!isPlausibleMappedModel(value))
136
+ return undefined;
137
+ return { ...model, model_name: name };
138
+ }
139
+ export function normalizeModelListResponse(response) {
140
+ if (Array.isArray(response))
141
+ return response;
142
+ if (isRecord(response)) {
143
+ for (const field of MODEL_ARRAY_FIELDS) {
144
+ if (Array.isArray(response[field]))
145
+ return response[field];
146
+ }
147
+ if (MODEL_ARRAY_FIELDS.some((field) => field in response))
148
+ throw modelListResponseError(response);
149
+ const mappedModels = Object.entries(response)
150
+ .map(([name, value]) => normalizeMappedModel(name, value))
151
+ .filter((model) => Boolean(model));
152
+ if (mappedModels.length > 0)
153
+ return mappedModels;
154
+ }
155
+ throw modelListResponseError(response);
156
+ }
23
157
  function mode(model) {
24
158
  return asString(model.model_info?.mode) ?? "chat";
25
159
  }
@@ -96,13 +230,13 @@ function modalities(model) {
96
230
  };
97
231
  }
98
232
  return {
99
- input: unique([
233
+ input: supportedModalities([
100
234
  TEXT_MODALITY,
101
235
  ...(model.model_info?.supported_modalities ?? []),
102
236
  ...(model.model_info?.supports_vision ? ["image"] : []),
103
237
  ...(model.model_info?.supports_pdf_input ? ["pdf"] : []),
104
238
  ]),
105
- output: unique([TEXT_MODALITY, ...(model.model_info?.output_modalities ?? [])]),
239
+ output: supportedModalities([TEXT_MODALITY, ...(model.model_info?.output_modalities ?? [])]),
106
240
  };
107
241
  }
108
242
  function supportsReasoning(model) {
@@ -127,15 +261,23 @@ function generatedModel(model) {
127
261
  ...(model.model_info?.supports_function_calling || model.model_info?.supports_tool_choice ? { tool_call: true } : {}),
128
262
  };
129
263
  }
264
+ function finalizedModel(model, override) {
265
+ const input = supportedModalities(model.modalities?.input ?? [TEXT_MODALITY]);
266
+ const output = supportedModalities(model.modalities?.output ?? [TEXT_MODALITY]);
267
+ const attachmentOverride = typeof override?.attachment === "boolean" ? override.attachment : undefined;
268
+ return {
269
+ ...model,
270
+ modalities: { input, output },
271
+ attachment: attachmentOverride ?? input.some((modality) => modality === "image" || modality === "pdf"),
272
+ };
273
+ }
130
274
  function v2Variants(model) {
131
275
  const variants = model.variants;
132
- if (!variants || typeof variants !== "object" || Array.isArray(variants))
276
+ if (!isRecord(variants))
133
277
  return undefined;
134
278
  return Object.entries(variants).map(([id, value]) => {
135
- const raw = value && typeof value === "object" && !Array.isArray(value) ? value : {};
136
- const body = raw.options && typeof raw.options === "object" && !Array.isArray(raw.options)
137
- ? raw.options
138
- : raw;
279
+ const raw = isRecord(value) ? { ...value } : {};
280
+ const body = isRecord(raw.options) ? { ...raw.options } : raw;
139
281
  if (body.reasoningEffort !== undefined && body.reasoning_effort === undefined) {
140
282
  body.reasoning_effort = body.reasoningEffort;
141
283
  delete body.reasoningEffort;
@@ -159,21 +301,26 @@ function v2Model(model, baseURL) {
159
301
  },
160
302
  capabilities: {
161
303
  tools: model.tool_call ?? false,
162
- input: model.modalities?.input ?? [TEXT_MODALITY],
163
- output: model.modalities?.output ?? [TEXT_MODALITY],
304
+ reasoning: model.reasoning ?? false,
305
+ attachment: model.attachment ?? false,
306
+ input: supportedModalities(model.modalities?.input ?? [TEXT_MODALITY]),
307
+ output: supportedModalities(model.modalities?.output ?? [TEXT_MODALITY]),
164
308
  },
165
309
  ...(v2Cost(model) ? { cost: v2Cost(model) } : {}),
166
310
  ...(model.limit ? { limit: { ...model.limit } } : {}),
167
311
  ...(v2Variants(model) ? { variants: v2Variants(model) } : {}),
168
312
  };
169
313
  }
170
- export function mapModels(models, options = {}) {
314
+ export function mapModels(response, options = {}) {
315
+ const models = normalizeModelListResponse(response);
171
316
  return Object.fromEntries(models
172
317
  .filter((model) => allowedByFilters(model, options))
173
318
  .map((model) => {
174
319
  const name = modelName(model);
175
320
  const generated = generatedModel(model);
176
- return name && generated ? [name, deepMerge(generated, options.overrides?.[name])] : undefined;
321
+ return name && generated
322
+ ? [name, finalizedModel(deepMerge(generated, options.overrides?.[name]), options.overrides?.[name])]
323
+ : undefined;
177
324
  })
178
325
  .filter((entry) => Boolean(entry)));
179
326
  }
@@ -196,7 +343,7 @@ export function mapModelsV2(models, options = {}) {
196
343
  return Object.fromEntries(Object.entries(legacy).map(([name, model]) => [name, v2Model(model, options.baseURL)]));
197
344
  }
198
345
  export function buildProviderV2(models, options = {}) {
199
- return {
346
+ return deepMerge({
200
347
  name: options.providerName ?? "LiteLLM",
201
348
  api: {
202
349
  type: "aisdk",
@@ -210,5 +357,5 @@ export function buildProviderV2(models, options = {}) {
210
357
  },
211
358
  },
212
359
  models: mapModelsV2(models, options),
213
- };
360
+ }, options.providerOverrides);
214
361
  }
package/dist/merge.d.ts CHANGED
File without changes
package/dist/merge.js CHANGED
File without changes
package/dist/options.d.ts CHANGED
@@ -2,3 +2,4 @@ import type { LiteLLMPluginOptions } from "./types.js";
2
2
  export type ResolvedOptions = Required<Pick<LiteLLMPluginOptions, "providerKey" | "providerName" | "timeoutMs" | "strict">> & Omit<LiteLLMPluginOptions, "providerKey" | "providerName" | "timeoutMs" | "strict">;
3
3
  export declare function resolveOptions(options?: LiteLLMPluginOptions): ResolvedOptions;
4
4
  export declare function modelInfoURL(baseURL: string): string;
5
+ export declare function modelGroupInfoURL(baseURL: string): string;
package/dist/options.js CHANGED
@@ -18,3 +18,6 @@ export function resolveOptions(options = {}) {
18
18
  export function modelInfoURL(baseURL) {
19
19
  return `${baseURL.replace(/\/+$/, "")}/model/info`;
20
20
  }
21
+ export function modelGroupInfoURL(baseURL) {
22
+ return new URL("/model_group/info", baseURL).toString();
23
+ }
package/dist/types.d.ts CHANGED
@@ -33,6 +33,8 @@ export type OpenCodeModelV2 = JsonObject & {
33
33
  };
34
34
  capabilities?: {
35
35
  tools: boolean;
36
+ reasoning: boolean;
37
+ attachment: boolean;
36
38
  input: string[];
37
39
  output: string[];
38
40
  };
@@ -123,8 +125,29 @@ export type LiteLLMModelInfo = JsonObject & {
123
125
  supported_params?: string[];
124
126
  };
125
127
  };
126
- export type LiteLLMModelInfoResponse = {
128
+ export type LiteLLMModelInfoResponse = LiteLLMModelInfo[] | {
127
129
  data?: LiteLLMModelInfo[];
130
+ models?: LiteLLMModelInfo[];
131
+ model_info?: LiteLLMModelInfo[];
132
+ };
133
+ export type LiteLLMModelGroup = JsonObject & {
134
+ model_group?: string;
135
+ mode?: string;
136
+ input_cost_per_token?: number | string;
137
+ output_cost_per_token?: number | string;
138
+ cache_creation_input_token_cost?: number | string;
139
+ cache_read_input_token_cost?: number | string;
140
+ max_tokens?: number | string;
141
+ max_input_tokens?: number | string;
142
+ max_output_tokens?: number | string;
143
+ supports_function_calling?: boolean;
144
+ supports_tool_choice?: boolean;
145
+ supports_vision?: boolean;
146
+ supports_pdf_input?: boolean;
147
+ supports_reasoning?: boolean;
148
+ supported_modalities?: string[];
149
+ output_modalities?: string[];
150
+ supported_openai_params?: string[];
128
151
  };
129
152
  export type OpenCodeConfig = {
130
153
  provider?: Record<string, OpenCodeProvider>;
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.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "OpenCode plugin that discovers LiteLLM models at runtime.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -23,7 +23,7 @@
23
23
  ],
24
24
  "scripts": {
25
25
  "build": "tsc -p tsconfig.json",
26
- "test": "node --test test/*.test.mjs",
26
+ "test": "node --test --test-concurrency=1 test/*.test.mjs",
27
27
  "prepack": "npm run build"
28
28
  },
29
29
  "peerDependencies": {