@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 +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 +160 -13
- 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 +24 -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
|
@@ -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:
|
|
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:
|
|
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 (!
|
|
276
|
+
if (!isRecord(variants))
|
|
133
277
|
return undefined;
|
|
134
278
|
return Object.entries(variants).map(([id, value]) => {
|
|
135
|
-
const raw =
|
|
136
|
-
const body = 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
|
-
|
|
163
|
-
|
|
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(
|
|
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
|
|
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
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.
|
|
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": {
|