saros-proxy 0.5.7 → 0.6.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.
@@ -0,0 +1,413 @@
1
+ /**
2
+ * models-sync.ts — Auto-sync models from upstream to opencode.json.
3
+ *
4
+ * Tier 1 of the auto model discovery plan.
5
+ * Fetches upstream model IDs, diffs against opencode.json, and adds
6
+ * any missing models with minimal stubs.
7
+ */
8
+ import { existsSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs';
9
+ import { getDefaultOpencodeConfigPath } from './cli/opencode-config.js';
10
+ import { getModelsList } from './models-fetcher.js';
11
+ import { MODELS_DEV_URL, MODELS_DEV_CACHE_TTL_MS, MODELS_DEV_TIMEOUT_MS, MODELS_DEV_PROVIDER_ID, MODELS_DEV_SAFE_FIELDS, } from './constants.js';
12
+ import { loadModelsFromJson } from './cli/opencode-config.js';
13
+ import { logger } from './logger.js';
14
+ // ---------------------------------------------------------------------------
15
+ // Title-case helper
16
+ // ---------------------------------------------------------------------------
17
+ /**
18
+ * Convert a model ID into a human-readable title.
19
+ *
20
+ * Splits on hyphens, capitalises the first letter of each segment,
21
+ * and joins with spaces. Segments with 3 or fewer characters are
22
+ * fully uppercased (e.g. "glm" → "GLM", "v2" → "V2").
23
+ *
24
+ * When a segment is purely numeric (e.g. "5", "3.7") it is attached
25
+ * to the preceding word with a hyphen (e.g. "GLM-5").
26
+ *
27
+ * Letter-number boundaries are split when 3+ letters precede a digit
28
+ * (e.g. "qwen3.7" → "Qwen 3.7").
29
+ */
30
+ function toTitleCase(id) {
31
+ const parts = id.split('-');
32
+ const titled = [];
33
+ for (let i = 0; i < parts.length; i++) {
34
+ const part = parts[i];
35
+ // Purely numeric segments (e.g. "5", "3.7") attach to previous
36
+ if (/^\d+(\.\d+)?$/.test(part)) {
37
+ if (titled.length > 0) {
38
+ titled[titled.length - 1] += '-' + part;
39
+ }
40
+ else {
41
+ titled.push(part);
42
+ }
43
+ continue;
44
+ }
45
+ // Insert space before a digit when preceded by 3+ letters
46
+ const expanded = part.replace(/([a-z]{3,})(\d)/gi, '$1 $2');
47
+ const words = expanded.split(/\s+/);
48
+ const processed = words.map((w) => {
49
+ if (w.length <= 3) {
50
+ // Words with uppercase letters or digits are codes/abbreviations → fully uppercase
51
+ // All-lowercase letter-only short words → title-case (e.g. "new" → "New")
52
+ if (/[A-Z0-9]/.test(w)) {
53
+ return w.toUpperCase();
54
+ }
55
+ return w.charAt(0).toUpperCase() + w.slice(1);
56
+ }
57
+ return w.charAt(0).toUpperCase() + w.slice(1);
58
+ });
59
+ titled.push(processed.join(' '));
60
+ }
61
+ return titled.join(' ');
62
+ }
63
+ // ---------------------------------------------------------------------------
64
+ // Public API
65
+ // ---------------------------------------------------------------------------
66
+ /**
67
+ * Fetch model IDs from the upstream OpenCode-Go API.
68
+ *
69
+ * Calls `getModelsList(config)` to obtain the /v1/models response,
70
+ * then extracts `data[].id` values.
71
+ *
72
+ * @param config — Proxy configuration
73
+ * @returns Array of model ID strings, or empty array on any failure
74
+ */
75
+ export async function fetchUpstreamModelIds(config) {
76
+ try {
77
+ const response = await getModelsList(config);
78
+ const body = await response.json();
79
+ if (typeof body !== 'object' || body === null) {
80
+ logger.warn('fetchUpstreamModelIds: upstream response is not an object');
81
+ return [];
82
+ }
83
+ const data = body.data;
84
+ if (!Array.isArray(data)) {
85
+ logger.warn('fetchUpstreamModelIds: upstream response has no data array');
86
+ return [];
87
+ }
88
+ return data
89
+ .map((entry) => {
90
+ if (typeof entry === 'object' && entry !== null) {
91
+ return entry.id;
92
+ }
93
+ return '';
94
+ })
95
+ .filter(Boolean);
96
+ }
97
+ catch (err) {
98
+ logger.warn({ err }, 'fetchUpstreamModelIds: failed to fetch upstream models');
99
+ return [];
100
+ }
101
+ }
102
+ /**
103
+ * Return model IDs from `upstream` that are NOT present in `current`.
104
+ *
105
+ * Preserves upstream order and deduplicates the result.
106
+ *
107
+ * @param current — Currently configured model IDs
108
+ * @param upstream — Model IDs from the upstream API
109
+ * @returns Missing model IDs (ordered and deduplicated)
110
+ */
111
+ export function getMissingModels(current, upstream) {
112
+ const currentSet = new Set(current);
113
+ const seen = new Set();
114
+ const result = [];
115
+ for (const id of upstream) {
116
+ if (!currentSet.has(id) && !seen.has(id)) {
117
+ seen.add(id);
118
+ result.push(id);
119
+ }
120
+ }
121
+ return result;
122
+ }
123
+ /**
124
+ * Safely pick only whitelisted fields from a models.dev metadata entry.
125
+ */
126
+ function pickSafeFields(entry) {
127
+ const result = {};
128
+ for (const key of MODELS_DEV_SAFE_FIELDS) {
129
+ if (key in entry) {
130
+ result[key] = entry[key];
131
+ }
132
+ }
133
+ return result;
134
+ }
135
+ /**
136
+ * Build a minimal model stub for use in opencode.json.
137
+ *
138
+ * Priority (first match wins):
139
+ * 1. models.dev metadata (if provided and found for modelId)
140
+ * 2. OPENCODE_MODELS bundled constants
141
+ * 3. Heuristic title-case of the ID with default limits
142
+ *
143
+ * @param modelId — The upstream model ID (e.g. "glm-5", "kimi-k2.7-code")
144
+ * @param modelsDevMetadata — Optional map from models.dev (takes precedence)
145
+ * @returns A stub object with `id`, `name`, `tool_call`, and `reasoning`
146
+ */
147
+ export function buildMinimalStub(modelId, modelsDevMetadata) {
148
+ // 1. models.dev metadata (highest priority)
149
+ if (modelsDevMetadata && modelId in modelsDevMetadata) {
150
+ const raw = modelsDevMetadata[modelId];
151
+ if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
152
+ // Edge case: if the dev entry exists but has none of our safe fields,
153
+ // the result is a valid { id } stub — Tiers 2/3 are intentionally skipped
154
+ // because the model is "known" via dev metadata.
155
+ const safe = pickSafeFields(raw);
156
+ safe.id = modelId;
157
+ return safe;
158
+ }
159
+ }
160
+ // 2. models.json bundled definitions
161
+ const bundled = loadModelsFromJson();
162
+ const known = bundled[modelId];
163
+ if (known && typeof known === 'object' && !Array.isArray(known)) {
164
+ return { ...known, id: modelId };
165
+ }
166
+ // 3. Heuristic fallback with sensible defaults
167
+ return {
168
+ id: modelId,
169
+ name: toTitleCase(modelId),
170
+ limit: { context: 262144, output: 65536 },
171
+ tool_call: true,
172
+ reasoning: true,
173
+ };
174
+ }
175
+ /**
176
+ * Read the set of currently configured model IDs from opencode.json.
177
+ *
178
+ * Extracts the keys of `provider["saros-proxy"].models`.
179
+ *
180
+ * @param configPath — Path to opencode.json
181
+ * @returns Array of model ID strings, or empty array on any failure
182
+ */
183
+ export function getModelsFromOpencodeConfig(configPath) {
184
+ try {
185
+ if (!existsSync(configPath))
186
+ return [];
187
+ const raw = readFileSync(configPath, 'utf-8');
188
+ const config = JSON.parse(raw);
189
+ const provider = config.provider;
190
+ if (!provider)
191
+ return [];
192
+ const sarosProvider = provider['saros-proxy'];
193
+ if (!sarosProvider || typeof sarosProvider !== 'object')
194
+ return [];
195
+ const models = sarosProvider.models;
196
+ if (!models || typeof models !== 'object')
197
+ return [];
198
+ return Object.keys(models);
199
+ }
200
+ catch {
201
+ return [];
202
+ }
203
+ }
204
+ /**
205
+ * Add missing model stubs to the saros-proxy.models map in opencode.json.
206
+ *
207
+ * 1. Reads the config file.
208
+ * 2. Ensures `saros-proxy` provider exists.
209
+ * 3. Creates a `.backup` copy.
210
+ * 4. Merges `buildMinimalStub(id)` for each missing ID.
211
+ * 5. Writes the updated JSON.
212
+ * 6. Validates the written file — restores from backup on failure.
213
+ *
214
+ * @param configPath — Path to opencode.json
215
+ * @param missingIds — Model IDs to add
216
+ * @param modelsDevMetadata — Optional models.dev metadata (passed to buildMinimalStub)
217
+ * @returns Result with success status and optional error message
218
+ */
219
+ export function addMissingModelsToOpencodeConfig(configPath, missingIds, modelsDevMetadata) {
220
+ try {
221
+ if (!existsSync(configPath)) {
222
+ return {
223
+ success: false,
224
+ error: `opencode.json not found at ${configPath}`,
225
+ };
226
+ }
227
+ const raw = readFileSync(configPath, 'utf-8');
228
+ let config;
229
+ try {
230
+ config = JSON.parse(raw);
231
+ }
232
+ catch {
233
+ return {
234
+ success: false,
235
+ error: 'Existing opencode.json contains invalid JSON',
236
+ };
237
+ }
238
+ const provider = config.provider;
239
+ if (!provider) {
240
+ return { success: false, error: 'Config has no "provider" section' };
241
+ }
242
+ const sarosProvider = provider['saros-proxy'];
243
+ if (!sarosProvider || typeof sarosProvider !== 'object' || Array.isArray(sarosProvider)) {
244
+ return {
245
+ success: false,
246
+ error: 'saros-proxy provider config is missing or malformed',
247
+ };
248
+ }
249
+ // Ensure models field exists
250
+ const sarosObj = sarosProvider;
251
+ if (!sarosObj.models || typeof sarosObj.models !== 'object' || Array.isArray(sarosObj.models)) {
252
+ sarosObj.models = {};
253
+ }
254
+ const models = sarosObj.models;
255
+ let changed = false;
256
+ // Add missing models
257
+ if (missingIds.length > 0) {
258
+ for (const id of missingIds) {
259
+ models[id] = buildMinimalStub(id, modelsDevMetadata);
260
+ }
261
+ changed = true;
262
+ }
263
+ // Enrich existing models with models.dev metadata (add missing fields only)
264
+ if (modelsDevMetadata) {
265
+ for (const [id, model] of Object.entries(models)) {
266
+ const devEntry = modelsDevMetadata[id];
267
+ if (!devEntry || typeof devEntry !== 'object' || Array.isArray(devEntry))
268
+ continue;
269
+ const additions = pickSafeFields(devEntry);
270
+ const modelObj = model;
271
+ let enriched = false;
272
+ for (const key of Object.keys(additions)) {
273
+ if (!(key in modelObj)) {
274
+ modelObj[key] = additions[key];
275
+ enriched = true;
276
+ }
277
+ }
278
+ if (enriched) {
279
+ changed = true;
280
+ }
281
+ }
282
+ }
283
+ // Nothing changed — skip backup and write
284
+ if (!changed) {
285
+ return { success: true, path: configPath, error: undefined, created: undefined };
286
+ }
287
+ // Backup before modifying (never overwrite existing backup)
288
+ const backupPath = configPath + '.backup';
289
+ if (!existsSync(backupPath)) {
290
+ copyFileSync(configPath, backupPath);
291
+ }
292
+ // Write updated config
293
+ const json = JSON.stringify(config, null, 2);
294
+ writeFileSync(configPath, json, 'utf-8');
295
+ // Validate: re-read and parse to ensure we didn't corrupt it
296
+ try {
297
+ const verifyRaw = readFileSync(configPath, 'utf-8');
298
+ JSON.parse(verifyRaw);
299
+ }
300
+ catch {
301
+ // Restore from backup
302
+ if (existsSync(backupPath)) {
303
+ copyFileSync(backupPath, configPath);
304
+ }
305
+ return {
306
+ success: false,
307
+ path: configPath,
308
+ error: 'Failed to write valid JSON. Original file restored from backup.',
309
+ };
310
+ }
311
+ return { success: true, path: configPath };
312
+ }
313
+ catch (err) {
314
+ // Restore from backup if available
315
+ const backupPath = configPath + '.backup';
316
+ if (existsSync(backupPath)) {
317
+ try {
318
+ copyFileSync(backupPath, configPath);
319
+ }
320
+ catch {
321
+ // Ignore restore errors — the backup file itself may be gone
322
+ }
323
+ }
324
+ const message = err instanceof Error ? err.message : String(err);
325
+ return { success: false, path: configPath, error: message };
326
+ }
327
+ }
328
+ /**
329
+ * Orchestrator: sync opencode.json models with upstream.
330
+ *
331
+ * Flow:
332
+ * fetchModelsDevMetadata()
333
+ * → fetchUpstreamModelIds(config)
334
+ * → getModelsFromOpencodeConfig(configPath)
335
+ * → getMissingModels(current, upstream)
336
+ * → addMissingModelsToOpencodeConfig(configPath, missing, devMetadata)
337
+ *
338
+ * @param config — Proxy configuration for upstream fetch
339
+ * @param configPath — Path to opencode.json (defaults to platform default)
340
+ * @returns Result with success status and optional error message
341
+ */
342
+ export async function syncOpencodeModelsWithUpstream(config, configPath) {
343
+ const path = configPath ?? getDefaultOpencodeConfigPath();
344
+ // Best-effort fetch of models.dev metadata (never throws, null → undefined)
345
+ const devMetadata = (await fetchModelsDevMetadata()) ?? undefined;
346
+ const upstreamIds = await fetchUpstreamModelIds(config);
347
+ if (upstreamIds.length === 0) {
348
+ logger.warn('syncOpencodeModelsWithUpstream: no upstream models returned, skipping sync');
349
+ return { success: false, error: 'No upstream models returned' };
350
+ }
351
+ const currentIds = getModelsFromOpencodeConfig(path);
352
+ const missingIds = getMissingModels(currentIds, upstreamIds);
353
+ if (missingIds.length > 0) {
354
+ logger.info('syncOpencodeModelsWithUpstream: adding %d missing models to opencode.json', missingIds.length);
355
+ }
356
+ return addMissingModelsToOpencodeConfig(path, missingIds, devMetadata);
357
+ }
358
+ let _modelsDevCache = null;
359
+ /**
360
+ * Extract the opencode-go provider's models from a models.dev API response.
361
+ *
362
+ * Pure function — callers handle JSON parsing and error recovery.
363
+ *
364
+ * @param parsed — Parsed models.dev JSON object (result of `JSON.parse`)
365
+ * @returns Model ID → metadata map, or empty object if provider not found
366
+ */
367
+ export function extractOpencodeGoModels(parsed) {
368
+ // The API returns a flat dictionary keyed by provider ID, e.g.:
369
+ // { "opencode-go": { id, env, npm, api, name, doc, models: { ... } }, ... }
370
+ const entry = parsed[MODELS_DEV_PROVIDER_ID];
371
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
372
+ return {};
373
+ const models = entry.models;
374
+ if (!models || typeof models !== 'object' || Array.isArray(models))
375
+ return {};
376
+ return models;
377
+ }
378
+ /**
379
+ * Fetch and cache models.dev metadata for opencode-go models.
380
+ *
381
+ * Caches in memory with TTL. Never throws — returns null on any failure.
382
+ * Skips in-flight deduplication (YAGNI — single orchestrator path).
383
+ *
384
+ * @returns Model ID → metadata map, null on failure, empty on missing provider
385
+ */
386
+ export async function fetchModelsDevMetadata() {
387
+ // Return cached data if within TTL
388
+ if (_modelsDevCache !== null && Date.now() - _modelsDevCache.ts < MODELS_DEV_CACHE_TTL_MS) {
389
+ return _modelsDevCache.data;
390
+ }
391
+ try {
392
+ const response = await fetch(MODELS_DEV_URL, {
393
+ signal: AbortSignal.timeout(MODELS_DEV_TIMEOUT_MS),
394
+ });
395
+ if (!response.ok)
396
+ return null;
397
+ const text = await response.text();
398
+ const parsed = JSON.parse(text);
399
+ const data = extractOpencodeGoModels(parsed);
400
+ _modelsDevCache = { ts: Date.now(), data };
401
+ return data;
402
+ }
403
+ catch {
404
+ return null;
405
+ }
406
+ }
407
+ /**
408
+ * Clear the models.dev in-memory cache (test helper).
409
+ */
410
+ export function resetModelsDevCacheState() {
411
+ _modelsDevCache = null;
412
+ }
413
+ //# sourceMappingURL=models-sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"models-sync.js","sourceRoot":"","sources":["../src/models-sync.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * probe-cache.ts — Persistent cache for model probe results.
3
+ *
4
+ * Stores probe results to ~/.config/saros/probe-cache.json with a 7-day TTL.
5
+ * Avoids re-probing models that have been successfully probed recently.
6
+ */
7
+ export interface ProbeResult {
8
+ status: 'ok' | 'error' | 'rate_limited' | 'unsupported';
9
+ ts: number;
10
+ details?: string;
11
+ }
12
+ export interface ModelProbe {
13
+ modelId: string;
14
+ liveness: ProbeResult;
15
+ reasoning: ProbeResult;
16
+ toolCalling: ProbeResult;
17
+ }
18
+ export interface ProbeCacheFile {
19
+ version: 1;
20
+ probes: Record<string, ModelProbe>;
21
+ }
22
+ /**
23
+ * Return the file path for the probe cache.
24
+ * Always uses `~/.config/saros/probe-cache.json` (matches daemon PID file convention).
25
+ */
26
+ export declare function getProbeCachePath(): string;
27
+ /**
28
+ * Load the probe cache from disk.
29
+ * Returns an empty cache if the file is missing or corrupt.
30
+ *
31
+ * @param cachePath — Path to the cache file (defaults to getProbeCachePath())
32
+ */
33
+ export declare function loadProbeCache(cachePath?: string): ProbeCacheFile;
34
+ /**
35
+ * Write the probe cache to disk.
36
+ * Creates the parent directory if it does not exist.
37
+ *
38
+ * @param cache — Cache data to write
39
+ * @param cachePath — Path to the cache file (defaults to getProbeCachePath())
40
+ */
41
+ export declare function saveProbeCache(cache: ProbeCacheFile, cachePath?: string): void;
42
+ /**
43
+ * Retrieve a cached probe result for the given model.
44
+ * Returns null if the model is not cached or the entry is older than TTL.
45
+ *
46
+ * @param modelId — Model ID to look up
47
+ * @param cachePath — Path to the cache file (defaults to getProbeCachePath())
48
+ */
49
+ export declare function getCachedProbe(modelId: string, cachePath?: string): ModelProbe | null;
50
+ /**
51
+ * Store a probe result for the given model.
52
+ * Loads existing cache, merges, and saves back to disk.
53
+ *
54
+ * @param modelId — Model ID to store
55
+ * @param probe — Probe result to store
56
+ * @param cachePath — Path to the cache file (defaults to getProbeCachePath())
57
+ */
58
+ export declare function setCachedProbe(modelId: string, probe: ModelProbe, cachePath?: string): void;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * probe-cache.ts — Persistent cache for model probe results.
3
+ *
4
+ * Stores probe results to ~/.config/saros/probe-cache.json with a 7-day TTL.
5
+ * Avoids re-probing models that have been successfully probed recently.
6
+ */
7
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
8
+ import { join, dirname } from 'node:path';
9
+ import { homedir } from 'node:os';
10
+ import { PROBE_CACHE_TTL_MS } from './constants.js';
11
+ // ---------------------------------------------------------------------------
12
+ // Path
13
+ // ---------------------------------------------------------------------------
14
+ /**
15
+ * Return the file path for the probe cache.
16
+ * Always uses `~/.config/saros/probe-cache.json` (matches daemon PID file convention).
17
+ */
18
+ export function getProbeCachePath() {
19
+ return join(homedir(), '.config', 'saros', 'probe-cache.json');
20
+ }
21
+ // ---------------------------------------------------------------------------
22
+ // Read / Write
23
+ // ---------------------------------------------------------------------------
24
+ /**
25
+ * Load the probe cache from disk.
26
+ * Returns an empty cache if the file is missing or corrupt.
27
+ *
28
+ * @param cachePath — Path to the cache file (defaults to getProbeCachePath())
29
+ */
30
+ export function loadProbeCache(cachePath = getProbeCachePath()) {
31
+ if (!existsSync(cachePath)) {
32
+ return { version: 1, probes: {} };
33
+ }
34
+ try {
35
+ const raw = readFileSync(cachePath, 'utf-8');
36
+ const parsed = JSON.parse(raw);
37
+ // Basic structure validation
38
+ if (parsed &&
39
+ typeof parsed === 'object' &&
40
+ parsed.version === 1 &&
41
+ parsed.probes &&
42
+ typeof parsed.probes === 'object' &&
43
+ !Array.isArray(parsed.probes)) {
44
+ return parsed;
45
+ }
46
+ return { version: 1, probes: {} };
47
+ }
48
+ catch {
49
+ // Corrupt JSON or read error — return fresh cache
50
+ return { version: 1, probes: {} };
51
+ }
52
+ }
53
+ /**
54
+ * Write the probe cache to disk.
55
+ * Creates the parent directory if it does not exist.
56
+ *
57
+ * @param cache — Cache data to write
58
+ * @param cachePath — Path to the cache file (defaults to getProbeCachePath())
59
+ */
60
+ export function saveProbeCache(cache, cachePath = getProbeCachePath()) {
61
+ mkdirSync(dirname(cachePath), { recursive: true });
62
+ writeFileSync(cachePath, JSON.stringify(cache, null, 2), 'utf-8');
63
+ }
64
+ // ---------------------------------------------------------------------------
65
+ // Accessors
66
+ // ---------------------------------------------------------------------------
67
+ /**
68
+ * Retrieve a cached probe result for the given model.
69
+ * Returns null if the model is not cached or the entry is older than TTL.
70
+ *
71
+ * @param modelId — Model ID to look up
72
+ * @param cachePath — Path to the cache file (defaults to getProbeCachePath())
73
+ */
74
+ export function getCachedProbe(modelId, cachePath = getProbeCachePath()) {
75
+ const cache = loadProbeCache(cachePath);
76
+ const probe = cache.probes[modelId];
77
+ if (!probe)
78
+ return null;
79
+ // Check TTL using the liveness timestamp
80
+ const now = Date.now();
81
+ if (now - probe.liveness.ts >= PROBE_CACHE_TTL_MS) {
82
+ return null;
83
+ }
84
+ return probe;
85
+ }
86
+ /**
87
+ * Store a probe result for the given model.
88
+ * Loads existing cache, merges, and saves back to disk.
89
+ *
90
+ * @param modelId — Model ID to store
91
+ * @param probe — Probe result to store
92
+ * @param cachePath — Path to the cache file (defaults to getProbeCachePath())
93
+ */
94
+ export function setCachedProbe(modelId, probe, cachePath = getProbeCachePath()) {
95
+ const cache = loadProbeCache(cachePath);
96
+ cache.probes[modelId] = probe;
97
+ saveProbeCache(cache, cachePath);
98
+ }
99
+ //# sourceMappingURL=probe-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"probe-cache.js","sourceRoot":"","sources":["../src/probe-cache.ts"],"names":[],"mappings":"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"}
package/dist/proxy.d.ts CHANGED
@@ -32,14 +32,4 @@ export declare function buildDownstreamHeaders(upstreamHeaders: Headers): Header
32
32
  * Check if a request body indicates streaming mode.
33
33
  */
34
34
  export declare function isStreamingRequest(bodyText: string): boolean;
35
- /**
36
- * Builds the OpenAI-compatible /v1/models response.
37
- * Returns the full list of OPENCODE_MODELS so the OpenCode client can
38
- * discover all available models. Without this, OpenCode falls back to a
39
- * hardcoded list of ~3 models (one per series).
40
- *
41
- * Exported for testing. The response is lightweight (~18 models) so it's
42
- * computed fresh on each request with no caching.
43
- */
44
- export declare function buildModelsListResponse(): Response;
45
35
  export declare function createProxyApp(config: ProxyConfig): Hono;
package/dist/proxy.js CHANGED
@@ -11,7 +11,8 @@ import crypto from 'node:crypto';
11
11
  import { createProxyState, selectKeyForRequest, failoverRequest, completeRequest, markKeyFailed, markKeySucceeded, classifyHttpError, } from './proxy-logic.js';
12
12
  import { logger, maskKey } from './logger.js';
13
13
  import { getAllUsage, isScraperRunning } from './scraper.js';
14
- import { MAX_BODY_SIZE, MAX_RETRIES, RATE_LIMIT_WINDOW_MS, RATE_LIMIT_MAX, OPENCODE_MODELS, } from './constants.js';
14
+ import { MAX_BODY_SIZE, MAX_RETRIES, RATE_LIMIT_WINDOW_MS, RATE_LIMIT_MAX, } from './constants.js';
15
+ import { getModelsList } from './models-fetcher.js';
15
16
  // ---------------------------------------------------------------------------
16
17
  // Helpers
17
18
  // ---------------------------------------------------------------------------
@@ -429,36 +430,6 @@ function wrapStreamWithErrorDetection(upstreamStream, state, requestId, keyLabel
429
430
  });
430
431
  }
431
432
  // ---------------------------------------------------------------------------
432
- // Models discovery (OpenAI-compatible /v1/models response)
433
- // ---------------------------------------------------------------------------
434
- /**
435
- * Builds the OpenAI-compatible /v1/models response.
436
- * Returns the full list of OPENCODE_MODELS so the OpenCode client can
437
- * discover all available models. Without this, OpenCode falls back to a
438
- * hardcoded list of ~3 models (one per series).
439
- *
440
- * Exported for testing. The response is lightweight (~18 models) so it's
441
- * computed fresh on each request with no caching.
442
- */
443
- export function buildModelsListResponse() {
444
- const now = Math.floor(Date.now() / 1000);
445
- const data = Object.values(OPENCODE_MODELS).map((model) => {
446
- const m = model;
447
- return {
448
- id: m.id,
449
- object: 'model',
450
- created: now,
451
- owned_by: 'saros',
452
- // Spread the rich model config (tool_call, limit, modalities, name, etc.)
453
- ...m,
454
- };
455
- });
456
- return new Response(JSON.stringify({ object: 'list', data }), {
457
- status: 200,
458
- headers: { 'content-type': 'application/json' },
459
- });
460
- }
461
- // ---------------------------------------------------------------------------
462
433
  // App factory
463
434
  // ---------------------------------------------------------------------------
464
435
  export function createProxyApp(config) {
@@ -540,8 +511,8 @@ export function createProxyApp(config) {
540
511
  // available models. Without this, OpenCode falls back to a hardcoded
541
512
  // list of ~3 models (one per series).
542
513
  // NOTE: Rate limiting applies to these routes via the '*' middleware above.
543
- app.get('/v1/models', (_c) => buildModelsListResponse());
544
- app.get('/zen/go/v1/models', (_c) => buildModelsListResponse());
514
+ app.get('/v1/models', (_c) => getModelsList(config));
515
+ app.get('/zen/go/v1/models', (_c) => getModelsList(config));
545
516
  // --- Upstream proxy handler (reused by both routes) ---
546
517
  async function handleProxyRequest(c, path) {
547
518
  const requestId = generateRequestId();