lv24-opencode-consilium 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +499 -3
- package/README.ru.md +500 -0
- package/README.zh.md +188 -0
- package/bin/cli.js +19 -0
- package/bin/mcp.js +24 -0
- package/consilium.config.example.json +84 -0
- package/dist/agents.d.ts +79 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +1051 -0
- package/dist/agents.js.map +1 -0
- package/dist/artifacts.d.ts +75 -0
- package/dist/artifacts.d.ts.map +1 -0
- package/dist/artifacts.js +464 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1392 -0
- package/dist/cli.js.map +1 -0
- package/dist/compat.d.ts +160 -0
- package/dist/compat.d.ts.map +1 -0
- package/dist/compat.js +419 -0
- package/dist/compat.js.map +1 -0
- package/dist/config.d.ts +112 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +728 -0
- package/dist/config.js.map +1 -0
- package/dist/consensus.d.ts +73 -0
- package/dist/consensus.d.ts.map +1 -0
- package/dist/consensus.js +273 -0
- package/dist/consensus.js.map +1 -0
- package/dist/defaults.d.ts +34 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +102 -0
- package/dist/defaults.js.map +1 -0
- package/dist/doctor.d.ts +27 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +798 -0
- package/dist/doctor.js.map +1 -0
- package/dist/fallback.d.ts +50 -0
- package/dist/fallback.d.ts.map +1 -0
- package/dist/fallback.js +134 -0
- package/dist/fallback.js.map +1 -0
- package/dist/git.d.ts +28 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +71 -0
- package/dist/git.js.map +1 -0
- package/dist/index.d.ts +100 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +653 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.d.ts +20 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +1215 -0
- package/dist/mcp.js.map +1 -0
- package/dist/models.d.ts +97 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +873 -0
- package/dist/models.js.map +1 -0
- package/dist/plugin.d.ts +59 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +223 -0
- package/dist/plugin.js.map +1 -0
- package/dist/presets.d.ts +27 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +86 -0
- package/dist/presets.js.map +1 -0
- package/dist/replay.d.ts +37 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +103 -0
- package/dist/replay.js.map +1 -0
- package/dist/report.d.ts +28 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +300 -0
- package/dist/report.js.map +1 -0
- package/dist/smart-experts.d.ts +44 -0
- package/dist/smart-experts.d.ts.map +1 -0
- package/dist/smart-experts.js +242 -0
- package/dist/smart-experts.js.map +1 -0
- package/dist/stream.d.ts +50 -0
- package/dist/stream.d.ts.map +1 -0
- package/dist/stream.js +219 -0
- package/dist/stream.js.map +1 -0
- package/dist/tool-run.d.ts +49 -0
- package/dist/tool-run.d.ts.map +1 -0
- package/dist/tool-run.js +143 -0
- package/dist/tool-run.js.map +1 -0
- package/dist/types.d.ts +352 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/variants.d.ts +30 -0
- package/dist/variants.d.ts.map +1 -0
- package/dist/variants.js +111 -0
- package/dist/variants.js.map +1 -0
- package/dist/verify.d.ts +33 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +80 -0
- package/dist/verify.js.map +1 -0
- package/dist/watch.d.ts +73 -0
- package/dist/watch.d.ts.map +1 -0
- package/dist/watch.js +350 -0
- package/dist/watch.js.map +1 -0
- package/docs/session-and-plugin.md +642 -0
- package/package.json +90 -6
- package/schema/config.schema.json +100 -0
package/dist/models.js
ADDED
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Каталог бесплатных моделей и подбор состава консилиума
|
|
3
|
+
* @module lv24-opencode-consilium
|
|
4
|
+
*/
|
|
5
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
|
|
6
|
+
import { tmpdir } from 'os';
|
|
7
|
+
import { dirname, join } from 'path';
|
|
8
|
+
import { DEFAULT_EXPERT_PROMPTS } from './defaults.js';
|
|
9
|
+
const MODELS_DEV_URL = 'https://models.dev/api.json';
|
|
10
|
+
const OPENROUTER_URL = 'https://openrouter.ai/api/v1/models';
|
|
11
|
+
const DEFAULT_CACHE_TTL_MS = 12 * 60 * 60 * 1000;
|
|
12
|
+
const DEFAULT_TIMEOUT_MS = 12_000;
|
|
13
|
+
const DEFAULT_MIN_CONTEXT = 32_768;
|
|
14
|
+
const EXCLUDED_PROVIDER = /coding-plan|code-plan|subscription/i;
|
|
15
|
+
/** Провайдеры платных подписок не участвуют в подборе бесплатных моделей */
|
|
16
|
+
export function isExcludedFreeProvider(providerID) {
|
|
17
|
+
return EXCLUDED_PROVIDER.test(providerID);
|
|
18
|
+
}
|
|
19
|
+
const ALPHA_PATTERN = /alpha|preview|stealth|experimental/i;
|
|
20
|
+
const AUDIO_PATTERN = /audio|whisper|tts|transcrib|speech/i;
|
|
21
|
+
const CHAIR_PROMPT = 'Ты председатель консилиума. Синтезируй мнения экспертов и дай итоговый план.';
|
|
22
|
+
const DEFAULT_AGENT_NAMES = ['arch', 'ux', 'ba', 'sec', 'consilium'];
|
|
23
|
+
const GENERIC_FAMILY_TOKENS = new Set([
|
|
24
|
+
'free',
|
|
25
|
+
'preview',
|
|
26
|
+
'alpha',
|
|
27
|
+
'beta',
|
|
28
|
+
'stealth',
|
|
29
|
+
'experimental',
|
|
30
|
+
'latest',
|
|
31
|
+
'chat',
|
|
32
|
+
'instruct'
|
|
33
|
+
]);
|
|
34
|
+
const FAMILY_PRIOR = [
|
|
35
|
+
['claude', 1],
|
|
36
|
+
['gpt', 1],
|
|
37
|
+
['gemini', 0.95],
|
|
38
|
+
['deepseek', 0.95],
|
|
39
|
+
['qwen', 0.9],
|
|
40
|
+
['glm', 0.9],
|
|
41
|
+
['kimi', 0.9],
|
|
42
|
+
['minimax', 0.85],
|
|
43
|
+
['llama', 0.8],
|
|
44
|
+
['mistral', 0.8],
|
|
45
|
+
['nemotron', 0.8],
|
|
46
|
+
['mimo', 0.75],
|
|
47
|
+
['gemma', 0.75]
|
|
48
|
+
];
|
|
49
|
+
const SOURCE_ORDER = {
|
|
50
|
+
'models.dev': 0,
|
|
51
|
+
openrouter: 1,
|
|
52
|
+
builtin: 2
|
|
53
|
+
};
|
|
54
|
+
/** Теневые цены платных аналогов: ключ → приор качества */
|
|
55
|
+
const shadowPrices = new Map();
|
|
56
|
+
/** Модели с alpha/preview-статусом: ключ → штраф */
|
|
57
|
+
const penalizedModels = new Set();
|
|
58
|
+
function clamp01(value) {
|
|
59
|
+
if (!Number.isFinite(value) || value < 0)
|
|
60
|
+
return 0;
|
|
61
|
+
return value > 1 ? 1 : value;
|
|
62
|
+
}
|
|
63
|
+
function bumpMax(map, key, value) {
|
|
64
|
+
const previous = map.get(key);
|
|
65
|
+
if (previous === undefined || value > previous)
|
|
66
|
+
map.set(key, value);
|
|
67
|
+
}
|
|
68
|
+
function registryKeys(model) {
|
|
69
|
+
const keys = [];
|
|
70
|
+
if (model.canonicalID)
|
|
71
|
+
keys.push(model.canonicalID.toLowerCase());
|
|
72
|
+
if (model.modelID)
|
|
73
|
+
keys.push(model.modelID.toLowerCase());
|
|
74
|
+
if (model.id)
|
|
75
|
+
keys.push(model.id.toLowerCase());
|
|
76
|
+
return keys;
|
|
77
|
+
}
|
|
78
|
+
function rememberShadow(model, prior) {
|
|
79
|
+
if (prior <= 0)
|
|
80
|
+
return;
|
|
81
|
+
for (const key of registryKeys(model))
|
|
82
|
+
bumpMax(shadowPrices, key, prior);
|
|
83
|
+
}
|
|
84
|
+
function rememberPenalty(model) {
|
|
85
|
+
for (const key of registryKeys(model))
|
|
86
|
+
penalizedModels.add(key);
|
|
87
|
+
}
|
|
88
|
+
function releaseRecency(releaseDate, now) {
|
|
89
|
+
if (!releaseDate)
|
|
90
|
+
return 0.5;
|
|
91
|
+
const timestamp = Date.parse(releaseDate);
|
|
92
|
+
if (!Number.isFinite(timestamp))
|
|
93
|
+
return 0.5;
|
|
94
|
+
const ageDays = Math.max(0, (now - timestamp) / 86_400_000);
|
|
95
|
+
return clamp01(Math.exp(-ageDays / 240));
|
|
96
|
+
}
|
|
97
|
+
function familyPriorFor(text) {
|
|
98
|
+
const lower = text.toLowerCase();
|
|
99
|
+
let prior = 0.6;
|
|
100
|
+
for (const [family, value] of FAMILY_PRIOR) {
|
|
101
|
+
if (lower.includes(family) && value > prior)
|
|
102
|
+
prior = value;
|
|
103
|
+
}
|
|
104
|
+
return prior;
|
|
105
|
+
}
|
|
106
|
+
function shadowPriceFor(model) {
|
|
107
|
+
for (const key of registryKeys(model)) {
|
|
108
|
+
const value = shadowPrices.get(key);
|
|
109
|
+
if (value !== undefined)
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
function isPenalized(model) {
|
|
115
|
+
if (ALPHA_PATTERN.test(model.name) || ALPHA_PATTERN.test(model.id) || ALPHA_PATTERN.test(model.modelID)) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
return registryKeys(model).some(key => penalizedModels.has(key));
|
|
119
|
+
}
|
|
120
|
+
function computeScore(model, now) {
|
|
121
|
+
const contextNorm = clamp01(Math.log2(Math.max(model.context, 1) / 8192) / 7);
|
|
122
|
+
const outputNorm = clamp01(Math.log2(Math.max(model.outputLimit, 1) / 8192) / 4);
|
|
123
|
+
const recency = releaseRecency(model.releaseDate, now);
|
|
124
|
+
const familyPrior = familyPriorFor(`${model.name} ${model.modelID}`);
|
|
125
|
+
const shadow = shadowPriceFor(model);
|
|
126
|
+
let score = 0.30 * contextNorm +
|
|
127
|
+
0.20 * (model.toolCall ? 1 : 0) +
|
|
128
|
+
0.10 * (model.structuredOutput ? 1 : 0) +
|
|
129
|
+
0.10 * (model.reasoning ? 1 : 0) +
|
|
130
|
+
0.10 * recency +
|
|
131
|
+
0.10 * outputNorm +
|
|
132
|
+
0.10 * familyPrior +
|
|
133
|
+
0.15 * shadow;
|
|
134
|
+
if (isPenalized(model))
|
|
135
|
+
score -= 0.25;
|
|
136
|
+
if (model.available)
|
|
137
|
+
score += 0.05;
|
|
138
|
+
return Math.round(score * 1_000_000) / 1_000_000;
|
|
139
|
+
}
|
|
140
|
+
function canonicalKey(model) {
|
|
141
|
+
const key = model.canonicalID && model.canonicalID.length > 0 ? model.canonicalID : model.modelID;
|
|
142
|
+
return key.toLowerCase();
|
|
143
|
+
}
|
|
144
|
+
function compareRanked(a, b) {
|
|
145
|
+
if (b.score !== a.score)
|
|
146
|
+
return b.score - a.score;
|
|
147
|
+
if (a.available !== b.available)
|
|
148
|
+
return a.available ? -1 : 1;
|
|
149
|
+
if (b.context !== a.context)
|
|
150
|
+
return b.context - a.context;
|
|
151
|
+
if (a.id === b.id)
|
|
152
|
+
return 0;
|
|
153
|
+
return a.id < b.id ? -1 : 1;
|
|
154
|
+
}
|
|
155
|
+
function isBetter(candidate, current) {
|
|
156
|
+
if (candidate.score !== current.score)
|
|
157
|
+
return candidate.score > current.score;
|
|
158
|
+
if (candidate.available !== current.available)
|
|
159
|
+
return candidate.available;
|
|
160
|
+
const sourceDelta = SOURCE_ORDER[candidate.source] - SOURCE_ORDER[current.source];
|
|
161
|
+
if (sourceDelta !== 0)
|
|
162
|
+
return sourceDelta < 0;
|
|
163
|
+
return candidate.id < current.id;
|
|
164
|
+
}
|
|
165
|
+
function sortRanked(models, includeUnavailable) {
|
|
166
|
+
const filtered = includeUnavailable ? models : models.filter(model => model.available);
|
|
167
|
+
const best = new Map();
|
|
168
|
+
for (const model of filtered) {
|
|
169
|
+
const key = canonicalKey(model);
|
|
170
|
+
const current = best.get(key);
|
|
171
|
+
if (!current || isBetter(model, current))
|
|
172
|
+
best.set(key, model);
|
|
173
|
+
}
|
|
174
|
+
return Array.from(best.values()).sort(compareRanked);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Оценить и отсортировать бесплатные модели
|
|
178
|
+
*/
|
|
179
|
+
export function rankFreeModels(models, opts = {}) {
|
|
180
|
+
const now = Date.now();
|
|
181
|
+
const scored = models.map(model => ({ ...model, score: computeScore(model, now) }));
|
|
182
|
+
return sortRanked(scored, opts.includeUnavailable ?? true);
|
|
183
|
+
}
|
|
184
|
+
const BUILTIN_FREE_MODELS_RAW = [
|
|
185
|
+
{
|
|
186
|
+
id: 'opencode/beam-free',
|
|
187
|
+
providerID: 'opencode',
|
|
188
|
+
modelID: 'beam-free',
|
|
189
|
+
name: 'Beam Free',
|
|
190
|
+
context: 1_048_576,
|
|
191
|
+
outputLimit: 131_072,
|
|
192
|
+
reasoning: true,
|
|
193
|
+
toolCall: true,
|
|
194
|
+
structuredOutput: true,
|
|
195
|
+
releaseDate: '2026-10-05',
|
|
196
|
+
score: 0,
|
|
197
|
+
available: true,
|
|
198
|
+
source: 'builtin'
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
id: 'opencode/inkling-2-small-free',
|
|
202
|
+
providerID: 'opencode',
|
|
203
|
+
modelID: 'inkling-2-small-free',
|
|
204
|
+
name: 'Inkling 2 Small Free',
|
|
205
|
+
context: 1_048_576,
|
|
206
|
+
outputLimit: 131_072,
|
|
207
|
+
reasoning: true,
|
|
208
|
+
toolCall: true,
|
|
209
|
+
structuredOutput: false,
|
|
210
|
+
releaseDate: '2026-11-05',
|
|
211
|
+
score: 0,
|
|
212
|
+
available: true,
|
|
213
|
+
source: 'builtin'
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: 'opencode/flint-40b-free',
|
|
217
|
+
providerID: 'opencode',
|
|
218
|
+
modelID: 'flint-40b-free',
|
|
219
|
+
name: 'Flint 40B Free',
|
|
220
|
+
context: 262_144,
|
|
221
|
+
outputLimit: 65_536,
|
|
222
|
+
reasoning: true,
|
|
223
|
+
toolCall: true,
|
|
224
|
+
structuredOutput: true,
|
|
225
|
+
releaseDate: '2026-10-21',
|
|
226
|
+
score: 0,
|
|
227
|
+
available: true,
|
|
228
|
+
source: 'builtin'
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: 'opencode/deepseek-v4.1-flash-free',
|
|
232
|
+
providerID: 'opencode',
|
|
233
|
+
modelID: 'deepseek-v4.1-flash-free',
|
|
234
|
+
name: 'DeepSeek V4.1 Flash Free',
|
|
235
|
+
context: 200_000,
|
|
236
|
+
outputLimit: 32_768,
|
|
237
|
+
reasoning: true,
|
|
238
|
+
toolCall: true,
|
|
239
|
+
structuredOutput: true,
|
|
240
|
+
releaseDate: '2026-09-10',
|
|
241
|
+
score: 0,
|
|
242
|
+
available: true,
|
|
243
|
+
source: 'builtin'
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
id: 'opencode/iceman-free',
|
|
247
|
+
providerID: 'opencode',
|
|
248
|
+
modelID: 'iceman-free',
|
|
249
|
+
name: 'Iceman Free',
|
|
250
|
+
context: 262_144,
|
|
251
|
+
outputLimit: 131_072,
|
|
252
|
+
reasoning: true,
|
|
253
|
+
toolCall: true,
|
|
254
|
+
structuredOutput: false,
|
|
255
|
+
releaseDate: '2026-11-06',
|
|
256
|
+
score: 0,
|
|
257
|
+
available: true,
|
|
258
|
+
source: 'builtin'
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: 'opencode/nemotron-3.5-super-vl-free',
|
|
262
|
+
providerID: 'opencode',
|
|
263
|
+
modelID: 'nemotron-3.5-super-vl-free',
|
|
264
|
+
name: 'Nemotron 3.5 Super VL Free',
|
|
265
|
+
context: 1_048_576,
|
|
266
|
+
outputLimit: 131_072,
|
|
267
|
+
reasoning: true,
|
|
268
|
+
toolCall: true,
|
|
269
|
+
structuredOutput: false,
|
|
270
|
+
releaseDate: '2026-10-15',
|
|
271
|
+
score: 0,
|
|
272
|
+
available: true,
|
|
273
|
+
source: 'builtin'
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
id: 'opencode/crouton-alpha-free',
|
|
277
|
+
providerID: 'opencode',
|
|
278
|
+
modelID: 'crouton-alpha-free',
|
|
279
|
+
name: 'Crouton Alpha Free',
|
|
280
|
+
context: 1_048_576,
|
|
281
|
+
outputLimit: 270_000,
|
|
282
|
+
reasoning: true,
|
|
283
|
+
toolCall: true,
|
|
284
|
+
structuredOutput: true,
|
|
285
|
+
releaseDate: '2026-11-03',
|
|
286
|
+
score: 0,
|
|
287
|
+
available: true,
|
|
288
|
+
source: 'builtin'
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
id: 'opencode/glm-5.3-flash-free',
|
|
292
|
+
providerID: 'opencode',
|
|
293
|
+
modelID: 'glm-5.3-flash-free',
|
|
294
|
+
name: 'GLM 5.3 Flash Free',
|
|
295
|
+
context: 200_000,
|
|
296
|
+
outputLimit: 32_768,
|
|
297
|
+
reasoning: true,
|
|
298
|
+
toolCall: true,
|
|
299
|
+
structuredOutput: true,
|
|
300
|
+
releaseDate: '2026-08-26',
|
|
301
|
+
score: 0,
|
|
302
|
+
available: true,
|
|
303
|
+
source: 'builtin'
|
|
304
|
+
}
|
|
305
|
+
];
|
|
306
|
+
/** Проверенный офлайн-набор бесплатных моделей */
|
|
307
|
+
export const BUILTIN_FREE_MODELS = rankFreeModels(BUILTIN_FREE_MODELS_RAW, {
|
|
308
|
+
includeUnavailable: true
|
|
309
|
+
});
|
|
310
|
+
function familyKey(model) {
|
|
311
|
+
const raw = (model.modelID || model.name).toLowerCase();
|
|
312
|
+
const last = raw.slice(raw.lastIndexOf('/') + 1);
|
|
313
|
+
const cleaned = stripFreeSuffix(last).replace(/-free$/, '');
|
|
314
|
+
const tokens = cleaned.split(/[^a-z0-9]+/).filter(token => token.length > 0);
|
|
315
|
+
const meaningful = tokens.filter(token => !GENERIC_FAMILY_TOKENS.has(token));
|
|
316
|
+
const first = meaningful[0] ?? tokens[0] ?? cleaned;
|
|
317
|
+
const second = meaningful[1];
|
|
318
|
+
const family = first.length <= 2 && second ? `${first}${second}` : first;
|
|
319
|
+
return `${model.providerID}:${family}`;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Выбрать N моделей с разными семействами провайдеров
|
|
323
|
+
*/
|
|
324
|
+
export function selectCouncilModels(models, size = 5) {
|
|
325
|
+
const limit = Number.isFinite(size) ? Math.max(0, Math.floor(size)) : 5;
|
|
326
|
+
if (limit === 0 || models.length === 0)
|
|
327
|
+
return [];
|
|
328
|
+
const ranked = sortRanked(models, true);
|
|
329
|
+
const available = ranked.filter(model => model.available);
|
|
330
|
+
const pool = available.length > 0 ? available : ranked;
|
|
331
|
+
const selected = [];
|
|
332
|
+
const seenFamilies = new Set();
|
|
333
|
+
for (const model of pool) {
|
|
334
|
+
const key = familyKey(model);
|
|
335
|
+
if (seenFamilies.has(key))
|
|
336
|
+
continue;
|
|
337
|
+
seenFamilies.add(key);
|
|
338
|
+
selected.push(model);
|
|
339
|
+
if (selected.length >= limit)
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
if (selected.length < limit) {
|
|
343
|
+
const selectedIds = new Set(selected.map(model => model.id));
|
|
344
|
+
for (const model of pool) {
|
|
345
|
+
if (selectedIds.has(model.id))
|
|
346
|
+
continue;
|
|
347
|
+
selected.push(model);
|
|
348
|
+
selectedIds.add(model.id);
|
|
349
|
+
if (selected.length >= limit)
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (!selected.some(model => model.toolCall)) {
|
|
354
|
+
const candidate = pool.find(model => model.toolCall && !selected.includes(model));
|
|
355
|
+
if (candidate) {
|
|
356
|
+
if (selected.length === 0)
|
|
357
|
+
selected.push(candidate);
|
|
358
|
+
else
|
|
359
|
+
selected[selected.length - 1] = candidate;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return selected;
|
|
363
|
+
}
|
|
364
|
+
function expertPrompt(name) {
|
|
365
|
+
const template = Object.prototype.hasOwnProperty.call(DEFAULT_EXPERT_PROMPTS, name)
|
|
366
|
+
? DEFAULT_EXPERT_PROMPTS[name]
|
|
367
|
+
: undefined;
|
|
368
|
+
if (typeof template === 'function') {
|
|
369
|
+
const text = template('').trim();
|
|
370
|
+
if (text.length > 0)
|
|
371
|
+
return text;
|
|
372
|
+
}
|
|
373
|
+
else if (typeof template === 'string' && template.trim().length > 0) {
|
|
374
|
+
return template.trim();
|
|
375
|
+
}
|
|
376
|
+
return `Ты эксперт консилиума (${name}). Анализируй задачу кратко, по делу и в рамках своей роли.`;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Собрать агентов консилиума (эксперты + председатель)
|
|
380
|
+
*/
|
|
381
|
+
export function buildCouncilAgents(models, names = DEFAULT_AGENT_NAMES) {
|
|
382
|
+
const agentNames = names.length > 0 ? names : DEFAULT_AGENT_NAMES;
|
|
383
|
+
const selected = selectCouncilModels(models, agentNames.length);
|
|
384
|
+
if (selected.length === 0) {
|
|
385
|
+
throw new Error('Нет доступных моделей для консилиума');
|
|
386
|
+
}
|
|
387
|
+
const chairIndex = Math.min(4, agentNames.length - 1);
|
|
388
|
+
const agents = {};
|
|
389
|
+
for (let index = 0; index < selected.length; index += 1) {
|
|
390
|
+
const name = agentNames[index % agentNames.length];
|
|
391
|
+
agents[name] = {
|
|
392
|
+
name,
|
|
393
|
+
mode: 'primary',
|
|
394
|
+
model: selected[index].id,
|
|
395
|
+
prompt: index === chairIndex ? CHAIR_PROMPT : expertPrompt(name)
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
return agents;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Собрать конфигурацию консилиума из моделей
|
|
402
|
+
*/
|
|
403
|
+
export function buildConsiliumConfigFromModels(models) {
|
|
404
|
+
const selected = selectCouncilModels(models, 5);
|
|
405
|
+
if (selected.length === 0) {
|
|
406
|
+
throw new Error('Нет доступных моделей для консилиума');
|
|
407
|
+
}
|
|
408
|
+
const expertNames = ['arch', 'ux', 'ba', 'sec'];
|
|
409
|
+
const experts = selected.slice(0, expertNames.length).map((model, index) => ({
|
|
410
|
+
name: expertNames[index],
|
|
411
|
+
model: model.id,
|
|
412
|
+
enabled: true,
|
|
413
|
+
priority: index + 1
|
|
414
|
+
}));
|
|
415
|
+
const chairModel = selected[4] ?? selected[0];
|
|
416
|
+
return {
|
|
417
|
+
experts,
|
|
418
|
+
chair: {
|
|
419
|
+
agent: 'consilium',
|
|
420
|
+
model: chairModel.id,
|
|
421
|
+
maxExpertTextLength: 3000
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Загрузить модели и сразу выбрать состав консилиума
|
|
427
|
+
*/
|
|
428
|
+
export async function getCouncilModels(opts = {}) {
|
|
429
|
+
const result = await fetchFreeModels(opts);
|
|
430
|
+
return {
|
|
431
|
+
models: result.models,
|
|
432
|
+
council: selectCouncilModels(result.models, 5),
|
|
433
|
+
result
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
function isRecord(value) {
|
|
437
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
438
|
+
}
|
|
439
|
+
function num(value) {
|
|
440
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
441
|
+
}
|
|
442
|
+
function str(value) {
|
|
443
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
444
|
+
}
|
|
445
|
+
function strArray(value) {
|
|
446
|
+
if (!Array.isArray(value))
|
|
447
|
+
return undefined;
|
|
448
|
+
const result = [];
|
|
449
|
+
for (const item of value) {
|
|
450
|
+
if (typeof item !== 'string')
|
|
451
|
+
return undefined;
|
|
452
|
+
result.push(item);
|
|
453
|
+
}
|
|
454
|
+
return result;
|
|
455
|
+
}
|
|
456
|
+
function stripFreeSuffix(value) {
|
|
457
|
+
return value.replace(/:free$/i, '');
|
|
458
|
+
}
|
|
459
|
+
function normalizeReleaseDate(value) {
|
|
460
|
+
const date = str(value);
|
|
461
|
+
if (!date)
|
|
462
|
+
return undefined;
|
|
463
|
+
const match = /^\d{4}-\d{2}-\d{2}/.exec(date);
|
|
464
|
+
return match ? match[0] : undefined;
|
|
465
|
+
}
|
|
466
|
+
function dateFromUnixSeconds(value) {
|
|
467
|
+
const seconds = num(value);
|
|
468
|
+
if (seconds === undefined)
|
|
469
|
+
return undefined;
|
|
470
|
+
const date = new Date(seconds * 1000);
|
|
471
|
+
if (Number.isNaN(date.getTime()))
|
|
472
|
+
return undefined;
|
|
473
|
+
return date.toISOString().slice(0, 10);
|
|
474
|
+
}
|
|
475
|
+
function isExpired(value, now) {
|
|
476
|
+
const expiration = str(value);
|
|
477
|
+
if (!expiration)
|
|
478
|
+
return false;
|
|
479
|
+
const timestamp = Date.parse(expiration);
|
|
480
|
+
return Number.isFinite(timestamp) && timestamp < now;
|
|
481
|
+
}
|
|
482
|
+
function providerAvailable(providerID, env) {
|
|
483
|
+
if (providerID === 'opencode')
|
|
484
|
+
return true;
|
|
485
|
+
if (!env || env.length === 0)
|
|
486
|
+
return true;
|
|
487
|
+
return env.some(key => Boolean(process.env[key]));
|
|
488
|
+
}
|
|
489
|
+
function parseModelsDev(data, minContext) {
|
|
490
|
+
if (!isRecord(data))
|
|
491
|
+
return { models: [], count: 0 };
|
|
492
|
+
const providers = [];
|
|
493
|
+
for (const [id, value] of Object.entries(data)) {
|
|
494
|
+
if (isExcludedFreeProvider(id) || !isRecord(value))
|
|
495
|
+
continue;
|
|
496
|
+
const models = value.models;
|
|
497
|
+
if (!isRecord(models))
|
|
498
|
+
continue;
|
|
499
|
+
providers.push({ id, env: strArray(value.env), models });
|
|
500
|
+
}
|
|
501
|
+
const byCanonical = new Map();
|
|
502
|
+
const byFamily = new Map();
|
|
503
|
+
for (const provider of providers) {
|
|
504
|
+
for (const value of Object.values(provider.models)) {
|
|
505
|
+
if (!isRecord(value))
|
|
506
|
+
continue;
|
|
507
|
+
const cost = isRecord(value.cost) ? value.cost : undefined;
|
|
508
|
+
const output = cost ? num(cost.output) : undefined;
|
|
509
|
+
if (output === undefined || output <= 0)
|
|
510
|
+
continue;
|
|
511
|
+
const prior = Math.log1p(output) / Math.log1p(50);
|
|
512
|
+
const canonical = str(value.canonical_model_id);
|
|
513
|
+
if (canonical)
|
|
514
|
+
bumpMax(byCanonical, canonical.toLowerCase(), prior);
|
|
515
|
+
const family = str(value.family);
|
|
516
|
+
if (family)
|
|
517
|
+
bumpMax(byFamily, family.toLowerCase(), prior);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const models = [];
|
|
521
|
+
for (const provider of providers) {
|
|
522
|
+
const available = providerAvailable(provider.id, provider.env);
|
|
523
|
+
for (const [modelID, value] of Object.entries(provider.models)) {
|
|
524
|
+
if (!isRecord(value))
|
|
525
|
+
continue;
|
|
526
|
+
const status = str(value.status);
|
|
527
|
+
if (status === 'deprecated')
|
|
528
|
+
continue;
|
|
529
|
+
const type = str(value.type);
|
|
530
|
+
if (type !== undefined && type !== 'chat')
|
|
531
|
+
continue;
|
|
532
|
+
if (value.tool_call !== true)
|
|
533
|
+
continue;
|
|
534
|
+
const cost = isRecord(value.cost) ? value.cost : undefined;
|
|
535
|
+
if (!cost)
|
|
536
|
+
continue;
|
|
537
|
+
if (num(cost.input) !== 0 || num(cost.output) !== 0)
|
|
538
|
+
continue;
|
|
539
|
+
const limit = isRecord(value.limit) ? value.limit : undefined;
|
|
540
|
+
const context = limit ? num(limit.context) : undefined;
|
|
541
|
+
if (context === undefined || context < minContext)
|
|
542
|
+
continue;
|
|
543
|
+
const outputLimit = limit ? num(limit.output) : undefined;
|
|
544
|
+
const name = str(value.name) ?? str(value.id) ?? modelID;
|
|
545
|
+
const releaseDate = normalizeReleaseDate(value.release_date);
|
|
546
|
+
const canonicalID = str(value.canonical_model_id) ?? stripFreeSuffix(modelID);
|
|
547
|
+
const family = str(value.family) ?? '';
|
|
548
|
+
const canonicalPrior = byCanonical.get(canonicalID.toLowerCase());
|
|
549
|
+
const familyPrior = family ? byFamily.get(family.toLowerCase()) : undefined;
|
|
550
|
+
const prior = canonicalPrior ?? familyPrior ?? 0;
|
|
551
|
+
const model = {
|
|
552
|
+
id: `${provider.id}/${modelID}`,
|
|
553
|
+
providerID: provider.id,
|
|
554
|
+
modelID,
|
|
555
|
+
name,
|
|
556
|
+
context,
|
|
557
|
+
outputLimit: outputLimit !== undefined && outputLimit > 0 ? outputLimit : 0,
|
|
558
|
+
reasoning: value.reasoning === true,
|
|
559
|
+
toolCall: true,
|
|
560
|
+
structuredOutput: value.structured_output === true,
|
|
561
|
+
releaseDate,
|
|
562
|
+
canonicalID,
|
|
563
|
+
score: 0,
|
|
564
|
+
available,
|
|
565
|
+
source: 'models.dev',
|
|
566
|
+
env: provider.env
|
|
567
|
+
};
|
|
568
|
+
rememberShadow(model, prior);
|
|
569
|
+
if (status === 'alpha' || value.experimental === true)
|
|
570
|
+
rememberPenalty(model);
|
|
571
|
+
models.push(model);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return { models, count: models.length };
|
|
575
|
+
}
|
|
576
|
+
function parseOpenRouter(data, minContext, now) {
|
|
577
|
+
const root = isRecord(data) ? data : undefined;
|
|
578
|
+
const list = root && Array.isArray(root.data) ? root.data : [];
|
|
579
|
+
const entries = [];
|
|
580
|
+
for (const item of list) {
|
|
581
|
+
if (!isRecord(item))
|
|
582
|
+
continue;
|
|
583
|
+
const id = str(item.id);
|
|
584
|
+
if (!id)
|
|
585
|
+
continue;
|
|
586
|
+
entries.push({ id, raw: item });
|
|
587
|
+
}
|
|
588
|
+
const paid = new Map();
|
|
589
|
+
for (const entry of entries) {
|
|
590
|
+
const pricing = isRecord(entry.raw.pricing) ? entry.raw.pricing : undefined;
|
|
591
|
+
if (!pricing)
|
|
592
|
+
continue;
|
|
593
|
+
const completion = Number(pricing.completion);
|
|
594
|
+
if (!Number.isFinite(completion) || completion <= 0)
|
|
595
|
+
continue;
|
|
596
|
+
const perMillion = completion * 1_000_000;
|
|
597
|
+
const prior = Math.log1p(perMillion) / Math.log1p(50);
|
|
598
|
+
const canonical = str(entry.raw.canonical_slug);
|
|
599
|
+
if (canonical)
|
|
600
|
+
bumpMax(paid, canonical.toLowerCase(), prior);
|
|
601
|
+
bumpMax(paid, stripFreeSuffix(entry.id).toLowerCase(), prior);
|
|
602
|
+
}
|
|
603
|
+
const available = Boolean(process.env.OPENROUTER_API_KEY);
|
|
604
|
+
const models = [];
|
|
605
|
+
for (const entry of entries) {
|
|
606
|
+
if (entry.id.toLowerCase() === 'openrouter/free')
|
|
607
|
+
continue;
|
|
608
|
+
const raw = entry.raw;
|
|
609
|
+
const pricing = isRecord(raw.pricing) ? raw.pricing : undefined;
|
|
610
|
+
if (!pricing)
|
|
611
|
+
continue;
|
|
612
|
+
if (String(pricing.prompt) !== '0' || String(pricing.completion) !== '0')
|
|
613
|
+
continue;
|
|
614
|
+
const architecture = isRecord(raw.architecture) ? raw.architecture : undefined;
|
|
615
|
+
const outputs = architecture ? strArray(architecture.output_modalities) : undefined;
|
|
616
|
+
if (!outputs || !outputs.includes('text'))
|
|
617
|
+
continue;
|
|
618
|
+
const supported = strArray(raw.supported_parameters) ?? [];
|
|
619
|
+
if (!supported.includes('tools'))
|
|
620
|
+
continue;
|
|
621
|
+
const name = str(raw.name) ?? entry.id;
|
|
622
|
+
if (AUDIO_PATTERN.test(entry.id) || AUDIO_PATTERN.test(name))
|
|
623
|
+
continue;
|
|
624
|
+
if (isExpired(raw.expiration_date, now))
|
|
625
|
+
continue;
|
|
626
|
+
const topProvider = isRecord(raw.top_provider) ? raw.top_provider : undefined;
|
|
627
|
+
const topContext = topProvider ? num(topProvider.context_length) : undefined;
|
|
628
|
+
const context = topContext ?? num(raw.context_length);
|
|
629
|
+
if (context === undefined || context < minContext)
|
|
630
|
+
continue;
|
|
631
|
+
const outputLimit = topProvider ? num(topProvider.max_completion_tokens) : undefined;
|
|
632
|
+
const canonicalID = str(raw.canonical_slug) ?? stripFreeSuffix(entry.id);
|
|
633
|
+
const prior = paid.get(canonicalID.toLowerCase()) ?? paid.get(stripFreeSuffix(entry.id).toLowerCase()) ?? 0;
|
|
634
|
+
const model = {
|
|
635
|
+
id: `openrouter/${entry.id}`,
|
|
636
|
+
providerID: 'openrouter',
|
|
637
|
+
modelID: entry.id,
|
|
638
|
+
name,
|
|
639
|
+
context,
|
|
640
|
+
outputLimit: outputLimit !== undefined && outputLimit > 0 ? outputLimit : 0,
|
|
641
|
+
reasoning: supported.includes('reasoning') || supported.includes('include_reasoning'),
|
|
642
|
+
toolCall: true,
|
|
643
|
+
structuredOutput: supported.includes('structured_outputs') ||
|
|
644
|
+
supported.includes('structured_output') ||
|
|
645
|
+
supported.includes('response_format'),
|
|
646
|
+
releaseDate: dateFromUnixSeconds(raw.created),
|
|
647
|
+
canonicalID,
|
|
648
|
+
score: 0,
|
|
649
|
+
available,
|
|
650
|
+
source: 'openrouter',
|
|
651
|
+
env: ['OPENROUTER_API_KEY']
|
|
652
|
+
};
|
|
653
|
+
rememberShadow(model, prior);
|
|
654
|
+
models.push(model);
|
|
655
|
+
}
|
|
656
|
+
return { models, count: models.length };
|
|
657
|
+
}
|
|
658
|
+
async function fetchJson(url, timeoutMs) {
|
|
659
|
+
const controller = new AbortController();
|
|
660
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
661
|
+
try {
|
|
662
|
+
const response = await fetch(url, {
|
|
663
|
+
signal: controller.signal,
|
|
664
|
+
headers: {
|
|
665
|
+
accept: 'application/json',
|
|
666
|
+
'user-agent': 'lv24-opencode-consilium/1.0'
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
if (!response.ok) {
|
|
670
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}`.trim());
|
|
671
|
+
}
|
|
672
|
+
const data = await response.json();
|
|
673
|
+
return data;
|
|
674
|
+
}
|
|
675
|
+
finally {
|
|
676
|
+
clearTimeout(timer);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
async function loadModelsDev(timeoutMs, minContext) {
|
|
680
|
+
const data = await fetchJson(MODELS_DEV_URL, timeoutMs);
|
|
681
|
+
return parseModelsDev(data, minContext);
|
|
682
|
+
}
|
|
683
|
+
async function loadOpenRouter(timeoutMs, minContext) {
|
|
684
|
+
const data = await fetchJson(OPENROUTER_URL, timeoutMs);
|
|
685
|
+
return parseOpenRouter(data, minContext, Date.now());
|
|
686
|
+
}
|
|
687
|
+
function errorMessage(reason) {
|
|
688
|
+
return reason instanceof Error ? reason.message : String(reason);
|
|
689
|
+
}
|
|
690
|
+
function toSourceStatus(source, settled) {
|
|
691
|
+
if (settled.status === 'fulfilled') {
|
|
692
|
+
return { source, ok: true, count: settled.value.count };
|
|
693
|
+
}
|
|
694
|
+
return { source, ok: false, count: 0, error: errorMessage(settled.reason) };
|
|
695
|
+
}
|
|
696
|
+
function isFreeModel(value) {
|
|
697
|
+
if (!isRecord(value))
|
|
698
|
+
return false;
|
|
699
|
+
if (typeof value.id !== 'string' || typeof value.providerID !== 'string' || typeof value.modelID !== 'string') {
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
if (typeof value.name !== 'string')
|
|
703
|
+
return false;
|
|
704
|
+
if (typeof value.context !== 'number' || !Number.isFinite(value.context))
|
|
705
|
+
return false;
|
|
706
|
+
if (typeof value.outputLimit !== 'number' || !Number.isFinite(value.outputLimit))
|
|
707
|
+
return false;
|
|
708
|
+
if (typeof value.reasoning !== 'boolean' || typeof value.toolCall !== 'boolean')
|
|
709
|
+
return false;
|
|
710
|
+
if (typeof value.structuredOutput !== 'boolean' || typeof value.available !== 'boolean')
|
|
711
|
+
return false;
|
|
712
|
+
if (typeof value.score !== 'number' || !Number.isFinite(value.score))
|
|
713
|
+
return false;
|
|
714
|
+
if (value.source !== 'models.dev' && value.source !== 'openrouter' && value.source !== 'builtin')
|
|
715
|
+
return false;
|
|
716
|
+
if (value.releaseDate !== undefined && typeof value.releaseDate !== 'string')
|
|
717
|
+
return false;
|
|
718
|
+
if (value.canonicalID !== undefined && typeof value.canonicalID !== 'string')
|
|
719
|
+
return false;
|
|
720
|
+
if (value.env !== undefined) {
|
|
721
|
+
if (!Array.isArray(value.env) || !value.env.every(item => typeof item === 'string'))
|
|
722
|
+
return false;
|
|
723
|
+
}
|
|
724
|
+
return true;
|
|
725
|
+
}
|
|
726
|
+
function isSourceStatus(value) {
|
|
727
|
+
if (!isRecord(value))
|
|
728
|
+
return false;
|
|
729
|
+
if (typeof value.source !== 'string' || typeof value.ok !== 'boolean' || typeof value.count !== 'number') {
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
if (value.error !== undefined && typeof value.error !== 'string')
|
|
733
|
+
return false;
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
function resolveCachePath() {
|
|
737
|
+
const fromEnv = process.env.CONSILIUM_MODELS_CACHE;
|
|
738
|
+
if (fromEnv && fromEnv.trim().length > 0)
|
|
739
|
+
return fromEnv;
|
|
740
|
+
return join(tmpdir(), 'lv24-opencode-consilium', 'models-cache.json');
|
|
741
|
+
}
|
|
742
|
+
function readCache(cachePath) {
|
|
743
|
+
try {
|
|
744
|
+
const text = readFileSync(cachePath, 'utf-8');
|
|
745
|
+
const parsed = JSON.parse(text);
|
|
746
|
+
if (!isRecord(parsed))
|
|
747
|
+
return null;
|
|
748
|
+
const fetchedAt = num(parsed.fetchedAt);
|
|
749
|
+
if (fetchedAt === undefined)
|
|
750
|
+
return null;
|
|
751
|
+
const modelsRaw = Array.isArray(parsed.models) ? parsed.models : [];
|
|
752
|
+
const models = modelsRaw.filter(isFreeModel).map(model => ({ ...model }));
|
|
753
|
+
if (models.length === 0)
|
|
754
|
+
return null;
|
|
755
|
+
const sourcesRaw = Array.isArray(parsed.sources) ? parsed.sources : [];
|
|
756
|
+
const sources = sourcesRaw.filter(isSourceStatus).map(source => ({ ...source }));
|
|
757
|
+
return { fetchedAt, models, sources };
|
|
758
|
+
}
|
|
759
|
+
catch {
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function writeCache(cachePath, cache) {
|
|
764
|
+
try {
|
|
765
|
+
mkdirSync(dirname(cachePath), { recursive: true });
|
|
766
|
+
const payload = JSON.stringify(cache);
|
|
767
|
+
const temporary = `${cachePath}.${process.pid}.tmp`;
|
|
768
|
+
writeFileSync(temporary, payload, 'utf-8');
|
|
769
|
+
renameSync(temporary, cachePath);
|
|
770
|
+
}
|
|
771
|
+
catch {
|
|
772
|
+
// Кэш необязателен
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
function prepare(models, minContext, includeUnavailable) {
|
|
776
|
+
const merged = models.map(model => ({ ...model }));
|
|
777
|
+
const seen = new Set(merged.map(model => model.id.toLowerCase()));
|
|
778
|
+
for (const builtin of BUILTIN_FREE_MODELS) {
|
|
779
|
+
if (!seen.has(builtin.id.toLowerCase())) {
|
|
780
|
+
merged.push({ ...builtin });
|
|
781
|
+
seen.add(builtin.id.toLowerCase());
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
const filtered = merged.filter(model => model.context >= minContext && (includeUnavailable || model.available));
|
|
785
|
+
return sortRanked(filtered, true);
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Загрузить бесплатные модели (models.dev + OpenRouter) с кэшем и запасным набором
|
|
789
|
+
*/
|
|
790
|
+
export async function fetchFreeModels(opts = {}) {
|
|
791
|
+
const now = Date.now();
|
|
792
|
+
const timeoutMs = typeof opts.timeoutMs === 'number' && Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0
|
|
793
|
+
? opts.timeoutMs
|
|
794
|
+
: DEFAULT_TIMEOUT_MS;
|
|
795
|
+
const ttl = typeof opts.cacheTtlMs === 'number' && Number.isFinite(opts.cacheTtlMs)
|
|
796
|
+
? opts.cacheTtlMs
|
|
797
|
+
: DEFAULT_CACHE_TTL_MS;
|
|
798
|
+
const minContext = typeof opts.minContext === 'number' && Number.isFinite(opts.minContext) && opts.minContext >= 0
|
|
799
|
+
? opts.minContext
|
|
800
|
+
: DEFAULT_MIN_CONTEXT;
|
|
801
|
+
const includeUnavailable = opts.includeUnavailable === true;
|
|
802
|
+
const cachePath = resolveCachePath();
|
|
803
|
+
const cached = opts.forceRefresh === true ? null : readCache(cachePath);
|
|
804
|
+
if (cached) {
|
|
805
|
+
const age = now - cached.fetchedAt;
|
|
806
|
+
if (ttl > 0 && age >= 0 && age < ttl) {
|
|
807
|
+
const models = prepare(cached.models, minContext, includeUnavailable);
|
|
808
|
+
if (models.length > 0) {
|
|
809
|
+
return {
|
|
810
|
+
models,
|
|
811
|
+
sources: cached.sources,
|
|
812
|
+
fromCache: true,
|
|
813
|
+
fetchedAt: cached.fetchedAt
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
const [modelsDevSettled, openRouterSettled] = await Promise.allSettled([
|
|
819
|
+
loadModelsDev(timeoutMs, minContext),
|
|
820
|
+
loadOpenRouter(timeoutMs, minContext)
|
|
821
|
+
]);
|
|
822
|
+
const sources = [
|
|
823
|
+
toSourceStatus('models.dev', modelsDevSettled),
|
|
824
|
+
toSourceStatus('openrouter', openRouterSettled)
|
|
825
|
+
];
|
|
826
|
+
const networkModels = [];
|
|
827
|
+
if (modelsDevSettled.status === 'fulfilled') {
|
|
828
|
+
networkModels.push(...modelsDevSettled.value.models);
|
|
829
|
+
}
|
|
830
|
+
if (openRouterSettled.status === 'fulfilled') {
|
|
831
|
+
networkModels.push(...openRouterSettled.value.models);
|
|
832
|
+
}
|
|
833
|
+
// Курируемый офлайн-набор дополняет каталог: бесплатные модели часто
|
|
834
|
+
// ротируются и исчезают из models.dev, хотя остаются доступными в OpenCode.
|
|
835
|
+
const seenIds = new Set(networkModels.map(model => model.id.toLowerCase()));
|
|
836
|
+
for (const builtin of BUILTIN_FREE_MODELS) {
|
|
837
|
+
if (!seenIds.has(builtin.id.toLowerCase())) {
|
|
838
|
+
networkModels.push({ ...builtin });
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (networkModels.length === 0) {
|
|
842
|
+
if (cached) {
|
|
843
|
+
const fallbackModels = prepare(cached.models, minContext, includeUnavailable);
|
|
844
|
+
if (fallbackModels.length > 0) {
|
|
845
|
+
return {
|
|
846
|
+
models: fallbackModels,
|
|
847
|
+
sources,
|
|
848
|
+
fromCache: true,
|
|
849
|
+
fetchedAt: cached.fetchedAt
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
return {
|
|
854
|
+
models: prepare(BUILTIN_FREE_MODELS, minContext, includeUnavailable),
|
|
855
|
+
sources,
|
|
856
|
+
fromCache: false,
|
|
857
|
+
fetchedAt: now
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
const fullRanked = rankFreeModels(networkModels, { includeUnavailable: true });
|
|
861
|
+
writeCache(cachePath, { fetchedAt: now, models: fullRanked, sources });
|
|
862
|
+
const models = rankFreeModels(networkModels, { includeUnavailable });
|
|
863
|
+
if (models.length === 0) {
|
|
864
|
+
return {
|
|
865
|
+
models: prepare(BUILTIN_FREE_MODELS, minContext, includeUnavailable),
|
|
866
|
+
sources,
|
|
867
|
+
fromCache: false,
|
|
868
|
+
fetchedAt: now
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
return { models, sources, fromCache: false, fetchedAt: now };
|
|
872
|
+
}
|
|
873
|
+
//# sourceMappingURL=models.js.map
|