@staticeng/opencode-litellm 0.1.8 → 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 +31 -3
- package/dist/helpers.d.ts +1 -1
- package/dist/helpers.js +1 -1
- package/dist/index.d.ts +13 -0
- package/dist/index.js +14 -1
- package/dist/litellm.d.ts +0 -0
- package/dist/litellm.js +0 -0
- package/dist/mapping.d.ts +47 -1
- package/dist/mapping.js +110 -20
- 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 +19 -0
- package/dist/types.js +0 -0
- package/package.json +1 -1
package/LICENSE
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
@@ -33,6 +33,8 @@ 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 should use the published package name as shown above. Do not use a local path or `file://` plugin reference for shared configuration.
|
|
37
|
+
|
|
36
38
|
## Options
|
|
37
39
|
|
|
38
40
|
| Option | Default | Purpose |
|
|
@@ -54,7 +56,9 @@ The LiteLLM key must have permission to call `GET /model/info` and root `GET /mo
|
|
|
54
56
|
|
|
55
57
|
## Overrides
|
|
56
58
|
|
|
57
|
-
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
|
+
|
|
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.
|
|
58
62
|
|
|
59
63
|
```json
|
|
60
64
|
{
|
|
@@ -68,7 +72,7 @@ LiteLLM metadata does not expose every OpenCode model field. Use `overrides` to
|
|
|
68
72
|
"chatgpt/gpt-5.5": {
|
|
69
73
|
"limit": { "context": 262144, "output": 128000 },
|
|
70
74
|
"variants": {
|
|
71
|
-
"high": { "
|
|
75
|
+
"high": { "reasoningEffort": "high" }
|
|
72
76
|
},
|
|
73
77
|
"interleaved": { "field": "reasoning_details" }
|
|
74
78
|
}
|
|
@@ -84,7 +88,22 @@ LiteLLM metadata does not expose every OpenCode model field. Use `overrides` to
|
|
|
84
88
|
}
|
|
85
89
|
```
|
|
86
90
|
|
|
87
|
-
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` |
|
|
103
|
+
|
|
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.
|
|
105
|
+
|
|
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.
|
|
88
107
|
|
|
89
108
|
## Runtime Behavior
|
|
90
109
|
|
|
@@ -93,8 +112,12 @@ Overrides win over generated values through a recursive deep merge. Arrays and s
|
|
|
93
112
|
- Adds or replaces both legacy `cfg.provider[providerKey]` and V2 `cfg.providers[providerKey]` during the config hook.
|
|
94
113
|
- Keeps namespaced deployment IDs and augments them with exact bare model-group IDs; exact collisions are enriched deterministically.
|
|
95
114
|
- Skips malformed group rows without a nonblank `model_group` and exposes reasoning capability on legacy and V2 model metadata.
|
|
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.
|
|
96
117
|
- Warns safely and keeps deployment models when group discovery fails in non-strict mode; strict mode mutates neither config surface on failure.
|
|
97
118
|
- Does not write `opencode.json`, cache model metadata, or persist generated provider data.
|
|
119
|
+
- Emits the required `limit.output` as `0` when LiteLLM reports a context/input limit but no output
|
|
120
|
+
limit. The zero sentinel retains known metadata without inventing a positive output allowance.
|
|
98
121
|
- Includes `chat` and `responses` modes by default; non-chat modes such as `embedding` and `rerank` are excluded unless explicitly included.
|
|
99
122
|
- No HuggingFace fallback is included in v1.
|
|
100
123
|
|
|
@@ -110,3 +133,8 @@ npm pack --dry-run
|
|
|
110
133
|
```
|
|
111
134
|
|
|
112
135
|
Confirm package contents before publishing and keep secrets out of fixtures, docs, logs, and config examples.
|
|
136
|
+
|
|
137
|
+
Publishing uses npm trusted publishing from the `npm-production` GitHub environment. Provenance
|
|
138
|
+
requires this GitHub repository to remain public. Before a release, configure npm's trusted
|
|
139
|
+
publisher for package `@staticeng/opencode-litellm` with this repository, workflow
|
|
140
|
+
`publish.yml`, and environment `npm-production`; no npm token is used by the workflow.
|
package/dist/helpers.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { buildProvider, buildProviderV2, mapModels, mapModelsV2, mergeModelInfo, normalizeModelListResponse, modelListResponseShape, } from "./mapping.js";
|
|
1
|
+
export { addOpenCodeVariantTombstones, buildProvider, buildProviderV2, isDeepSeekV4ReasoningModel, mapModels, mapModelsV2, mergeModelInfo, normalizeModelListResponse, modelListResponseShape, } from "./mapping.js";
|
|
2
2
|
export { fetchModelGroupInfo, fetchModelInfo, normalizeModelGroupResponse } from "./litellm.js";
|
|
3
3
|
export { modelGroupInfoURL, modelInfoURL } from "./options.js";
|
|
4
4
|
export { deepMerge } from "./merge.js";
|
package/dist/helpers.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { buildProvider, buildProviderV2, mapModels, mapModelsV2, mergeModelInfo, normalizeModelListResponse, modelListResponseShape, } from "./mapping.js";
|
|
1
|
+
export { addOpenCodeVariantTombstones, buildProvider, buildProviderV2, isDeepSeekV4ReasoningModel, mapModels, mapModelsV2, mergeModelInfo, normalizeModelListResponse, modelListResponseShape, } from "./mapping.js";
|
|
2
2
|
export { fetchModelGroupInfo, fetchModelInfo, normalizeModelGroupResponse } from "./litellm.js";
|
|
3
3
|
export { modelGroupInfoURL, modelInfoURL } from "./options.js";
|
|
4
4
|
export { deepMerge } from "./merge.js";
|
package/dist/index.d.ts
CHANGED
|
@@ -2,5 +2,18 @@ import type { Config, PluginInput } from "@opencode-ai/plugin";
|
|
|
2
2
|
import type { LiteLLMPluginOptions } from "./types.js";
|
|
3
3
|
declare const LiteLLMPlugin: (ctx: PluginInput, options?: LiteLLMPluginOptions) => Promise<{
|
|
4
4
|
config: (cfg: Config) => Promise<void>;
|
|
5
|
+
"chat.params": (input: {
|
|
6
|
+
sessionID: string;
|
|
7
|
+
agent: string;
|
|
8
|
+
model: import("@opencode-ai/sdk").Model;
|
|
9
|
+
provider: import("@opencode-ai/plugin").ProviderContext;
|
|
10
|
+
message: import("@opencode-ai/sdk").UserMessage;
|
|
11
|
+
}, output: {
|
|
12
|
+
temperature: number;
|
|
13
|
+
topP: number;
|
|
14
|
+
topK: number;
|
|
15
|
+
maxOutputTokens: number | undefined;
|
|
16
|
+
options: Record<string, any>;
|
|
17
|
+
}) => Promise<void>;
|
|
5
18
|
}>;
|
|
6
19
|
export default LiteLLMPlugin;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fetchModelGroupInfo, fetchModelInfo } from "./litellm.js";
|
|
2
|
-
import { buildProvider, buildProviderV2, mergeModelInfo } from "./mapping.js";
|
|
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 } });
|
|
@@ -53,6 +53,19 @@ const LiteLLMPlugin = async (ctx, options) => {
|
|
|
53
53
|
throw error;
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
|
+
"chat.params": (async (input, output) => {
|
|
57
|
+
const provider = input.provider;
|
|
58
|
+
if ((provider.info?.id ?? provider.id) !== resolved.providerKey)
|
|
59
|
+
return;
|
|
60
|
+
if (!isContractReasoningModel(input.model.id))
|
|
61
|
+
return;
|
|
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
|
+
}
|
|
68
|
+
}),
|
|
56
69
|
};
|
|
57
70
|
};
|
|
58
71
|
export default LiteLLMPlugin;
|
package/dist/litellm.d.ts
CHANGED
|
File without changes
|
package/dist/litellm.js
CHANGED
|
File without changes
|
package/dist/mapping.d.ts
CHANGED
|
@@ -10,11 +10,57 @@ export type ModelListResponseShape = {
|
|
|
10
10
|
export declare function modelListResponseShape(value: unknown): ModelListResponseShape;
|
|
11
11
|
export declare function mergeModelInfo(deployments: LiteLLMModelInfo[], groups: LiteLLMModelInfo[]): LiteLLMModelInfo[];
|
|
12
12
|
export declare function normalizeModelListResponse(response: unknown): LiteLLMModelInfo[];
|
|
13
|
+
export declare function isDeepSeekV4ReasoningModel(name: string | undefined): boolean;
|
|
14
|
+
export declare function isQwen38ReasoningModel(name: string | undefined): boolean;
|
|
15
|
+
export declare function isContractReasoningModel(name: string | undefined): boolean;
|
|
16
|
+
export declare function addOpenCodeVariantTombstones(provider: OpenCodeProvider): OpenCodeProvider;
|
|
13
17
|
export declare function mapModels(response: unknown, options?: LiteLLMPluginOptions): {
|
|
14
18
|
[k: string]: OpenCodeModel;
|
|
15
19
|
};
|
|
16
20
|
export declare function buildProvider(models: unknown, options?: LiteLLMPluginOptions): OpenCodeProvider;
|
|
17
21
|
export declare function mapModelsV2(models: unknown, options?: LiteLLMPluginOptions): {
|
|
18
|
-
[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
|
+
};
|
|
19
65
|
};
|
|
20
66
|
export declare function buildProviderV2(models: unknown, options?: LiteLLMPluginOptions): OpenCodeProviderV2;
|
package/dist/mapping.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
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"]);
|
|
8
|
+
const REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
|
|
9
|
+
const REASONING_EFFORT_SET = new Set(REASONING_EFFORTS);
|
|
7
10
|
const NUMERIC_MODEL_INFO_FIELDS = new Set([
|
|
8
11
|
"input_cost_per_token",
|
|
9
12
|
"output_cost_per_token",
|
|
@@ -96,6 +99,9 @@ function enrichMetadata(existing, incoming, key) {
|
|
|
96
99
|
}
|
|
97
100
|
return merged;
|
|
98
101
|
}
|
|
102
|
+
if (key === "supports_reasoning" && typeof existing === "boolean" && typeof incoming === "boolean") {
|
|
103
|
+
return existing && incoming;
|
|
104
|
+
}
|
|
99
105
|
if (typeof existing === "boolean" && typeof incoming === "boolean")
|
|
100
106
|
return existing || incoming;
|
|
101
107
|
if (isValidMetadata(existing, key))
|
|
@@ -161,6 +167,8 @@ function allowedByFilters(model, options) {
|
|
|
161
167
|
const name = modelName(model);
|
|
162
168
|
if (!name)
|
|
163
169
|
return false;
|
|
170
|
+
if (isRetiredModelAlias(name))
|
|
171
|
+
return false;
|
|
164
172
|
if (options.includeModels?.length && !options.includeModels.includes(name))
|
|
165
173
|
return false;
|
|
166
174
|
if (options.excludeModels?.includes(name))
|
|
@@ -205,12 +213,12 @@ function v2Cost(model) {
|
|
|
205
213
|
function limit(model) {
|
|
206
214
|
const context = asNumber(model.model_info?.max_input_tokens) ?? asNumber(model.model_info?.max_tokens);
|
|
207
215
|
const output = asNumber(model.model_info?.max_output_tokens) ?? asNumber(model.model_info?.max_tokens);
|
|
208
|
-
if (context === undefined
|
|
216
|
+
if (context === undefined)
|
|
209
217
|
return undefined;
|
|
210
218
|
return {
|
|
211
219
|
context,
|
|
212
220
|
...(asNumber(model.model_info?.max_input_tokens) === undefined ? {} : { input: asNumber(model.model_info?.max_input_tokens) }),
|
|
213
|
-
output,
|
|
221
|
+
output: output ?? 0,
|
|
214
222
|
};
|
|
215
223
|
}
|
|
216
224
|
function canonicalModelName(model) {
|
|
@@ -240,16 +248,67 @@ function modalities(model) {
|
|
|
240
248
|
};
|
|
241
249
|
}
|
|
242
250
|
function supportsReasoning(model) {
|
|
243
|
-
|
|
244
|
-
model.model_info
|
|
251
|
+
if (typeof model.model_info?.supports_reasoning === "boolean")
|
|
252
|
+
return model.model_info.supports_reasoning;
|
|
253
|
+
return Boolean(model.model_info?.supported_params?.includes("reasoning_effort") ||
|
|
245
254
|
model.model_info?.supported_params?.includes("thinking"));
|
|
246
255
|
}
|
|
247
|
-
function
|
|
256
|
+
export function isDeepSeekV4ReasoningModel(name) {
|
|
257
|
+
return modelContract(name)?.family === "DeepSeek V4 Flash";
|
|
258
|
+
}
|
|
259
|
+
export function isQwen38ReasoningModel(name) {
|
|
260
|
+
return modelContract(name)?.family === "Qwen3.8";
|
|
261
|
+
}
|
|
262
|
+
export function isContractReasoningModel(name) {
|
|
263
|
+
return modelContract(name) !== undefined;
|
|
264
|
+
}
|
|
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
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
reasoning: true,
|
|
284
|
+
options: { ...defaultMode.legacy, variantDefault: entry.variantDefault },
|
|
285
|
+
variants,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
export function addOpenCodeVariantTombstones(provider) {
|
|
289
|
+
return {
|
|
290
|
+
...provider,
|
|
291
|
+
models: Object.fromEntries(Object.entries(provider.models).map(([name, model]) => [
|
|
292
|
+
name,
|
|
293
|
+
isContractReasoningModel(name) ? deepMerge(contractDefaults(name, true) ?? {}, model) : model,
|
|
294
|
+
])),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function reasoningVariants(model) {
|
|
298
|
+
const reported = new Set((model.model_info?.supported_reasoning_efforts ?? [])
|
|
299
|
+
.map((effort) => asString(effort)?.toLowerCase())
|
|
300
|
+
.filter((effort) => effort !== undefined && REASONING_EFFORT_SET.has(effort)));
|
|
301
|
+
const efforts = REASONING_EFFORTS.filter((effort) => reported.has(effort));
|
|
302
|
+
if (!efforts.length)
|
|
303
|
+
return undefined;
|
|
304
|
+
return Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }]));
|
|
305
|
+
}
|
|
306
|
+
function generatedModel(model, tombstones = false) {
|
|
248
307
|
const name = modelName(model);
|
|
249
308
|
if (!name)
|
|
250
309
|
return undefined;
|
|
251
310
|
const mappedModalities = modalities(model);
|
|
252
|
-
|
|
311
|
+
const generated = {
|
|
253
312
|
id: name,
|
|
254
313
|
name,
|
|
255
314
|
...(asString(model.model_info?.base_model) ? { family: asString(model.model_info?.base_model) } : {}),
|
|
@@ -258,8 +317,10 @@ function generatedModel(model) {
|
|
|
258
317
|
modalities: mappedModalities,
|
|
259
318
|
...(mappedModalities.input.includes("image") || mappedModalities.input.includes("pdf") ? { attachment: true } : {}),
|
|
260
319
|
...(supportsReasoning(model) ? { reasoning: true } : {}),
|
|
320
|
+
...(supportsReasoning(model) && reasoningVariants(model) ? { variants: reasoningVariants(model) } : {}),
|
|
261
321
|
...(model.model_info?.supports_function_calling || model.model_info?.supports_tool_choice ? { tool_call: true } : {}),
|
|
262
322
|
};
|
|
323
|
+
return deepMerge(generated, contractDefaults(name, tombstones));
|
|
263
324
|
}
|
|
264
325
|
function finalizedModel(model, override) {
|
|
265
326
|
const input = supportedModalities(model.modalities?.input ?? [TEXT_MODALITY]);
|
|
@@ -271,19 +332,37 @@ function finalizedModel(model, override) {
|
|
|
271
332
|
attachment: attachmentOverride ?? input.some((modality) => modality === "image" || modality === "pdf"),
|
|
272
333
|
};
|
|
273
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
|
+
}
|
|
274
344
|
function v2Variants(model) {
|
|
275
345
|
const variants = model.variants;
|
|
276
346
|
if (!isRecord(variants))
|
|
277
347
|
return undefined;
|
|
278
|
-
return Object.entries(variants).map(([id, value]) => {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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());
|
|
287
366
|
}
|
|
288
367
|
function v2Model(model, baseURL) {
|
|
289
368
|
const id = asString(model.id) ?? asString(model.name);
|
|
@@ -306,24 +385,31 @@ function v2Model(model, baseURL) {
|
|
|
306
385
|
input: supportedModalities(model.modalities?.input ?? [TEXT_MODALITY]),
|
|
307
386
|
output: supportedModalities(model.modalities?.output ?? [TEXT_MODALITY]),
|
|
308
387
|
},
|
|
388
|
+
...(isRecord(model.options) ? { options: { ...model.options } } : {}),
|
|
309
389
|
...(v2Cost(model) ? { cost: v2Cost(model) } : {}),
|
|
310
390
|
...(model.limit ? { limit: { ...model.limit } } : {}),
|
|
311
391
|
...(v2Variants(model) ? { variants: v2Variants(model) } : {}),
|
|
312
392
|
};
|
|
313
393
|
}
|
|
314
|
-
|
|
394
|
+
function mapModelsInternal(response, options, tombstones) {
|
|
315
395
|
const models = normalizeModelListResponse(response);
|
|
316
396
|
return Object.fromEntries(models
|
|
317
397
|
.filter((model) => allowedByFilters(model, options))
|
|
318
398
|
.map((model) => {
|
|
319
399
|
const name = modelName(model);
|
|
320
|
-
const generated = generatedModel(model);
|
|
400
|
+
const generated = generatedModel(model, tombstones);
|
|
321
401
|
return name && generated
|
|
322
|
-
? [
|
|
402
|
+
? [
|
|
403
|
+
name,
|
|
404
|
+
finalizedModel(deepMerge(generated, options.overrides?.[name]), options.overrides?.[name]),
|
|
405
|
+
]
|
|
323
406
|
: undefined;
|
|
324
407
|
})
|
|
325
408
|
.filter((entry) => Boolean(entry)));
|
|
326
409
|
}
|
|
410
|
+
export function mapModels(response, options = {}) {
|
|
411
|
+
return mapModelsInternal(response, options, false);
|
|
412
|
+
}
|
|
327
413
|
export function buildProvider(models, options = {}) {
|
|
328
414
|
return deepMerge({
|
|
329
415
|
id: options.providerKey ?? "LiteLLM",
|
|
@@ -335,12 +421,16 @@ export function buildProvider(models, options = {}) {
|
|
|
335
421
|
apiKey: options.apiKey,
|
|
336
422
|
...(options.apiKey ? { body: { apiKey: options.apiKey } } : {}),
|
|
337
423
|
},
|
|
338
|
-
models:
|
|
424
|
+
models: mapModelsInternal(models, options, true),
|
|
339
425
|
}, options.providerOverrides);
|
|
340
426
|
}
|
|
341
427
|
export function mapModelsV2(models, options = {}) {
|
|
342
428
|
const legacy = mapModels(models, options);
|
|
343
|
-
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
|
+
}));
|
|
344
434
|
}
|
|
345
435
|
export function buildProviderV2(models, options = {}) {
|
|
346
436
|
return deepMerge({
|
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;
|
|
@@ -20,6 +33,7 @@ export type OpenCodeModel = JsonObject & {
|
|
|
20
33
|
attachment?: boolean;
|
|
21
34
|
reasoning?: boolean;
|
|
22
35
|
tool_call?: boolean;
|
|
36
|
+
variants?: Record<string, JsonObject>;
|
|
23
37
|
};
|
|
24
38
|
export type OpenCodeModelV2 = JsonObject & {
|
|
25
39
|
family?: string;
|
|
@@ -120,6 +134,10 @@ export type LiteLLMModelInfo = JsonObject & {
|
|
|
120
134
|
supports_vision?: boolean;
|
|
121
135
|
supports_pdf_input?: boolean;
|
|
122
136
|
supports_reasoning?: boolean;
|
|
137
|
+
supported_reasoning_efforts?: string[];
|
|
138
|
+
supports_low_reasoning_effort?: boolean;
|
|
139
|
+
supports_xhigh_reasoning_effort?: boolean;
|
|
140
|
+
supports_max_reasoning_effort?: boolean;
|
|
123
141
|
supported_modalities?: string[];
|
|
124
142
|
output_modalities?: string[];
|
|
125
143
|
supported_params?: string[];
|
|
@@ -145,6 +163,7 @@ export type LiteLLMModelGroup = JsonObject & {
|
|
|
145
163
|
supports_vision?: boolean;
|
|
146
164
|
supports_pdf_input?: boolean;
|
|
147
165
|
supports_reasoning?: boolean;
|
|
166
|
+
supported_reasoning_efforts?: string[];
|
|
148
167
|
supported_modalities?: string[];
|
|
149
168
|
output_modalities?: string[];
|
|
150
169
|
supported_openai_params?: string[];
|
package/dist/types.js
CHANGED
|
File without changes
|