@staticeng/opencode-litellm 0.1.9 → 0.2.0
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 +0 -0
- package/README.md +20 -10
- package/dist/helpers.d.ts +0 -0
- package/dist/helpers.js +0 -0
- package/dist/index.d.ts +0 -0
- package/dist/index.js +7 -2
- package/dist/litellm.d.ts +0 -0
- package/dist/litellm.js +0 -0
- package/dist/mapping.d.ts +44 -1
- package/dist/mapping.js +73 -102
- package/dist/merge.d.ts +0 -0
- package/dist/merge.js +0 -0
- package/dist/model-contracts.d.ts +5 -0
- package/dist/model-contracts.js +82 -0
- package/dist/options.d.ts +0 -0
- package/dist/options.js +0 -0
- package/dist/types.d.ts +13 -0
- package/dist/types.js +0 -0
- package/package.json +1 -1
package/LICENSE
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ npm install @staticeng/opencode-litellm
|
|
|
15
15
|
"$schema": "https://opencode.ai/config.json",
|
|
16
16
|
"plugin": [
|
|
17
17
|
[
|
|
18
|
-
"@staticeng/opencode-litellm
|
|
18
|
+
"@staticeng/opencode-litellm",
|
|
19
19
|
{
|
|
20
20
|
"baseURL": "https://litellm.example.com/v1",
|
|
21
21
|
"apiKeyEnv": "LITELLM_API_KEY"
|
|
@@ -33,7 +33,7 @@ export LITELLM_API_KEY="..."
|
|
|
33
33
|
|
|
34
34
|
Do not put real API keys in `opencode.json`. If you pass `apiKey` directly, the plugin uses it only in memory and never logs it, but environment variables are recommended.
|
|
35
35
|
|
|
36
|
-
Shared or synchronized OpenCode configuration
|
|
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.
|
|
37
37
|
|
|
38
38
|
## Options
|
|
39
39
|
|
|
@@ -56,11 +56,9 @@ The LiteLLM key must have permission to call `GET /model/info` and root `GET /mo
|
|
|
56
56
|
|
|
57
57
|
## Overrides
|
|
58
58
|
|
|
59
|
-
LiteLLM metadata does not expose every OpenCode model field.
|
|
59
|
+
LiteLLM metadata does not expose every OpenCode model field. Version 0.2.0 includes exact built-in contracts for the currently approved GPT-5.4, GPT-5.5, GPT-5.6, GPT-5.3 Codex Spark, DeepSeek V4 Flash, and Qwen3.8 aliases. Use `overrides` to customize those defaults or to add metadata for unknown models.
|
|
60
60
|
|
|
61
|
-
When LiteLLM publishes `model_info.supported_reasoning_efforts`, the plugin maps
|
|
62
|
-
that list to OpenCode variants automatically. It does not guess effort levels
|
|
63
|
-
from model names; the LiteLLM catalog remains the source of truth.
|
|
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
62
|
|
|
65
63
|
```json
|
|
66
64
|
{
|
|
@@ -90,11 +88,22 @@ from model names; the LiteLLM catalog remains the source of truth.
|
|
|
90
88
|
}
|
|
91
89
|
```
|
|
92
90
|
|
|
93
|
-
Overrides win over generated values through a recursive deep merge. Arrays and scalar values are replaced rather than concatenated.
|
|
91
|
+
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.
|
|
92
|
+
|
|
93
|
+
The built-in mode matrix is:
|
|
94
|
+
|
|
95
|
+
| Contract | Modes in order | Default |
|
|
96
|
+
| --- | --- | --- |
|
|
97
|
+
| GPT-5.4 / GPT-5.4 Mini | `none`, `low`, `medium`, `high`, `xhigh` | `none` |
|
|
98
|
+
| GPT-5.5 | `none`, `low`, `medium`, `high`, `xhigh` | `medium` |
|
|
99
|
+
| GPT-5.6 Luna / Sol / Terra | `none`, `low`, `medium`, `high`, `xhigh`, `max` | `medium` |
|
|
100
|
+
| GPT-5.3 Codex Spark | `none`, `low`, `medium`, `high`, `xhigh` | `high` |
|
|
101
|
+
| DeepSeek V4 Flash | `off`, `low`, `high`, `max` | `max` |
|
|
102
|
+
| Qwen3.8 | `off`, `low`, `medium`, `xhigh` | `xhigh` |
|
|
94
103
|
|
|
95
|
-
|
|
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 default `xhigh` effort before serialization. The `variantDefault` compatibility marker is UI-only and is stripped before requests for exact built-in aliases.
|
|
96
105
|
|
|
97
|
-
|
|
106
|
+
Normal GPT-5.3 Codex aliases are retired and filtered even if stale discovery metadata still reports them. GPT-5.3 Codex Spark is a separate exact contract and remains available whenever discovered. Unlisted aliases such as `defend/gpt-5.5` do not inherit a contract.
|
|
98
107
|
|
|
99
108
|
## Runtime Behavior
|
|
100
109
|
|
|
@@ -103,7 +112,8 @@ The exact model group `qwen3.8-27b-refusal-dial` similarly exposes only `off`, `
|
|
|
103
112
|
- Adds or replaces both legacy `cfg.provider[providerKey]` and V2 `cfg.providers[providerKey]` during the config hook.
|
|
104
113
|
- Keeps namespaced deployment IDs and augments them with exact bare model-group IDs; exact collisions are enriched deterministically.
|
|
105
114
|
- Skips malformed group rows without a nonblank `model_group` and exposes reasoning capability on legacy and V2 model metadata.
|
|
106
|
-
-
|
|
115
|
+
- Applies exact built-in model contracts as defaults, then applies explicit model and provider overrides last.
|
|
116
|
+
- Filters retired normal GPT-5.3 Codex aliases without redirecting them; discovered Spark aliases remain independent.
|
|
107
117
|
- Warns safely and keeps deployment models when group discovery fails in non-strict mode; strict mode mutates neither config surface on failure.
|
|
108
118
|
- Does not write `opencode.json`, cache model metadata, or persist generated provider data.
|
|
109
119
|
- Emits the required `limit.output` as `0` when LiteLLM reports a context/input limit but no output
|
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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fetchModelGroupInfo, fetchModelInfo } from "./litellm.js";
|
|
2
|
-
import {
|
|
2
|
+
import { buildProvider, buildProviderV2, isContractReasoningModel, isQwen38ReasoningModel, 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 } });
|
|
@@ -25,7 +25,7 @@ const LiteLLMPlugin = async (ctx, options) => {
|
|
|
25
25
|
throw error;
|
|
26
26
|
}
|
|
27
27
|
const models = mergeModelInfo(deployments, groups);
|
|
28
|
-
const provider =
|
|
28
|
+
const provider = buildProvider(models, resolved);
|
|
29
29
|
const config = cfg;
|
|
30
30
|
const providerV2 = buildProviderV2(models, resolved);
|
|
31
31
|
cfg.provider = {
|
|
@@ -60,6 +60,11 @@ const LiteLLMPlugin = async (ctx, options) => {
|
|
|
60
60
|
if (!isContractReasoningModel(input.model.id))
|
|
61
61
|
return;
|
|
62
62
|
delete output.options.variantDefault;
|
|
63
|
+
const selectedVariant = input.message?.model?.variant;
|
|
64
|
+
if (isQwen38ReasoningModel(input.model.id) && selectedVariant === "off") {
|
|
65
|
+
delete output.options.reasoningEffort;
|
|
66
|
+
delete output.options.reasoning_effort;
|
|
67
|
+
}
|
|
63
68
|
}),
|
|
64
69
|
};
|
|
65
70
|
};
|
package/dist/litellm.d.ts
CHANGED
|
File without changes
|
package/dist/litellm.js
CHANGED
|
File without changes
|
package/dist/mapping.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare function modelListResponseShape(value: unknown): ModelListRespons
|
|
|
11
11
|
export declare function mergeModelInfo(deployments: LiteLLMModelInfo[], groups: LiteLLMModelInfo[]): LiteLLMModelInfo[];
|
|
12
12
|
export declare function normalizeModelListResponse(response: unknown): LiteLLMModelInfo[];
|
|
13
13
|
export declare function isDeepSeekV4ReasoningModel(name: string | undefined): boolean;
|
|
14
|
+
export declare function isQwen38ReasoningModel(name: string | undefined): boolean;
|
|
14
15
|
export declare function isContractReasoningModel(name: string | undefined): boolean;
|
|
15
16
|
export declare function addOpenCodeVariantTombstones(provider: OpenCodeProvider): OpenCodeProvider;
|
|
16
17
|
export declare function mapModels(response: unknown, options?: LiteLLMPluginOptions): {
|
|
@@ -18,6 +19,48 @@ export declare function mapModels(response: unknown, options?: LiteLLMPluginOpti
|
|
|
18
19
|
};
|
|
19
20
|
export declare function buildProvider(models: unknown, options?: LiteLLMPluginOptions): OpenCodeProvider;
|
|
20
21
|
export declare function mapModelsV2(models: unknown, options?: LiteLLMPluginOptions): {
|
|
21
|
-
[k: string]: OpenCodeModelV2
|
|
22
|
+
[k: string]: OpenCodeModelV2 | {
|
|
23
|
+
variants: {
|
|
24
|
+
id: string;
|
|
25
|
+
body: {
|
|
26
|
+
[x: string]: unknown;
|
|
27
|
+
};
|
|
28
|
+
}[];
|
|
29
|
+
family?: string;
|
|
30
|
+
name?: string;
|
|
31
|
+
api?: {
|
|
32
|
+
id?: string;
|
|
33
|
+
type: "aisdk";
|
|
34
|
+
package: string;
|
|
35
|
+
url?: string;
|
|
36
|
+
settings?: import("./types.js").JsonObject;
|
|
37
|
+
};
|
|
38
|
+
capabilities?: {
|
|
39
|
+
tools: boolean;
|
|
40
|
+
reasoning: boolean;
|
|
41
|
+
attachment: boolean;
|
|
42
|
+
input: string[];
|
|
43
|
+
output: string[];
|
|
44
|
+
};
|
|
45
|
+
request?: {
|
|
46
|
+
headers?: Record<string, string>;
|
|
47
|
+
body?: import("./types.js").JsonObject;
|
|
48
|
+
variant?: string;
|
|
49
|
+
};
|
|
50
|
+
cost?: {
|
|
51
|
+
input: number;
|
|
52
|
+
output: number;
|
|
53
|
+
cache?: {
|
|
54
|
+
read?: number;
|
|
55
|
+
write?: number;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
disabled?: boolean;
|
|
59
|
+
limit?: {
|
|
60
|
+
context?: number;
|
|
61
|
+
input?: number;
|
|
62
|
+
output?: number;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
22
65
|
};
|
|
23
66
|
export declare function buildProviderV2(models: unknown, options?: LiteLLMPluginOptions): OpenCodeProviderV2;
|
package/dist/mapping.js
CHANGED
|
@@ -1,38 +1,10 @@
|
|
|
1
1
|
import { deepMerge } from "./merge.js";
|
|
2
|
+
import { isRetiredModelAlias, modelContract } from "./model-contracts.js";
|
|
2
3
|
const DEFAULT_MODES = new Set(["chat", "responses"]);
|
|
3
4
|
const NON_CHAT_MODES = new Set(["embedding", "rerank"]);
|
|
4
5
|
const TEXT_MODALITY = "text";
|
|
5
6
|
const OPENCODE_MODALITIES = new Set(["text", "audio", "image", "video", "pdf"]);
|
|
6
7
|
const TEXT_ONLY_MODELS = new Set(["minimax-m2.7", "minimax_m2.7"]);
|
|
7
|
-
const DEEPSEEK_V4_REASONING_MODELS = new Set([
|
|
8
|
-
"deepseek-v4-flash-fp8-mtp",
|
|
9
|
-
"deepseek-v4-flash-fp8-mtp-norefusal",
|
|
10
|
-
]);
|
|
11
|
-
const QWEN_38_REASONING_MODELS = new Set(["qwen3.8-27b-refusal-dial"]);
|
|
12
|
-
const DEEPSEEK_V4_VARIANTS = {
|
|
13
|
-
off: { reasoningEffort: "none" },
|
|
14
|
-
low: { reasoningEffort: "low" },
|
|
15
|
-
high: { reasoningEffort: "high" },
|
|
16
|
-
max: { reasoningEffort: "max" },
|
|
17
|
-
};
|
|
18
|
-
const DEEPSEEK_V4_VARIANTS_V2 = [
|
|
19
|
-
{ id: "off", body: { reasoning_effort: "none" } },
|
|
20
|
-
{ id: "low", body: { reasoning_effort: "low" } },
|
|
21
|
-
{ id: "high", body: { reasoning_effort: "high" } },
|
|
22
|
-
{ id: "max", body: { reasoning_effort: "max" } },
|
|
23
|
-
];
|
|
24
|
-
const QWEN_38_VARIANTS = {
|
|
25
|
-
off: { chat_template_kwargs: { enable_thinking: false } },
|
|
26
|
-
low: { reasoningEffort: "low" },
|
|
27
|
-
medium: { reasoningEffort: "medium" },
|
|
28
|
-
xhigh: { reasoningEffort: "xhigh" },
|
|
29
|
-
};
|
|
30
|
-
const QWEN_38_VARIANTS_V2 = [
|
|
31
|
-
{ id: "off", body: { chat_template_kwargs: { enable_thinking: false } } },
|
|
32
|
-
{ id: "low", body: { reasoning_effort: "low" } },
|
|
33
|
-
{ id: "medium", body: { reasoning_effort: "medium" } },
|
|
34
|
-
{ id: "xhigh", body: { reasoning_effort: "xhigh" } },
|
|
35
|
-
];
|
|
36
8
|
const REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
|
|
37
9
|
const REASONING_EFFORT_SET = new Set(REASONING_EFFORTS);
|
|
38
10
|
const NUMERIC_MODEL_INFO_FIELDS = new Set([
|
|
@@ -195,6 +167,8 @@ function allowedByFilters(model, options) {
|
|
|
195
167
|
const name = modelName(model);
|
|
196
168
|
if (!name)
|
|
197
169
|
return false;
|
|
170
|
+
if (isRetiredModelAlias(name))
|
|
171
|
+
return false;
|
|
198
172
|
if (options.includeModels?.length && !options.includeModels.includes(name))
|
|
199
173
|
return false;
|
|
200
174
|
if (options.excludeModels?.includes(name))
|
|
@@ -280,29 +254,35 @@ function supportsReasoning(model) {
|
|
|
280
254
|
model.model_info?.supported_params?.includes("thinking"));
|
|
281
255
|
}
|
|
282
256
|
export function isDeepSeekV4ReasoningModel(name) {
|
|
283
|
-
return name
|
|
257
|
+
return modelContract(name)?.family === "DeepSeek V4 Flash";
|
|
258
|
+
}
|
|
259
|
+
export function isQwen38ReasoningModel(name) {
|
|
260
|
+
return modelContract(name)?.family === "Qwen3.8";
|
|
284
261
|
}
|
|
285
262
|
export function isContractReasoningModel(name) {
|
|
286
|
-
return
|
|
263
|
+
return modelContract(name) !== undefined;
|
|
287
264
|
}
|
|
288
|
-
function
|
|
289
|
-
const
|
|
290
|
-
if (!
|
|
291
|
-
return
|
|
265
|
+
function contractDefaults(name, tombstones = false) {
|
|
266
|
+
const entry = modelContract(name);
|
|
267
|
+
if (!entry)
|
|
268
|
+
return undefined;
|
|
269
|
+
const defaultMode = entry.modes.find((mode) => mode.id === entry.defaultMode);
|
|
270
|
+
if (!defaultMode)
|
|
271
|
+
throw new Error(`Model contract ${entry.family} has no default mode body`);
|
|
272
|
+
const variants = Object.fromEntries(entry.modes.map((mode) => [mode.id, { ...mode.legacy }]));
|
|
273
|
+
if (tombstones) {
|
|
274
|
+
const disabled = entry.family === "DeepSeek V4 Flash"
|
|
275
|
+
? ["medium", "xhigh"]
|
|
276
|
+
: entry.family === "Qwen3.8"
|
|
277
|
+
? ["none", "minimal", "high", "max"]
|
|
278
|
+
: [];
|
|
279
|
+
for (const effort of disabled)
|
|
280
|
+
variants[effort] = { disabled: true };
|
|
281
|
+
}
|
|
292
282
|
return {
|
|
293
|
-
...model,
|
|
294
283
|
reasoning: true,
|
|
295
|
-
options: { ...
|
|
296
|
-
variants
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
function enforceReasoningProviderModels(provider) {
|
|
300
|
-
return {
|
|
301
|
-
...provider,
|
|
302
|
-
models: Object.fromEntries(Object.entries(provider.models).map(([name, model]) => [
|
|
303
|
-
name,
|
|
304
|
-
isContractReasoningModel(name) ? enforceReasoningVariants({ ...model, id: name }) : model,
|
|
305
|
-
])),
|
|
284
|
+
options: { ...defaultMode.legacy, variantDefault: entry.variantDefault },
|
|
285
|
+
variants,
|
|
306
286
|
};
|
|
307
287
|
}
|
|
308
288
|
export function addOpenCodeVariantTombstones(provider) {
|
|
@@ -310,22 +290,7 @@ export function addOpenCodeVariantTombstones(provider) {
|
|
|
310
290
|
...provider,
|
|
311
291
|
models: Object.fromEntries(Object.entries(provider.models).map(([name, model]) => [
|
|
312
292
|
name,
|
|
313
|
-
isContractReasoningModel(name)
|
|
314
|
-
? {
|
|
315
|
-
...model,
|
|
316
|
-
variants: {
|
|
317
|
-
...(isDeepSeekV4ReasoningModel(name) ? DEEPSEEK_V4_VARIANTS : QWEN_38_VARIANTS),
|
|
318
|
-
...(isDeepSeekV4ReasoningModel(name)
|
|
319
|
-
? { medium: { disabled: true }, xhigh: { disabled: true } }
|
|
320
|
-
: {
|
|
321
|
-
none: { disabled: true },
|
|
322
|
-
minimal: { disabled: true },
|
|
323
|
-
high: { disabled: true },
|
|
324
|
-
max: { disabled: true },
|
|
325
|
-
}),
|
|
326
|
-
},
|
|
327
|
-
}
|
|
328
|
-
: model,
|
|
293
|
+
isContractReasoningModel(name) ? deepMerge(contractDefaults(name, true) ?? {}, model) : model,
|
|
329
294
|
])),
|
|
330
295
|
};
|
|
331
296
|
}
|
|
@@ -338,12 +303,12 @@ function reasoningVariants(model) {
|
|
|
338
303
|
return undefined;
|
|
339
304
|
return Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }]));
|
|
340
305
|
}
|
|
341
|
-
function generatedModel(model) {
|
|
306
|
+
function generatedModel(model, tombstones = false) {
|
|
342
307
|
const name = modelName(model);
|
|
343
308
|
if (!name)
|
|
344
309
|
return undefined;
|
|
345
310
|
const mappedModalities = modalities(model);
|
|
346
|
-
|
|
311
|
+
const generated = {
|
|
347
312
|
id: name,
|
|
348
313
|
name,
|
|
349
314
|
...(asString(model.model_info?.base_model) ? { family: asString(model.model_info?.base_model) } : {}),
|
|
@@ -355,6 +320,7 @@ function generatedModel(model) {
|
|
|
355
320
|
...(supportsReasoning(model) && reasoningVariants(model) ? { variants: reasoningVariants(model) } : {}),
|
|
356
321
|
...(model.model_info?.supports_function_calling || model.model_info?.supports_tool_choice ? { tool_call: true } : {}),
|
|
357
322
|
};
|
|
323
|
+
return deepMerge(generated, contractDefaults(name, tombstones));
|
|
358
324
|
}
|
|
359
325
|
function finalizedModel(model, override) {
|
|
360
326
|
const input = supportedModalities(model.modalities?.input ?? [TEXT_MODALITY]);
|
|
@@ -366,19 +332,37 @@ function finalizedModel(model, override) {
|
|
|
366
332
|
attachment: attachmentOverride ?? input.some((modality) => modality === "image" || modality === "pdf"),
|
|
367
333
|
};
|
|
368
334
|
}
|
|
335
|
+
function v2VariantBody(value) {
|
|
336
|
+
const raw = isRecord(value) ? { ...value } : {};
|
|
337
|
+
const body = isRecord(raw.options) ? { ...raw.options } : raw;
|
|
338
|
+
if (body.reasoningEffort !== undefined && body.reasoning_effort === undefined) {
|
|
339
|
+
body.reasoning_effort = body.reasoningEffort;
|
|
340
|
+
delete body.reasoningEffort;
|
|
341
|
+
}
|
|
342
|
+
return body;
|
|
343
|
+
}
|
|
369
344
|
function v2Variants(model) {
|
|
370
345
|
const variants = model.variants;
|
|
371
346
|
if (!isRecord(variants))
|
|
372
347
|
return undefined;
|
|
373
|
-
return Object.entries(variants).map(([id, value]) => {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
348
|
+
return Object.entries(variants).map(([id, value]) => ({ id, body: v2VariantBody(value) }));
|
|
349
|
+
}
|
|
350
|
+
function contractV2Variants(name, override) {
|
|
351
|
+
const entry = modelContract(name);
|
|
352
|
+
if (!entry)
|
|
353
|
+
return undefined;
|
|
354
|
+
const variants = entry.modes.map((mode) => ({ id: mode.id, body: { ...mode.v2Body } }));
|
|
355
|
+
if (!isRecord(override?.variants))
|
|
356
|
+
return variants;
|
|
357
|
+
const byID = new Map(variants.map((variant) => [variant.id, variant]));
|
|
358
|
+
for (const [id, value] of Object.entries(override.variants)) {
|
|
359
|
+
const current = byID.get(id);
|
|
360
|
+
byID.set(id, {
|
|
361
|
+
id,
|
|
362
|
+
body: deepMerge(current?.body ?? {}, v2VariantBody(value)),
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
return Array.from(byID.values());
|
|
382
366
|
}
|
|
383
367
|
function v2Model(model, baseURL) {
|
|
384
368
|
const id = asString(model.id) ?? asString(model.name);
|
|
@@ -407,24 +391,27 @@ function v2Model(model, baseURL) {
|
|
|
407
391
|
...(v2Variants(model) ? { variants: v2Variants(model) } : {}),
|
|
408
392
|
};
|
|
409
393
|
}
|
|
410
|
-
|
|
394
|
+
function mapModelsInternal(response, options, tombstones) {
|
|
411
395
|
const models = normalizeModelListResponse(response);
|
|
412
396
|
return Object.fromEntries(models
|
|
413
397
|
.filter((model) => allowedByFilters(model, options))
|
|
414
398
|
.map((model) => {
|
|
415
399
|
const name = modelName(model);
|
|
416
|
-
const generated = generatedModel(model);
|
|
400
|
+
const generated = generatedModel(model, tombstones);
|
|
417
401
|
return name && generated
|
|
418
402
|
? [
|
|
419
403
|
name,
|
|
420
|
-
|
|
404
|
+
finalizedModel(deepMerge(generated, options.overrides?.[name]), options.overrides?.[name]),
|
|
421
405
|
]
|
|
422
406
|
: undefined;
|
|
423
407
|
})
|
|
424
408
|
.filter((entry) => Boolean(entry)));
|
|
425
409
|
}
|
|
410
|
+
export function mapModels(response, options = {}) {
|
|
411
|
+
return mapModelsInternal(response, options, false);
|
|
412
|
+
}
|
|
426
413
|
export function buildProvider(models, options = {}) {
|
|
427
|
-
return
|
|
414
|
+
return deepMerge({
|
|
428
415
|
id: options.providerKey ?? "LiteLLM",
|
|
429
416
|
name: options.providerName ?? "LiteLLM",
|
|
430
417
|
npm: "@ai-sdk/openai-compatible",
|
|
@@ -434,15 +421,19 @@ export function buildProvider(models, options = {}) {
|
|
|
434
421
|
apiKey: options.apiKey,
|
|
435
422
|
...(options.apiKey ? { body: { apiKey: options.apiKey } } : {}),
|
|
436
423
|
},
|
|
437
|
-
models:
|
|
438
|
-
}, options.providerOverrides)
|
|
424
|
+
models: mapModelsInternal(models, options, true),
|
|
425
|
+
}, options.providerOverrides);
|
|
439
426
|
}
|
|
440
427
|
export function mapModelsV2(models, options = {}) {
|
|
441
428
|
const legacy = mapModels(models, options);
|
|
442
|
-
return Object.fromEntries(Object.entries(legacy).map(([name, model]) =>
|
|
429
|
+
return Object.fromEntries(Object.entries(legacy).map(([name, model]) => {
|
|
430
|
+
const mapped = v2Model(model, options.baseURL);
|
|
431
|
+
const variants = contractV2Variants(name, options.overrides?.[name]);
|
|
432
|
+
return [name, variants ? { ...mapped, variants } : mapped];
|
|
433
|
+
}));
|
|
443
434
|
}
|
|
444
435
|
export function buildProviderV2(models, options = {}) {
|
|
445
|
-
|
|
436
|
+
return deepMerge({
|
|
446
437
|
name: options.providerName ?? "LiteLLM",
|
|
447
438
|
api: {
|
|
448
439
|
type: "aisdk",
|
|
@@ -457,24 +448,4 @@ export function buildProviderV2(models, options = {}) {
|
|
|
457
448
|
},
|
|
458
449
|
models: mapModelsV2(models, options),
|
|
459
450
|
}, options.providerOverrides);
|
|
460
|
-
return {
|
|
461
|
-
...provider,
|
|
462
|
-
models: Object.fromEntries(Object.entries(provider.models).map(([name, model]) => [
|
|
463
|
-
name,
|
|
464
|
-
isContractReasoningModel(name)
|
|
465
|
-
? {
|
|
466
|
-
...model,
|
|
467
|
-
capabilities: {
|
|
468
|
-
tools: model.capabilities?.tools ?? false,
|
|
469
|
-
reasoning: true,
|
|
470
|
-
attachment: model.capabilities?.attachment ?? false,
|
|
471
|
-
input: model.capabilities?.input ?? [TEXT_MODALITY],
|
|
472
|
-
output: model.capabilities?.output ?? [TEXT_MODALITY],
|
|
473
|
-
},
|
|
474
|
-
options: { ...(isRecord(model.options) ? model.options : {}), variantDefault: false },
|
|
475
|
-
variants: isDeepSeekV4ReasoningModel(name) ? DEEPSEEK_V4_VARIANTS_V2 : QWEN_38_VARIANTS_V2,
|
|
476
|
-
}
|
|
477
|
-
: model,
|
|
478
|
-
])),
|
|
479
|
-
};
|
|
480
451
|
}
|
package/dist/merge.d.ts
CHANGED
|
File without changes
|
package/dist/merge.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ModelContract } from "./types.js";
|
|
2
|
+
export declare const MODEL_CONTRACTS: readonly ModelContract[];
|
|
3
|
+
export declare const RETIRED_MODEL_ALIASES: readonly string[];
|
|
4
|
+
export declare function modelContract(name: string | undefined): ModelContract | undefined;
|
|
5
|
+
export declare function isRetiredModelAlias(name: string | undefined): boolean;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
function freezeJson(value) {
|
|
2
|
+
for (const nested of Object.values(value)) {
|
|
3
|
+
if (nested && typeof nested === "object" && !Object.isFrozen(nested))
|
|
4
|
+
Object.freeze(nested);
|
|
5
|
+
}
|
|
6
|
+
return Object.freeze(value);
|
|
7
|
+
}
|
|
8
|
+
function effortModes(ids) {
|
|
9
|
+
return Object.freeze(ids.map((id) => Object.freeze({
|
|
10
|
+
id,
|
|
11
|
+
legacy: freezeJson({ reasoningEffort: id }),
|
|
12
|
+
v2Body: freezeJson({ reasoning_effort: id }),
|
|
13
|
+
})));
|
|
14
|
+
}
|
|
15
|
+
function contract(family, aliases, modes, defaultMode) {
|
|
16
|
+
if (!modes.some((mode) => mode.id === defaultMode))
|
|
17
|
+
throw new Error(`Model contract ${family} has an invalid default`);
|
|
18
|
+
return Object.freeze({
|
|
19
|
+
family,
|
|
20
|
+
aliases: Object.freeze([...aliases]),
|
|
21
|
+
modes,
|
|
22
|
+
defaultMode,
|
|
23
|
+
reasoning: true,
|
|
24
|
+
variantDefault: false,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const GPT_STANDARD_MODES = effortModes(["none", "low", "medium", "high", "xhigh"]);
|
|
28
|
+
const GPT_MAX_MODES = effortModes(["none", "low", "medium", "high", "xhigh", "max"]);
|
|
29
|
+
const DEEPSEEK_MODES = Object.freeze([
|
|
30
|
+
Object.freeze({ id: "off", legacy: freezeJson({ reasoningEffort: "none" }), v2Body: freezeJson({ reasoning_effort: "none" }) }),
|
|
31
|
+
Object.freeze({ id: "low", legacy: freezeJson({ reasoningEffort: "low" }), v2Body: freezeJson({ reasoning_effort: "low" }) }),
|
|
32
|
+
Object.freeze({ id: "high", legacy: freezeJson({ reasoningEffort: "high" }), v2Body: freezeJson({ reasoning_effort: "high" }) }),
|
|
33
|
+
Object.freeze({ id: "max", legacy: freezeJson({ reasoningEffort: "max" }), v2Body: freezeJson({ reasoning_effort: "max" }) }),
|
|
34
|
+
]);
|
|
35
|
+
const qwenOff = Object.freeze({ enable_thinking: false });
|
|
36
|
+
const QWEN_MODES = Object.freeze([
|
|
37
|
+
Object.freeze({
|
|
38
|
+
id: "off",
|
|
39
|
+
legacy: freezeJson({ chat_template_kwargs: qwenOff }),
|
|
40
|
+
v2Body: freezeJson({ chat_template_kwargs: qwenOff }),
|
|
41
|
+
}),
|
|
42
|
+
Object.freeze({ id: "low", legacy: freezeJson({ reasoningEffort: "low" }), v2Body: freezeJson({ reasoning_effort: "low" }) }),
|
|
43
|
+
Object.freeze({ id: "medium", legacy: freezeJson({ reasoningEffort: "medium" }), v2Body: freezeJson({ reasoning_effort: "medium" }) }),
|
|
44
|
+
Object.freeze({ id: "xhigh", legacy: freezeJson({ reasoningEffort: "xhigh" }), v2Body: freezeJson({ reasoning_effort: "xhigh" }) }),
|
|
45
|
+
]);
|
|
46
|
+
const chatGptAliases = (model) => Object.freeze([
|
|
47
|
+
model,
|
|
48
|
+
`chatgpt/${model}`,
|
|
49
|
+
`chatgpt-account2/${model}`,
|
|
50
|
+
`chatgpt-account3/${model}`,
|
|
51
|
+
]);
|
|
52
|
+
export const MODEL_CONTRACTS = Object.freeze([
|
|
53
|
+
contract("GPT-5.4", chatGptAliases("gpt-5.4"), GPT_STANDARD_MODES, "none"),
|
|
54
|
+
contract("GPT-5.4 Mini", chatGptAliases("gpt-5.4-mini"), GPT_STANDARD_MODES, "none"),
|
|
55
|
+
contract("GPT-5.5", chatGptAliases("gpt-5.5"), GPT_STANDARD_MODES, "medium"),
|
|
56
|
+
contract("GPT-5.6 Luna", chatGptAliases("gpt-5.6-luna"), GPT_MAX_MODES, "medium"),
|
|
57
|
+
contract("GPT-5.6 Sol", chatGptAliases("gpt-5.6-sol"), GPT_MAX_MODES, "medium"),
|
|
58
|
+
contract("GPT-5.6 Terra", chatGptAliases("gpt-5.6-terra"), GPT_MAX_MODES, "medium"),
|
|
59
|
+
contract("GPT-5.3 Codex Spark", chatGptAliases("gpt-5.3-codex-spark"), GPT_STANDARD_MODES, "high"),
|
|
60
|
+
contract("DeepSeek V4 Flash", ["deepseek-v4-flash-fp8-mtp", "deepseek-v4-flash-fp8-mtp-norefusal"], DEEPSEEK_MODES, "max"),
|
|
61
|
+
contract("Qwen3.8", ["qwen3.8-27b-refusal-dial"], QWEN_MODES, "xhigh"),
|
|
62
|
+
]);
|
|
63
|
+
export const RETIRED_MODEL_ALIASES = Object.freeze(chatGptAliases("gpt-5.3-codex"));
|
|
64
|
+
const contractsByAlias = new Map();
|
|
65
|
+
for (const entry of MODEL_CONTRACTS) {
|
|
66
|
+
for (const alias of entry.aliases) {
|
|
67
|
+
if (contractsByAlias.has(alias))
|
|
68
|
+
throw new Error(`Duplicate model contract alias: ${alias}`);
|
|
69
|
+
contractsByAlias.set(alias, entry);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const retiredAliases = new Set(RETIRED_MODEL_ALIASES);
|
|
73
|
+
for (const alias of retiredAliases) {
|
|
74
|
+
if (contractsByAlias.has(alias))
|
|
75
|
+
throw new Error(`Active and retired model contract alias collision: ${alias}`);
|
|
76
|
+
}
|
|
77
|
+
export function modelContract(name) {
|
|
78
|
+
return name === undefined ? undefined : contractsByAlias.get(name);
|
|
79
|
+
}
|
|
80
|
+
export function isRetiredModelAlias(name) {
|
|
81
|
+
return name !== undefined && retiredAliases.has(name);
|
|
82
|
+
}
|
package/dist/options.d.ts
CHANGED
|
File without changes
|
package/dist/options.js
CHANGED
|
File without changes
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
export type JsonObject = Record<string, unknown>;
|
|
2
|
+
export type ModelContractMode = Readonly<{
|
|
3
|
+
id: string;
|
|
4
|
+
legacy: Readonly<JsonObject>;
|
|
5
|
+
v2Body: Readonly<JsonObject>;
|
|
6
|
+
}>;
|
|
7
|
+
export type ModelContract = Readonly<{
|
|
8
|
+
family: string;
|
|
9
|
+
aliases: readonly string[];
|
|
10
|
+
modes: readonly ModelContractMode[];
|
|
11
|
+
defaultMode: string;
|
|
12
|
+
reasoning: true;
|
|
13
|
+
variantDefault: false;
|
|
14
|
+
}>;
|
|
2
15
|
export type OpenCodeModel = JsonObject & {
|
|
3
16
|
id?: string;
|
|
4
17
|
name?: string;
|
package/dist/types.js
CHANGED
|
File without changes
|