llm-switcher 1.3.7 → 1.4.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/CHANGELOG.md +24 -0
- package/README.md +25 -17
- package/README.vi.md +24 -13
- package/config.example.json +0 -11
- package/docs/TOKEN-OPTIMIZER-INTEROP.md +110 -110
- package/docs/response-matrix.json +1130 -1130
- package/docs/tool-vocabulary.md +236 -0
- package/formats.mjs +277 -55
- package/package.json +1 -1
- package/proxy.mjs +55 -19
- package/service.mjs +6 -3
- package/skills/llm-switcher/SKILL.md +93 -93
- package/state.mjs +23 -91
- package/switch +0 -0
- package/switch.cmd +2 -2
- package/switch.mjs +2 -2
- package/tests/dashboard.e2e.test.mjs +10 -13
- package/tests/formats.test.mjs +98 -1
- package/tests/gateway.e2e.test.mjs +23 -9
- package/tests/helpers.mjs +24 -24
- package/tests/live-optimizer-interop.mjs +205 -205
- package/tests/service.test.mjs +6 -4
- package/tests/state.test.mjs +6 -7
- package/tests/tool-repair.test.mjs +280 -0
- package/tests/ui.test.mjs +22 -15
- package/tests/update.test.mjs +16 -0
- package/tests/windows.test.mjs +124 -0
- package/toolvocab.mjs +272 -0
- package/ui.html +32 -76
- package/update.mjs +13 -5
- package/windows.mjs +141 -0
package/windows.mjs
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// The real window of every model a profile serves.
|
|
2
|
+
//
|
|
3
|
+
// A coding tool sizes a session, and its auto-compact point, from the window its own catalog
|
|
4
|
+
// gives it. So the gateway reports what the model really has instead of a number someone picked
|
|
5
|
+
// in a checkbox. Two numbers come out of a provider's list:
|
|
6
|
+
//
|
|
7
|
+
// context the whole window the model accepts
|
|
8
|
+
// compact the window to compress at, which a Codex model publishes as `context_window`
|
|
9
|
+
// next to `max_context_window` (272K against 872K)
|
|
10
|
+
//
|
|
11
|
+
// A model whose list gives only one window has no compact window, and a provider that lists no
|
|
12
|
+
// limits at all leaves both unknown: the caller then falls back to its own template rather than
|
|
13
|
+
// inventing a number.
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import os from 'node:os';
|
|
17
|
+
|
|
18
|
+
const ONE_HOUR_MS = 60 * 60 * 1000;
|
|
19
|
+
const DEFAULT_TTL_MS = ONE_HOUR_MS;
|
|
20
|
+
const FETCH_TIMEOUT_MS = 15000;
|
|
21
|
+
|
|
22
|
+
const n = v => (Number.isFinite(v) && v > 0 ? Math.round(v) : 0);
|
|
23
|
+
|
|
24
|
+
// One entry of a provider's list -> the two windows, or null when it names none.
|
|
25
|
+
export function parseModelWindows(m) {
|
|
26
|
+
if (!m || typeof m !== 'object') return null;
|
|
27
|
+
const top = m.top_provider && typeof m.top_provider === 'object' ? m.top_provider : {};
|
|
28
|
+
const context = n(m.max_context_window) || n(m.context_length) || n(top.context_length) || n(m.context_window);
|
|
29
|
+
// intact serves the threshold under its own name; a Codex-shaped list carries it as the
|
|
30
|
+
// smaller of the two windows it publishes.
|
|
31
|
+
const compact = n(m.compact_window) || n(m.compaction_threshold)
|
|
32
|
+
|| (context && n(m.context_window) && n(m.context_window) < context ? n(m.context_window) : 0);
|
|
33
|
+
if (!context && !compact) return null;
|
|
34
|
+
return { context, compact };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The list as it names the models, so a caller can look one up by the id a profile maps.
|
|
38
|
+
export function parseModelList(data) {
|
|
39
|
+
const entries = Array.isArray(data?.data) ? data.data
|
|
40
|
+
: Array.isArray(data) ? data
|
|
41
|
+
: Array.isArray(data?.models) ? data.models
|
|
42
|
+
: [];
|
|
43
|
+
const out = {};
|
|
44
|
+
for (const m of entries) {
|
|
45
|
+
const id = String(typeof m === 'string' ? m : (m?.id || m?.name || '')).replace(/^models\//, '');
|
|
46
|
+
if (!id) continue;
|
|
47
|
+
const w = parseModelWindows(m);
|
|
48
|
+
if (w) out[id] = w;
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Reads a provider's model list for its windows. Returns {} on any failure: a profile whose
|
|
55
|
+
* upstream cannot be reached keeps the last answer, never a wrong window.
|
|
56
|
+
*/
|
|
57
|
+
export async function fetchProfileWindows(baseURL, apiKey, { timeoutMs = FETCH_TIMEOUT_MS } = {}) {
|
|
58
|
+
const base = String(baseURL || '').replace(/\/+$/, '');
|
|
59
|
+
if (!base) return {};
|
|
60
|
+
try {
|
|
61
|
+
const headers = { accept: 'application/json' };
|
|
62
|
+
if (apiKey) {
|
|
63
|
+
headers.authorization = `Bearer ${apiKey}`;
|
|
64
|
+
headers['x-api-key'] = apiKey;
|
|
65
|
+
}
|
|
66
|
+
const res = await fetch(`${base}/models`, { headers, signal: AbortSignal.timeout(timeoutMs) });
|
|
67
|
+
if (!res.ok) return {};
|
|
68
|
+
return parseModelList(await res.json());
|
|
69
|
+
} catch {
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The windows of a profile's models, from the cache when it is fresh and from the profile's own
|
|
76
|
+
* list when it is not. A failed read keeps the stale answer, because an old window only sizes a
|
|
77
|
+
* session slightly wrong, while an empty one would hand the tool a template window it may not
|
|
78
|
+
* have. Never throws.
|
|
79
|
+
*/
|
|
80
|
+
export async function resolveProfileWindows(stateDir, cacheKey, baseURL, apiKey, { ttlMs = DEFAULT_TTL_MS, now = Date.now() } = {}) {
|
|
81
|
+
const cache = readWindowsCache(stateDir);
|
|
82
|
+
const entry = cache[cacheKey] || {};
|
|
83
|
+
const fresh = entry.at && now - entry.at < ttlMs;
|
|
84
|
+
if (fresh) return entry.models || {};
|
|
85
|
+
const models = await fetchProfileWindows(baseURL, apiKey);
|
|
86
|
+
if (Object.keys(models).length) {
|
|
87
|
+
cache[cacheKey] = { at: now, baseURL, models };
|
|
88
|
+
writeWindowsCache(stateDir, cache);
|
|
89
|
+
return models;
|
|
90
|
+
}
|
|
91
|
+
return entry.models || {};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function windowsCachePath(stateDir) {
|
|
95
|
+
return path.join(stateDir, 'model-windows.json');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function readWindowsCache(stateDir) {
|
|
99
|
+
try {
|
|
100
|
+
const d = JSON.parse(fs.readFileSync(windowsCachePath(stateDir), 'utf8'));
|
|
101
|
+
return d && typeof d === 'object' && !Array.isArray(d) ? d : {};
|
|
102
|
+
} catch {
|
|
103
|
+
return {};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function writeWindowsCache(stateDir, cache) {
|
|
108
|
+
const p = windowsCachePath(stateDir);
|
|
109
|
+
const tmp = `${p}.${process.pid}.tmp`;
|
|
110
|
+
try {
|
|
111
|
+
fs.writeFileSync(tmp, JSON.stringify(cache), 'utf8');
|
|
112
|
+
fs.renameSync(tmp, p);
|
|
113
|
+
return true;
|
|
114
|
+
} catch {
|
|
115
|
+
try { fs.rmSync(tmp, { force: true }); } catch {}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The windows the Codex CLI itself cached for its signed-in account. It is the fallback for a
|
|
122
|
+
* profile that lists no limits: those numbers came from the provider the CLI uses, so they are
|
|
123
|
+
* better than the template and never a guess. Returns {} when the tool keeps no list.
|
|
124
|
+
*/
|
|
125
|
+
export function readLocalCodexWindows(sources = {}) {
|
|
126
|
+
const codexHome = sources.codexHome || process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
127
|
+
try {
|
|
128
|
+
const d = JSON.parse(fs.readFileSync(path.join(codexHome, 'models_cache.json'), 'utf8'));
|
|
129
|
+
const models = Array.isArray(d?.models) ? d.models : [];
|
|
130
|
+
const out = {};
|
|
131
|
+
for (const m of models) {
|
|
132
|
+
const id = m && typeof m.slug === 'string' ? m.slug : '';
|
|
133
|
+
const context = n(m?.max_context_window) || n(m?.context_window);
|
|
134
|
+
const compact = context && n(m.context_window) < context ? n(m.context_window) : 0;
|
|
135
|
+
if (id && context) out[id] = { context, compact };
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
} catch {
|
|
139
|
+
return {};
|
|
140
|
+
}
|
|
141
|
+
}
|