@nvae/llmswitch 0.9.0 → 1.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/dist/adapters/codex.js +19 -1
- package/dist/adapters/opencode.js +27 -5
- package/dist/bridge/server.js +7 -6
- package/dist/index.js +0 -0
- package/dist/utils/fetch-models.js +26 -12
- package/dist/utils/model-metadata.js +22 -0
- package/package.json +1 -1
package/dist/adapters/codex.js
CHANGED
|
@@ -15,6 +15,20 @@ export function envKeyName(profileName) {
|
|
|
15
15
|
const key = providerKey(profileName).toUpperCase();
|
|
16
16
|
return `LLM_SWITCH_${key}_API_KEY`;
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Model-level metadata Codex accepts in config.toml. Only `model_context_window`
|
|
20
|
+
* is a stable model-info key (schema is deny_unknown_fields — never write
|
|
21
|
+
* removed keys like model_max_output_tokens).
|
|
22
|
+
*/
|
|
23
|
+
function applyModelInfo(config, profile) {
|
|
24
|
+
const context = profile.models.meta?.[profile.models.default]?.context;
|
|
25
|
+
if (typeof context === "number" && context > 0) {
|
|
26
|
+
config.model_context_window = Math.round(context);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
delete config.model_context_window;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
18
32
|
export function readCodexConfig(path = getCodexConfigPath()) {
|
|
19
33
|
if (!existsSync(path))
|
|
20
34
|
return {};
|
|
@@ -41,12 +55,14 @@ export function buildCodexConfig(existing, profile, effectiveBaseUrl) {
|
|
|
41
55
|
}
|
|
42
56
|
}
|
|
43
57
|
providers[id] = providerBlock;
|
|
44
|
-
|
|
58
|
+
const next = {
|
|
45
59
|
...existing,
|
|
46
60
|
model: profile.models.default,
|
|
47
61
|
model_provider: id,
|
|
48
62
|
model_providers: providers,
|
|
49
63
|
};
|
|
64
|
+
applyModelInfo(next, profile);
|
|
65
|
+
return next;
|
|
50
66
|
}
|
|
51
67
|
export function buildCodexEnvFile(existingContent, profile, effectiveApiKey = profile.apiKey || "llm-switch-bridge") {
|
|
52
68
|
const lines = existingContent ? existingContent.split(/\r?\n/) : [];
|
|
@@ -172,11 +188,13 @@ export async function deactivateCodexProfile(profileName) {
|
|
|
172
188
|
if (next.model_provider === id) {
|
|
173
189
|
delete next.model_provider;
|
|
174
190
|
delete next.model;
|
|
191
|
+
delete next.model_context_window;
|
|
175
192
|
}
|
|
176
193
|
}
|
|
177
194
|
else if (next.model_provider) {
|
|
178
195
|
delete next.model_provider;
|
|
179
196
|
delete next.model;
|
|
197
|
+
delete next.model_context_window;
|
|
180
198
|
}
|
|
181
199
|
next.model_providers = providers;
|
|
182
200
|
atomicWriteFile(configPath, stringify(next) + "\n");
|
|
@@ -32,13 +32,26 @@ export function readOpenCodeAuth(path = getOpenCodeAuthPath()) {
|
|
|
32
32
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
33
33
|
}
|
|
34
34
|
/**
|
|
35
|
-
* Build one model entry for the OpenCode provider block
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* {
|
|
35
|
+
* Build one model entry for the OpenCode provider block, filling in every
|
|
36
|
+
* per-model field the OpenCode schema supports when metadata from
|
|
37
|
+
* models.lonae.com is available:
|
|
38
|
+
* { name, family, release_date, limit, cost, modalities, attachment,
|
|
39
|
+
* reasoning, temperature, tool_call }
|
|
39
40
|
*/
|
|
40
41
|
function buildOpenCodeModelEntry(id, meta) {
|
|
41
|
-
const entry = { name: id };
|
|
42
|
+
const entry = { name: meta?.name || id };
|
|
43
|
+
if (meta?.family) {
|
|
44
|
+
entry.family = meta.family;
|
|
45
|
+
}
|
|
46
|
+
if (meta?.releaseDate) {
|
|
47
|
+
entry.release_date = meta.releaseDate;
|
|
48
|
+
}
|
|
49
|
+
if (typeof meta?.context === "number" && typeof meta?.maxOutput === "number") {
|
|
50
|
+
entry.limit = { context: meta.context, output: meta.maxOutput };
|
|
51
|
+
}
|
|
52
|
+
if (meta?.cost) {
|
|
53
|
+
entry.cost = { input: meta.cost.input, output: meta.cost.output };
|
|
54
|
+
}
|
|
42
55
|
if (meta?.modalities) {
|
|
43
56
|
entry.modalities = {
|
|
44
57
|
input: meta.modalities.input?.length ? meta.modalities.input : ["text"],
|
|
@@ -48,6 +61,15 @@ function buildOpenCodeModelEntry(id, meta) {
|
|
|
48
61
|
if (typeof meta?.attachment === "boolean") {
|
|
49
62
|
entry.attachment = meta.attachment;
|
|
50
63
|
}
|
|
64
|
+
if (typeof meta?.reasoning === "boolean") {
|
|
65
|
+
entry.reasoning = meta.reasoning;
|
|
66
|
+
}
|
|
67
|
+
if (typeof meta?.temperature === "boolean") {
|
|
68
|
+
entry.temperature = meta.temperature;
|
|
69
|
+
}
|
|
70
|
+
if (typeof meta?.toolCall === "boolean") {
|
|
71
|
+
entry.tool_call = meta.toolCall;
|
|
72
|
+
}
|
|
51
73
|
return entry;
|
|
52
74
|
}
|
|
53
75
|
export function buildOpenCodeProviderBlock(profile, overrides) {
|
package/dist/bridge/server.js
CHANGED
|
@@ -6,6 +6,7 @@ import { anthropicToChatRequest } from "./anthropic-translate-request.js";
|
|
|
6
6
|
import { chatChunkToAnthropicEvents, chatCompletionToAnthropicMessage, createAnthropicStreamState, forceCompleteAnthropicStream, parseChatSseLine, } from "./anthropic-translate-response.js";
|
|
7
7
|
import { collectCustomToolNames, responsesToChatRequest, responsesToCompletionsRequest, } from "./translate-request.js";
|
|
8
8
|
import { chatChunkToResponsesEvents, chatCompletionToResponse, createStreamState, forceCompleteStream, parseChatSseLine as parseChatSseLineResponses, } from "./translate-response.js";
|
|
9
|
+
import { modelItemId, modelsFromPayload } from "../utils/fetch-models.js";
|
|
9
10
|
function headerValue(value) {
|
|
10
11
|
return Array.isArray(value) ? value[0] : value;
|
|
11
12
|
}
|
|
@@ -149,8 +150,7 @@ async function fetchModelsJson(upstream) {
|
|
|
149
150
|
}
|
|
150
151
|
try {
|
|
151
152
|
const json = (await response.json());
|
|
152
|
-
|
|
153
|
-
return { ok: true, status: 200, data: data };
|
|
153
|
+
return { ok: true, status: 200, data: modelsFromPayload(json) };
|
|
154
154
|
}
|
|
155
155
|
catch {
|
|
156
156
|
return { ok: false, status: 502, data: [] };
|
|
@@ -175,13 +175,14 @@ async function proxyModelsMerged(_req, res, upstreams) {
|
|
|
175
175
|
if (!result.ok)
|
|
176
176
|
continue;
|
|
177
177
|
for (const item of result.data) {
|
|
178
|
-
const id = item
|
|
179
|
-
? String(item.id)
|
|
180
|
-
: "";
|
|
178
|
+
const id = modelItemId(item);
|
|
181
179
|
if (!id || seen.has(id))
|
|
182
180
|
continue;
|
|
183
181
|
seen.add(id);
|
|
184
|
-
|
|
182
|
+
const row = item && typeof item === "object"
|
|
183
|
+
? item
|
|
184
|
+
: {};
|
|
185
|
+
merged.push(row.id ? item : { ...row, id });
|
|
185
186
|
}
|
|
186
187
|
}
|
|
187
188
|
if (!merged.length && results.every((r) => !r.ok)) {
|
package/dist/index.js
CHANGED
|
File without changes
|
|
@@ -69,7 +69,25 @@ export function buildModelsRequestHeaders(apiFormat, apiKey, customHeaders = {})
|
|
|
69
69
|
}
|
|
70
70
|
return headers;
|
|
71
71
|
}
|
|
72
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Extract a model identifier from a list item. Providers disagree on the field
|
|
74
|
+
* name: OpenAI uses `id`, some gateways `name`/`model`, and Codex-style
|
|
75
|
+
* catalogs (e.g. BigModel) use `slug`.
|
|
76
|
+
*/
|
|
77
|
+
export function modelItemId(item) {
|
|
78
|
+
if (typeof item === "string")
|
|
79
|
+
return item.trim();
|
|
80
|
+
if (!item || typeof item !== "object")
|
|
81
|
+
return "";
|
|
82
|
+
const row = item;
|
|
83
|
+
const id = row.id ?? row.name ?? row.model ?? row.slug;
|
|
84
|
+
return typeof id === "string" ? id.trim() : "";
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Collect the model-array buckets from a /models payload, accepting the common
|
|
88
|
+
* `data` and `models` shapes plus a bare array.
|
|
89
|
+
*/
|
|
90
|
+
export function modelsFromPayload(payload) {
|
|
73
91
|
if (!payload || typeof payload !== "object")
|
|
74
92
|
return [];
|
|
75
93
|
const root = payload;
|
|
@@ -80,18 +98,14 @@ export function parseModelIds(payload) {
|
|
|
80
98
|
buckets.push(...root.models);
|
|
81
99
|
if (Array.isArray(payload))
|
|
82
100
|
buckets.push(...payload);
|
|
101
|
+
return buckets;
|
|
102
|
+
}
|
|
103
|
+
export function parseModelIds(payload) {
|
|
83
104
|
const ids = new Set();
|
|
84
|
-
for (const item of
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
if (!item || typeof item !== "object")
|
|
90
|
-
continue;
|
|
91
|
-
const row = item;
|
|
92
|
-
const id = row.id ?? row.name ?? row.model;
|
|
93
|
-
if (typeof id === "string" && id.trim())
|
|
94
|
-
ids.add(id.trim());
|
|
105
|
+
for (const item of modelsFromPayload(payload)) {
|
|
106
|
+
const id = modelItemId(item);
|
|
107
|
+
if (id)
|
|
108
|
+
ids.add(id);
|
|
95
109
|
}
|
|
96
110
|
return Array.from(ids).sort((a, b) => a.localeCompare(b));
|
|
97
111
|
}
|
|
@@ -40,6 +40,16 @@ function parseModalities(row) {
|
|
|
40
40
|
output: output ?? ["text"],
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
|
+
function asNumber(value) {
|
|
44
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
45
|
+
}
|
|
46
|
+
function parseCost(row) {
|
|
47
|
+
const input = asNumber(row.priceInput);
|
|
48
|
+
const output = asNumber(row.priceOutput);
|
|
49
|
+
return input !== undefined && output !== undefined
|
|
50
|
+
? { input, output }
|
|
51
|
+
: undefined;
|
|
52
|
+
}
|
|
43
53
|
/** Insert into a normalized bucket, preferring undated ids as canonical. */
|
|
44
54
|
function indexNorm(bucket, datedMetas, key, meta, dated) {
|
|
45
55
|
if (!key)
|
|
@@ -76,8 +86,20 @@ export function parseModelMetadata(payload) {
|
|
|
76
86
|
const meta = {
|
|
77
87
|
id,
|
|
78
88
|
name: typeof row.name === "string" && row.name.trim() ? row.name.trim() : undefined,
|
|
89
|
+
family: typeof row.family === "string" && row.family.trim()
|
|
90
|
+
? row.family.trim()
|
|
91
|
+
: undefined,
|
|
92
|
+
releaseDate: typeof row.releaseDate === "string" && row.releaseDate.trim()
|
|
93
|
+
? row.releaseDate.trim()
|
|
94
|
+
: undefined,
|
|
95
|
+
context: asNumber(row.context),
|
|
96
|
+
maxOutput: asNumber(row.output),
|
|
97
|
+
reasoning: typeof row.reasoning === "boolean" ? row.reasoning : undefined,
|
|
98
|
+
toolCall: typeof row.toolCall === "boolean" ? row.toolCall : undefined,
|
|
99
|
+
temperature: typeof row.temperature === "boolean" ? row.temperature : undefined,
|
|
79
100
|
modalities: parseModalities(row),
|
|
80
101
|
attachment: typeof row.attachment === "boolean" ? row.attachment : undefined,
|
|
102
|
+
cost: parseCost(row),
|
|
81
103
|
};
|
|
82
104
|
const bareIndex = id.indexOf("/");
|
|
83
105
|
const bare = bareIndex >= 0 ? id.slice(bareIndex + 1).trim() : id;
|