@staticeng/opencode-litellm 0.1.5 → 0.1.7
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 +10 -7
- package/dist/helpers.d.ts +4 -3
- package/dist/helpers.js +3 -2
- package/dist/index.d.ts +0 -0
- package/dist/index.js +20 -3
- package/dist/litellm.d.ts +2 -0
- package/dist/litellm.js +57 -21
- package/dist/mapping.d.ts +15 -4
- package/dist/mapping.js +144 -9
- package/dist/merge.d.ts +0 -0
- package/dist/merge.js +0 -0
- package/dist/options.d.ts +1 -0
- package/dist/options.js +3 -0
- package/dist/types.d.ts +23 -1
- package/dist/types.js +0 -0
- package/package.json +2 -2
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
|
|
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
|
|
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` |
|
|
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
|
|
92
|
-
-
|
|
93
|
-
- Adds or replaces `cfg.provider[providerKey]` during
|
|
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 {
|
|
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
|
|
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 {
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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:
|
|
6
|
-
export declare function mapModelsV2(models:
|
|
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:
|
|
20
|
+
export declare function buildProviderV2(models: unknown, options?: LiteLLMPluginOptions): OpenCodeProviderV2;
|
package/dist/mapping.js
CHANGED
|
@@ -3,6 +3,60 @@ const DEFAULT_MODES = new Set(["chat", "responses"]);
|
|
|
3
3
|
const NON_CHAT_MODES = new Set(["embedding", "rerank"]);
|
|
4
4
|
const TEXT_MODALITY = "text";
|
|
5
5
|
const TEXT_ONLY_MODELS = new Set(["minimax-m2.7", "minimax_m2.7"]);
|
|
6
|
+
const NUMERIC_MODEL_INFO_FIELDS = new Set([
|
|
7
|
+
"input_cost_per_token",
|
|
8
|
+
"output_cost_per_token",
|
|
9
|
+
"cache_creation_input_token_cost",
|
|
10
|
+
"cache_read_input_token_cost",
|
|
11
|
+
"max_tokens",
|
|
12
|
+
"max_input_tokens",
|
|
13
|
+
"max_output_tokens",
|
|
14
|
+
]);
|
|
15
|
+
const MODEL_LIST_RESPONSE_ERROR_MESSAGE = "LiteLLM model list response did not contain a model array";
|
|
16
|
+
const MODEL_ARRAY_FIELDS = ["data", "models", "model_info"];
|
|
17
|
+
const MODEL_INFO_FIELDS = new Set([
|
|
18
|
+
"id",
|
|
19
|
+
"base_model",
|
|
20
|
+
"mode",
|
|
21
|
+
"input_cost_per_token",
|
|
22
|
+
"output_cost_per_token",
|
|
23
|
+
"max_tokens",
|
|
24
|
+
"max_input_tokens",
|
|
25
|
+
"max_output_tokens",
|
|
26
|
+
"supports_function_calling",
|
|
27
|
+
"supports_tool_choice",
|
|
28
|
+
"supports_vision",
|
|
29
|
+
"supports_pdf_input",
|
|
30
|
+
"supports_reasoning",
|
|
31
|
+
"supported_modalities",
|
|
32
|
+
"output_modalities",
|
|
33
|
+
"supported_params",
|
|
34
|
+
]);
|
|
35
|
+
function isRecord(value) {
|
|
36
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
37
|
+
}
|
|
38
|
+
export function modelListResponseShape(value) {
|
|
39
|
+
if (Array.isArray(value))
|
|
40
|
+
return { type: "array", length: value.length };
|
|
41
|
+
if (value === null)
|
|
42
|
+
return { type: "null" };
|
|
43
|
+
if (!isRecord(value))
|
|
44
|
+
return { type: typeof value };
|
|
45
|
+
const values = Object.values(value);
|
|
46
|
+
return {
|
|
47
|
+
type: "object",
|
|
48
|
+
keyCount: Object.keys(value).length,
|
|
49
|
+
arrayFields: MODEL_ARRAY_FIELDS.filter((field) => Array.isArray(value[field])),
|
|
50
|
+
objectValueCount: values.filter(isRecord).length,
|
|
51
|
+
scalarValueCount: values.filter((entry) => !isRecord(entry) && !Array.isArray(entry)).length,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function modelListResponseError(response) {
|
|
55
|
+
return Object.assign(new Error(MODEL_LIST_RESPONSE_ERROR_MESSAGE), {
|
|
56
|
+
name: "LiteLLMModelListResponseError",
|
|
57
|
+
responseShape: modelListResponseShape(response),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
6
60
|
function asString(value) {
|
|
7
61
|
return typeof value === "string" && value.trim() ? value : undefined;
|
|
8
62
|
}
|
|
@@ -17,9 +71,85 @@ function perMillion(value) {
|
|
|
17
71
|
function unique(values) {
|
|
18
72
|
return Array.from(new Set(values));
|
|
19
73
|
}
|
|
74
|
+
function isValidMetadata(value, key) {
|
|
75
|
+
if (value === undefined || value === null || value === "")
|
|
76
|
+
return false;
|
|
77
|
+
if (key && NUMERIC_MODEL_INFO_FIELDS.has(key))
|
|
78
|
+
return asNumber(value) !== undefined;
|
|
79
|
+
if (typeof value === "number")
|
|
80
|
+
return Number.isFinite(value) && value > 0;
|
|
81
|
+
if (Array.isArray(value))
|
|
82
|
+
return value.length > 0;
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
function enrichMetadata(existing, incoming, key) {
|
|
86
|
+
if (Array.isArray(existing) && Array.isArray(incoming))
|
|
87
|
+
return unique([...existing, ...incoming].filter(asString));
|
|
88
|
+
if (isRecord(existing) && isRecord(incoming)) {
|
|
89
|
+
const merged = { ...existing };
|
|
90
|
+
for (const [nestedKey, value] of Object.entries(incoming)) {
|
|
91
|
+
merged[nestedKey] = enrichMetadata(merged[nestedKey], value, nestedKey);
|
|
92
|
+
}
|
|
93
|
+
return merged;
|
|
94
|
+
}
|
|
95
|
+
if (typeof existing === "boolean" && typeof incoming === "boolean")
|
|
96
|
+
return existing || incoming;
|
|
97
|
+
if (isValidMetadata(existing, key))
|
|
98
|
+
return existing;
|
|
99
|
+
return isValidMetadata(incoming, key) ? incoming : existing;
|
|
100
|
+
}
|
|
20
101
|
function modelName(model) {
|
|
21
102
|
return asString(model.model_name) ?? asString(model.litellm_params?.model) ?? asString(model.model_info?.id);
|
|
22
103
|
}
|
|
104
|
+
export function mergeModelInfo(deployments, groups) {
|
|
105
|
+
const byName = new Map();
|
|
106
|
+
for (const model of [...deployments, ...groups]) {
|
|
107
|
+
const name = modelName(model);
|
|
108
|
+
if (!name)
|
|
109
|
+
continue;
|
|
110
|
+
const existing = byName.get(name);
|
|
111
|
+
byName.set(name, existing ? enrichMetadata(existing, model) : model);
|
|
112
|
+
}
|
|
113
|
+
return Array.from(byName.values());
|
|
114
|
+
}
|
|
115
|
+
function hasModelInfoFields(value) {
|
|
116
|
+
return Object.keys(value).some((key) => MODEL_INFO_FIELDS.has(key));
|
|
117
|
+
}
|
|
118
|
+
function isPlausibleMappedModel(value) {
|
|
119
|
+
if (isRecord(value.model_info) && hasModelInfoFields(value.model_info))
|
|
120
|
+
return true;
|
|
121
|
+
if (isRecord(value.litellm_params) && typeof value.litellm_params.model === "string")
|
|
122
|
+
return true;
|
|
123
|
+
return hasModelInfoFields(value);
|
|
124
|
+
}
|
|
125
|
+
function normalizeMappedModel(name, value) {
|
|
126
|
+
if (!isRecord(value))
|
|
127
|
+
return undefined;
|
|
128
|
+
const model = value;
|
|
129
|
+
if (modelName(model))
|
|
130
|
+
return model;
|
|
131
|
+
if (!isPlausibleMappedModel(value))
|
|
132
|
+
return undefined;
|
|
133
|
+
return { ...model, model_name: name };
|
|
134
|
+
}
|
|
135
|
+
export function normalizeModelListResponse(response) {
|
|
136
|
+
if (Array.isArray(response))
|
|
137
|
+
return response;
|
|
138
|
+
if (isRecord(response)) {
|
|
139
|
+
for (const field of MODEL_ARRAY_FIELDS) {
|
|
140
|
+
if (Array.isArray(response[field]))
|
|
141
|
+
return response[field];
|
|
142
|
+
}
|
|
143
|
+
if (MODEL_ARRAY_FIELDS.some((field) => field in response))
|
|
144
|
+
throw modelListResponseError(response);
|
|
145
|
+
const mappedModels = Object.entries(response)
|
|
146
|
+
.map(([name, value]) => normalizeMappedModel(name, value))
|
|
147
|
+
.filter((model) => Boolean(model));
|
|
148
|
+
if (mappedModels.length > 0)
|
|
149
|
+
return mappedModels;
|
|
150
|
+
}
|
|
151
|
+
throw modelListResponseError(response);
|
|
152
|
+
}
|
|
23
153
|
function mode(model) {
|
|
24
154
|
return asString(model.model_info?.mode) ?? "chat";
|
|
25
155
|
}
|
|
@@ -129,13 +259,15 @@ function generatedModel(model) {
|
|
|
129
259
|
}
|
|
130
260
|
function v2Variants(model) {
|
|
131
261
|
const variants = model.variants;
|
|
132
|
-
if (!
|
|
262
|
+
if (!isRecord(variants))
|
|
133
263
|
return undefined;
|
|
134
264
|
return Object.entries(variants).map(([id, value]) => {
|
|
135
|
-
const raw =
|
|
136
|
-
const body = raw.options
|
|
137
|
-
|
|
138
|
-
|
|
265
|
+
const raw = isRecord(value) ? { ...value } : {};
|
|
266
|
+
const body = isRecord(raw.options) ? { ...raw.options } : raw;
|
|
267
|
+
if (body.reasoningEffort !== undefined && body.reasoning_effort === undefined) {
|
|
268
|
+
body.reasoning_effort = body.reasoningEffort;
|
|
269
|
+
delete body.reasoningEffort;
|
|
270
|
+
}
|
|
139
271
|
return { id, body };
|
|
140
272
|
});
|
|
141
273
|
}
|
|
@@ -155,6 +287,7 @@ function v2Model(model, baseURL) {
|
|
|
155
287
|
},
|
|
156
288
|
capabilities: {
|
|
157
289
|
tools: model.tool_call ?? false,
|
|
290
|
+
reasoning: model.reasoning ?? false,
|
|
158
291
|
input: model.modalities?.input ?? [TEXT_MODALITY],
|
|
159
292
|
output: model.modalities?.output ?? [TEXT_MODALITY],
|
|
160
293
|
},
|
|
@@ -163,7 +296,8 @@ function v2Model(model, baseURL) {
|
|
|
163
296
|
...(v2Variants(model) ? { variants: v2Variants(model) } : {}),
|
|
164
297
|
};
|
|
165
298
|
}
|
|
166
|
-
export function mapModels(
|
|
299
|
+
export function mapModels(response, options = {}) {
|
|
300
|
+
const models = normalizeModelListResponse(response);
|
|
167
301
|
return Object.fromEntries(models
|
|
168
302
|
.filter((model) => allowedByFilters(model, options))
|
|
169
303
|
.map((model) => {
|
|
@@ -178,10 +312,11 @@ export function buildProvider(models, options = {}) {
|
|
|
178
312
|
id: options.providerKey ?? "LiteLLM",
|
|
179
313
|
name: options.providerName ?? "LiteLLM",
|
|
180
314
|
npm: "@ai-sdk/openai-compatible",
|
|
181
|
-
api:
|
|
315
|
+
api: options.baseURL,
|
|
182
316
|
options: {
|
|
183
317
|
baseURL: options.baseURL,
|
|
184
318
|
apiKey: options.apiKey,
|
|
319
|
+
...(options.apiKey ? { body: { apiKey: options.apiKey } } : {}),
|
|
185
320
|
},
|
|
186
321
|
models: mapModels(models, options),
|
|
187
322
|
}, options.providerOverrides);
|
|
@@ -191,7 +326,7 @@ export function mapModelsV2(models, options = {}) {
|
|
|
191
326
|
return Object.fromEntries(Object.entries(legacy).map(([name, model]) => [name, v2Model(model, options.baseURL)]));
|
|
192
327
|
}
|
|
193
328
|
export function buildProviderV2(models, options = {}) {
|
|
194
|
-
return {
|
|
329
|
+
return deepMerge({
|
|
195
330
|
name: options.providerName ?? "LiteLLM",
|
|
196
331
|
api: {
|
|
197
332
|
type: "aisdk",
|
|
@@ -205,5 +340,5 @@ export function buildProviderV2(models, options = {}) {
|
|
|
205
340
|
},
|
|
206
341
|
},
|
|
207
342
|
models: mapModelsV2(models, options),
|
|
208
|
-
};
|
|
343
|
+
}, options.providerOverrides);
|
|
209
344
|
}
|
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
package/dist/types.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ export type OpenCodeModelV2 = JsonObject & {
|
|
|
33
33
|
};
|
|
34
34
|
capabilities?: {
|
|
35
35
|
tools: boolean;
|
|
36
|
+
reasoning: boolean;
|
|
36
37
|
input: string[];
|
|
37
38
|
output: string[];
|
|
38
39
|
};
|
|
@@ -123,8 +124,29 @@ export type LiteLLMModelInfo = JsonObject & {
|
|
|
123
124
|
supported_params?: string[];
|
|
124
125
|
};
|
|
125
126
|
};
|
|
126
|
-
export type LiteLLMModelInfoResponse = {
|
|
127
|
+
export type LiteLLMModelInfoResponse = LiteLLMModelInfo[] | {
|
|
127
128
|
data?: LiteLLMModelInfo[];
|
|
129
|
+
models?: LiteLLMModelInfo[];
|
|
130
|
+
model_info?: LiteLLMModelInfo[];
|
|
131
|
+
};
|
|
132
|
+
export type LiteLLMModelGroup = JsonObject & {
|
|
133
|
+
model_group?: string;
|
|
134
|
+
mode?: string;
|
|
135
|
+
input_cost_per_token?: number | string;
|
|
136
|
+
output_cost_per_token?: number | string;
|
|
137
|
+
cache_creation_input_token_cost?: number | string;
|
|
138
|
+
cache_read_input_token_cost?: number | string;
|
|
139
|
+
max_tokens?: number | string;
|
|
140
|
+
max_input_tokens?: number | string;
|
|
141
|
+
max_output_tokens?: number | string;
|
|
142
|
+
supports_function_calling?: boolean;
|
|
143
|
+
supports_tool_choice?: boolean;
|
|
144
|
+
supports_vision?: boolean;
|
|
145
|
+
supports_pdf_input?: boolean;
|
|
146
|
+
supports_reasoning?: boolean;
|
|
147
|
+
supported_modalities?: string[];
|
|
148
|
+
output_modalities?: string[];
|
|
149
|
+
supported_openai_params?: string[];
|
|
128
150
|
};
|
|
129
151
|
export type OpenCodeConfig = {
|
|
130
152
|
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.
|
|
3
|
+
"version": "0.1.7",
|
|
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": {
|