@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.
@@ -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
- return {
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. When metadata from
36
- * models.lonae.com is available, expose the supported input/output modalities
37
- * and attachment support, e.g.
38
- * { "name": "id", "modalities": { "input": ["text","image"], "output": ["text"] }, "attachment": true }
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) {
@@ -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
- const data = Array.isArray(json.data) ? json.data : [];
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 && typeof item === "object" && "id" in 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
- merged.push(item);
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
- export function parseModelIds(payload) {
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 buckets) {
85
- if (typeof item === "string" && item.trim()) {
86
- ids.add(item.trim());
87
- continue;
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nvae/llmswitch",
3
- "version": "0.9.0",
3
+ "version": "1.2.0",
4
4
  "description": "CLI to switch LLM providers and models for Claude Code, Codex, and OpenCode",
5
5
  "type": "module",
6
6
  "bin": {