analyzthis_design 2.0.0 → 2.1.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/HOW-TO-USE.md +436 -0
- package/README.md +29 -13
- package/agents/cards/evolve-check.md +38 -0
- package/agents/manifests/evolve-check.json +16 -0
- package/dist/HOW-TO-USE.md +15 -3
- package/dist/README.md +29 -13
- package/dist/agents/cards/evolve-check.md +38 -0
- package/dist/agents/manifests/evolve-check.json +16 -0
- package/dist/bin/cli.js +1225 -1
- package/dist/lib/cache.js +111 -1
- package/dist/lib/chunk-executor.js +219 -1
- package/dist/lib/chunk-models.js +228 -1
- package/dist/lib/chunk-planner.js +328 -1
- package/dist/lib/chunk-router.js +66 -1
- package/dist/lib/chunk-run.js +199 -1
- package/dist/lib/chunk-synthesis.js +176 -1
- package/dist/lib/chunk-telemetry.js +88 -1
- package/dist/lib/collect.js +858 -1
- package/dist/lib/cost.js +119 -1
- package/dist/lib/dedup.js +167 -1
- package/dist/lib/deliberation.js +721 -1
- package/dist/lib/design-spec.js +236 -1
- package/dist/lib/evolution-metrics.js +197 -0
- package/dist/lib/evolve.js +361 -1
- package/dist/lib/export.js +77 -1
- package/dist/lib/feedback-submit.js +324 -1
- package/dist/lib/feedback.js +182 -1
- package/dist/lib/host-llm.js +251 -1
- package/dist/lib/install.js +301 -1
- package/dist/lib/knowledge.js +384 -1
- package/dist/lib/lessons.js +217 -1
- package/dist/lib/moodboard.js +563 -1
- package/dist/lib/orchestrator/run.js +935 -1
- package/dist/lib/outcome.js +193 -1
- package/dist/lib/platforms.js +166 -1
- package/dist/lib/provider.js +57 -1
- package/dist/lib/query-expander.js +83 -1
- package/dist/lib/ranker.js +105 -1
- package/dist/lib/reference-pack.js +221 -0
- package/dist/lib/research.js +143 -1
- package/dist/lib/retrieve.js +131 -1
- package/dist/lib/session.js +185 -1
- package/dist/lib/source-discovery.js +486 -1
- package/dist/lib/synthesis.js +155 -1
- package/dist/lib/token-gate.js +46 -1
- package/dist/skills/design-reference/google-fonts.csv +1924 -1924
- package/dist/skills/design-reference/products.csv +162 -162
- package/dist/skills/design-reference/schema.json +159 -0
- package/dist/skills/design-reference/stacks/angular.csv +1 -1
- package/dist/skills/design-reference/stacks/astro.csv +1 -1
- package/dist/skills/design-reference/stacks/laravel.csv +2 -2
- package/dist/skills/design-reference/stacks/threejs.csv +54 -54
- package/dist/skills/design-reference/styles.csv +85 -85
- package/dist/skills/design-reference/typography.csv +75 -74
- package/dist/skills/design-reference/ui-reasoning.csv +1 -1
- package/dist/skills/evolve-check/SKILL.md +106 -0
- package/package.json +8 -8
- package/scripts/validate-csvs.js +197 -0
- package/skills/design-reference/google-fonts.csv +1924 -1924
- package/skills/design-reference/products.csv +162 -162
- package/skills/design-reference/schema.json +159 -0
- package/skills/design-reference/stacks/angular.csv +1 -1
- package/skills/design-reference/stacks/astro.csv +1 -1
- package/skills/design-reference/stacks/laravel.csv +2 -2
- package/skills/design-reference/stacks/threejs.csv +54 -54
- package/skills/design-reference/styles.csv +85 -85
- package/skills/design-reference/typography.csv +75 -74
- package/skills/design-reference/ui-reasoning.csv +1 -1
- package/skills/evolve-check/SKILL.md +106 -0
package/dist/lib/cache.js
CHANGED
|
@@ -1 +1,111 @@
|
|
|
1
|
-
'use strict';
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Small on-disk cache under ~/.analyzthis_design/cache/, keyed by a caller-chosen
|
|
5
|
+
* string (e.g. "retrieve:colors.csv:<filterHash>", "figma:<fileKey>:<nodeId>",
|
|
6
|
+
* "kb:<projectId>:<syncHash>"). Used to avoid re-reading/re-filtering the same
|
|
7
|
+
* CSV rows, Figma node, or knowledge-bank slice on every persona call within a run.
|
|
8
|
+
*
|
|
9
|
+
* Not a distributed cache — this is a single-machine, single-user speed-up for
|
|
10
|
+
* the standalone `run` CLI (and can be reused by IDE-side tooling if desired).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
|
|
18
|
+
const CACHE_ROOT = path.join(os.homedir(), '.analyzthis_design', 'cache');
|
|
19
|
+
|
|
20
|
+
function keyToFile(key) {
|
|
21
|
+
const hash = crypto.createHash('sha1').update(key).digest('hex');
|
|
22
|
+
return path.join(CACHE_ROOT, `${hash}.json`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Build a stable hash for a filter description (object or string), for use in
|
|
27
|
+
* cache keys like `retrieve:colors.csv:{filterHash}`.
|
|
28
|
+
*/
|
|
29
|
+
function hashFilter(filter) {
|
|
30
|
+
const normalized = typeof filter === 'string' ? filter : JSON.stringify(filter, Object.keys(filter).sort());
|
|
31
|
+
return crypto.createHash('sha1').update(normalized).digest('hex').slice(0, 12);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Read a cached value. Returns null on miss, expiry, or corrupt entry.
|
|
36
|
+
* @param {string} key
|
|
37
|
+
* @param {{ ttlMs?: number }} opts - ttlMs of 0/undefined means "no expiry".
|
|
38
|
+
*/
|
|
39
|
+
function get(key, { ttlMs } = {}) {
|
|
40
|
+
const file = keyToFile(key);
|
|
41
|
+
if (!fs.existsSync(file)) return null;
|
|
42
|
+
try {
|
|
43
|
+
const entry = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
44
|
+
if (ttlMs && Date.now() - entry.cachedAt > ttlMs) return null;
|
|
45
|
+
return entry.value;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Write a value to the cache under `key`.
|
|
53
|
+
*/
|
|
54
|
+
function set(key, value) {
|
|
55
|
+
fs.mkdirSync(CACHE_ROOT, { recursive: true });
|
|
56
|
+
const file = keyToFile(key);
|
|
57
|
+
fs.writeFileSync(file, JSON.stringify({ key, cachedAt: Date.now(), value }, null, 2));
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Fetch-through helper: return the cached value for `key` if present (and not
|
|
63
|
+
* expired), otherwise call `computeFn`, cache its result, and return it.
|
|
64
|
+
* @returns {{ value: any, hit: boolean }}
|
|
65
|
+
*/
|
|
66
|
+
function getOrCompute(key, computeFn, { ttlMs } = {}) {
|
|
67
|
+
const cached = get(key, { ttlMs });
|
|
68
|
+
if (cached !== null) return { value: cached, hit: true };
|
|
69
|
+
const value = computeFn();
|
|
70
|
+
set(key, value);
|
|
71
|
+
return { value, hit: false };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Delete one cache entry.
|
|
76
|
+
*/
|
|
77
|
+
function invalidate(key) {
|
|
78
|
+
const file = keyToFile(key);
|
|
79
|
+
if (fs.existsSync(file)) fs.rmSync(file);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Delete every cache entry whose original key starts with `prefix` (e.g.
|
|
84
|
+
* "retrieve:" or "kb:<projectId>:"). Used on `sync` / `session reset`.
|
|
85
|
+
*/
|
|
86
|
+
function invalidatePrefix(prefix) {
|
|
87
|
+
if (!fs.existsSync(CACHE_ROOT)) return 0;
|
|
88
|
+
let removed = 0;
|
|
89
|
+
for (const f of fs.readdirSync(CACHE_ROOT)) {
|
|
90
|
+
const full = path.join(CACHE_ROOT, f);
|
|
91
|
+
try {
|
|
92
|
+
const entry = JSON.parse(fs.readFileSync(full, 'utf8'));
|
|
93
|
+
if (typeof entry.key === 'string' && entry.key.startsWith(prefix)) {
|
|
94
|
+
fs.rmSync(full);
|
|
95
|
+
removed += 1;
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// corrupt entry — leave it; not worth failing the caller over
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return removed;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Clear the entire cache directory.
|
|
106
|
+
*/
|
|
107
|
+
function clearAll() {
|
|
108
|
+
fs.rmSync(CACHE_ROOT, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { CACHE_ROOT, hashFilter, get, set, getOrCompute, invalidate, invalidatePrefix, clearAll };
|
|
@@ -1 +1,219 @@
|
|
|
1
|
-
'use strict';
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Chunk executor (v2.0).
|
|
5
|
+
*
|
|
6
|
+
* Runs a single chunk against a selected model, with one retry and fallback
|
|
7
|
+
* to the next-best model. Never uses Devi/host as the primary chunk executor;
|
|
8
|
+
* it is only used as a last resort if the user has no other models.
|
|
9
|
+
*
|
|
10
|
+
* CommonJS, 'use strict', var.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
var chunkModels = require('./chunk-models');
|
|
14
|
+
var chunkRouter = require('./chunk-router');
|
|
15
|
+
var telemetry = require('./chunk-telemetry');
|
|
16
|
+
var referencePack = require('./reference-pack');
|
|
17
|
+
|
|
18
|
+
function buildChunkPrompt(chunk, priorResults, contextPack) {
|
|
19
|
+
var priorContext = '';
|
|
20
|
+
var deps = chunk.depends_on || [];
|
|
21
|
+
if (deps.length) {
|
|
22
|
+
var lines = [];
|
|
23
|
+
for (var i = 0; i < deps.length; i++) {
|
|
24
|
+
var id = deps[i];
|
|
25
|
+
var found = null;
|
|
26
|
+
for (var j = 0; j < priorResults.length; j++) {
|
|
27
|
+
if (priorResults[j].chunk_id === id) { found = priorResults[j]; break; }
|
|
28
|
+
}
|
|
29
|
+
if (found) {
|
|
30
|
+
lines.push('--- Output from chunk "' + id + '" ---\n' + found.output.slice(0, 1500));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
priorContext = lines.join('\n\n');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
var userParts = [chunk.user_prompt || chunk.goal];
|
|
37
|
+
if (priorContext) {
|
|
38
|
+
userParts.push('', 'Prior chunk outputs you must use:', priorContext);
|
|
39
|
+
}
|
|
40
|
+
if (contextPack) {
|
|
41
|
+
userParts.push('', 'Original task context:', JSON.stringify(contextPack, null, 2).slice(0, 2000));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
system: chunk.system_prompt || ('You are the ' + chunk.persona + ' persona. ' + chunk.goal),
|
|
46
|
+
user: userParts.join('\n'),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseStructured(text, schema) {
|
|
51
|
+
var result = {
|
|
52
|
+
grades: {},
|
|
53
|
+
score: null,
|
|
54
|
+
top_fixes: [],
|
|
55
|
+
verdict: null,
|
|
56
|
+
raw: text,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
var scoreMatch = text.match(/(\d+(?:\.\d+)?)\s*\/\s*5/);
|
|
60
|
+
if (scoreMatch) result.score = Number(scoreMatch[1]);
|
|
61
|
+
|
|
62
|
+
var gradeMatches = text.match(/(\w+)\[([A-F])\]/gi);
|
|
63
|
+
if (gradeMatches) {
|
|
64
|
+
for (var g = 0; g < gradeMatches.length; g++) {
|
|
65
|
+
var m = gradeMatches[g].match(/(\w+)\[([A-F])\]/i);
|
|
66
|
+
if (m) result.grades[m[1].toLowerCase()] = m[2].toUpperCase();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
var fixesMatch = text.match(/Top\s*2\s*fixes:\s*1\.\s*([^\n]+)\s*2\.\s*([^\n]+)/i);
|
|
71
|
+
if (fixesMatch) {
|
|
72
|
+
result.top_fixes = [fixesMatch[1].trim(), fixesMatch[2].trim()];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
var v = text.match(/verdict[\s:]*(SHIP|REVISE|BLOCK)/i);
|
|
76
|
+
if (v) result.verdict = v[1].toUpperCase();
|
|
77
|
+
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function executeChunk(opts) {
|
|
82
|
+
opts = opts || {};
|
|
83
|
+
var chunk = opts.chunk;
|
|
84
|
+
var priorResults = opts.priorResults || [];
|
|
85
|
+
var contextPack = opts.contextPack;
|
|
86
|
+
var state = opts.state;
|
|
87
|
+
var task = (contextPack && contextPack.task) || opts.task || '';
|
|
88
|
+
var config = opts.config || chunkModels.loadConfig();
|
|
89
|
+
var budget = opts.budget || 'auto';
|
|
90
|
+
var callLlmFn = opts.callLlmFn;
|
|
91
|
+
var attempt = 0;
|
|
92
|
+
var maxAttempts = opts.maxAttempts || 3;
|
|
93
|
+
|
|
94
|
+
if (!callLlmFn) {
|
|
95
|
+
var orchestrator = require('./orchestrator/run');
|
|
96
|
+
callLlmFn = orchestrator.callLlm;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Build reference pack (CSV + vault slices) for this chunk's persona.
|
|
100
|
+
var refPack = null;
|
|
101
|
+
var cacheHit = false;
|
|
102
|
+
if (state && task && chunk.persona) {
|
|
103
|
+
try {
|
|
104
|
+
refPack = await referencePack.buildReferencePack(chunk.persona, task, state, callLlmFn);
|
|
105
|
+
if (refPack && refPack.cacheHit) cacheHit = true;
|
|
106
|
+
} catch (e) { /* reference pack unavailable — continue without */ }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
var ranked;
|
|
110
|
+
try {
|
|
111
|
+
var resolved = await chunkRouter.resolveChunkModel({
|
|
112
|
+
effort: chunk.effort || 'standard',
|
|
113
|
+
chunkType: chunk.id + '/' + chunk.persona,
|
|
114
|
+
persona: chunk.persona,
|
|
115
|
+
budget: budget,
|
|
116
|
+
config: config,
|
|
117
|
+
});
|
|
118
|
+
ranked = resolved.ranked;
|
|
119
|
+
} catch (e) {
|
|
120
|
+
return {
|
|
121
|
+
chunk_id: chunk.id,
|
|
122
|
+
success: false,
|
|
123
|
+
error: e.message,
|
|
124
|
+
attempts: 0,
|
|
125
|
+
output: '',
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
var lastError = null;
|
|
130
|
+
var modelIndex = 0;
|
|
131
|
+
|
|
132
|
+
while (modelIndex < ranked.length && attempt < maxAttempts) {
|
|
133
|
+
var model = ranked[modelIndex];
|
|
134
|
+
attempt++;
|
|
135
|
+
|
|
136
|
+
var prompts = buildChunkPrompt(chunk, priorResults, contextPack);
|
|
137
|
+
|
|
138
|
+
// Inject ranked reference citations into the user prompt.
|
|
139
|
+
if (refPack && refPack.citations && refPack.citations.length) {
|
|
140
|
+
prompts.user += '\n\nRelevant references (cite these in your output):\n' +
|
|
141
|
+
refPack.citations.join('\n');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
var start = Date.now();
|
|
145
|
+
try {
|
|
146
|
+
var text = await callLlmFn({
|
|
147
|
+
provider: model.provider,
|
|
148
|
+
model: model.model,
|
|
149
|
+
system: prompts.system,
|
|
150
|
+
user: prompts.user,
|
|
151
|
+
maxTokens: model.context > 16000 ? 2500 : 1200,
|
|
152
|
+
personaId: chunk.persona,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
var inTok = Math.ceil((prompts.system.length + prompts.user.length) / 4);
|
|
156
|
+
var outTok = Math.ceil(text.length / 4);
|
|
157
|
+
|
|
158
|
+
telemetry.record({
|
|
159
|
+
chunk_id: chunk.id,
|
|
160
|
+
persona: chunk.persona,
|
|
161
|
+
model: chunkModels.formatModelName(model),
|
|
162
|
+
provider: model.provider,
|
|
163
|
+
effort: chunk.effort || 'standard',
|
|
164
|
+
input_tokens: inTok,
|
|
165
|
+
output_tokens: outTok,
|
|
166
|
+
cost: chunkModels.estimateCost(model, inTok, outTok),
|
|
167
|
+
success: true,
|
|
168
|
+
attempts: attempt,
|
|
169
|
+
at: new Date().toISOString(),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
chunk_id: chunk.id,
|
|
174
|
+
persona: chunk.persona,
|
|
175
|
+
goal: chunk.goal,
|
|
176
|
+
model_used: { provider: model.provider, model: model.model, cost: model.cost },
|
|
177
|
+
output: text,
|
|
178
|
+
structured_output: parseStructured(text, chunk.output_schema),
|
|
179
|
+
tokens: { input: inTok, output: outTok },
|
|
180
|
+
cost: chunkModels.estimateCost(model, inTok, outTok),
|
|
181
|
+
latency_ms: Date.now() - start,
|
|
182
|
+
attempts: attempt,
|
|
183
|
+
cache_hit: cacheHit,
|
|
184
|
+
references: (refPack && refPack.citations) || [],
|
|
185
|
+
success: true,
|
|
186
|
+
};
|
|
187
|
+
} catch (e) {
|
|
188
|
+
lastError = e.message;
|
|
189
|
+
telemetry.record({
|
|
190
|
+
chunk_id: chunk.id,
|
|
191
|
+
persona: chunk.persona,
|
|
192
|
+
model: chunkModels.formatModelName(model),
|
|
193
|
+
provider: model.provider,
|
|
194
|
+
effort: chunk.effort || 'standard',
|
|
195
|
+
success: false,
|
|
196
|
+
error: e.message,
|
|
197
|
+
attempts: attempt,
|
|
198
|
+
at: new Date().toISOString(),
|
|
199
|
+
});
|
|
200
|
+
modelIndex++;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
chunk_id: chunk.id,
|
|
206
|
+
persona: chunk.persona,
|
|
207
|
+
goal: chunk.goal,
|
|
208
|
+
success: false,
|
|
209
|
+
error: lastError || 'All chunk models failed',
|
|
210
|
+
attempts: attempt,
|
|
211
|
+
output: '',
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
module.exports = {
|
|
216
|
+
executeChunk: executeChunk,
|
|
217
|
+
buildChunkPrompt: buildChunkPrompt,
|
|
218
|
+
parseStructured: parseStructured,
|
|
219
|
+
};
|
package/dist/lib/chunk-models.js
CHANGED
|
@@ -1 +1,228 @@
|
|
|
1
|
-
'use strict';
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Chunk model pool (v2.0).
|
|
5
|
+
*
|
|
6
|
+
* Discovers local Ollama models, maintains a curated list of cloud
|
|
7
|
+
* free/cheap models, and filters by effort, budget, and available API keys.
|
|
8
|
+
*
|
|
9
|
+
* CommonJS, 'use strict', var, try/catch around JSON — compatible with the
|
|
10
|
+
* Llama 30B safety settings used elsewhere in this repo.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
var fs = require('fs');
|
|
14
|
+
var path = require('path');
|
|
15
|
+
var os = require('os');
|
|
16
|
+
var http = require('http');
|
|
17
|
+
|
|
18
|
+
var CONFIG_FILE = path.join(os.homedir(), '.analyzthis_design', 'config.json');
|
|
19
|
+
|
|
20
|
+
function loadConfig() {
|
|
21
|
+
if (!fs.existsSync(CONFIG_FILE)) return {};
|
|
22
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); }
|
|
23
|
+
catch (e) { return {}; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function hasEnv(name) {
|
|
27
|
+
return !!process.env[name];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Curated cloud models. Prices are estimates (USD per 1M tokens input/output).
|
|
31
|
+
// The router treats cost=0 as free; anything >0 as cheap/paid.
|
|
32
|
+
var DEFAULT_CLOUD_POOL = [
|
|
33
|
+
// Free-ish / very cheap with user key
|
|
34
|
+
{ provider: 'groq', model: 'llama-3.1-8b-instant', cost: 0.00005, effort: ['trivial', 'standard'], context: 131072, key_env: 'GROQ_API_KEY' },
|
|
35
|
+
{ provider: 'groq', model: 'mixtral-8x7b-32768', cost: 0.00024, effort: ['trivial', 'standard', 'hard'], context: 32768, key_env: 'GROQ_API_KEY' },
|
|
36
|
+
{ provider: 'google', model: 'gemini-1.5-flash', cost: 0.000075, effort: ['trivial', 'standard', 'hard'], context: 128000, key_env: 'GEMINI_API_KEY' },
|
|
37
|
+
{ provider: 'google', model: 'gemini-1.5-flash-8b', cost: 0.0000375, effort: ['trivial', 'standard'], context: 128000, key_env: 'GEMINI_API_KEY' },
|
|
38
|
+
{ provider: 'deepseek', model: 'deepseek-chat', cost: 0.00007, effort: ['trivial', 'standard', 'hard'], context: 64000, key_env: 'DEEPSEEK_API_KEY' },
|
|
39
|
+
{ provider: 'openrouter', model: 'meta-llama/llama-3.1-8b-instruct:free', cost: 0, effort: ['trivial', 'standard'], context: 131072, key_env: 'OPENROUTER_API_KEY' },
|
|
40
|
+
{ provider: 'together', model: 'meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo', cost: 0.00018, effort: ['trivial', 'standard'], context: 8192, key_env: 'TOGETHER_API_KEY' },
|
|
41
|
+
// Paid but cheap fallback
|
|
42
|
+
{ provider: 'openai', model: 'gpt-4o-mini', cost: 0.00015, effort: ['trivial', 'standard'], context: 128000, key_env: 'OPENAI_API_KEY' },
|
|
43
|
+
// Frontier chunk fallback (only if budget allows)
|
|
44
|
+
{ provider: 'anthropic', model: 'claude-sonnet-4-20250514', cost: 0.003, effort: ['standard', 'hard'], context: 200000, key_env: 'ANTHROPIC_API_KEY' },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// Recommended Ollama models for chunk tasks, ordered by capability/cost trade-off.
|
|
48
|
+
var DEFAULT_OLLAMA_MODELS = [
|
|
49
|
+
{ provider: 'ollama', model: 'llama3.1', cost: 0, effort: ['trivial', 'standard'], context: 8192 },
|
|
50
|
+
{ provider: 'ollama', model: 'mistral', cost: 0, effort: ['trivial', 'standard'], context: 8192 },
|
|
51
|
+
{ provider: 'ollama', model: 'qwen2.5', cost: 0, effort: ['trivial', 'standard', 'hard'], context: 128000 },
|
|
52
|
+
{ provider: 'ollama', model: 'gemma3:4b', cost: 0, effort: ['trivial', 'standard'], context: 8192 },
|
|
53
|
+
{ provider: 'ollama', model: 'phi4', cost: 0, effort: ['trivial', 'standard'], context: 8192 },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
function detectOllama(baseUrl) {
|
|
57
|
+
baseUrl = baseUrl || 'http://localhost:11434';
|
|
58
|
+
return new Promise(function(resolve) {
|
|
59
|
+
var url = new URL('/api/tags', baseUrl);
|
|
60
|
+
var lib = url.protocol === 'https:' ? require('https') : http;
|
|
61
|
+
var req = lib.get(url.toString(), { timeout: 1500 }, function(res) {
|
|
62
|
+
var chunks = [];
|
|
63
|
+
res.on('data', function(c) { chunks.push(c); });
|
|
64
|
+
res.on('end', function() {
|
|
65
|
+
if (res.statusCode !== 200) { resolve([]); return; }
|
|
66
|
+
try {
|
|
67
|
+
var data = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
68
|
+
var models = (data.models || []).map(function(m) { return m.name || m.model; });
|
|
69
|
+
resolve(models);
|
|
70
|
+
} catch (e) { resolve([]); }
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
req.on('error', function() { resolve([]); });
|
|
74
|
+
req.on('timeout', function() { req.destroy(); resolve([]); });
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function filterOllamaModels(available) {
|
|
79
|
+
var result = [];
|
|
80
|
+
for (var i = 0; i < DEFAULT_OLLAMA_MODELS.length; i++) {
|
|
81
|
+
var spec = DEFAULT_OLLAMA_MODELS[i];
|
|
82
|
+
for (var j = 0; j < available.length; j++) {
|
|
83
|
+
var name = available[j];
|
|
84
|
+
// Accept exact match or prefix match (e.g. "llama3.1:latest" matches "llama3.1")
|
|
85
|
+
if (name === spec.model || name.indexOf(spec.model + ':') === 0) {
|
|
86
|
+
result.push(Object.assign({}, spec, { model: name }));
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isKeyAvailable(spec) {
|
|
95
|
+
if (!spec.key_env) return true;
|
|
96
|
+
return hasEnv(spec.key_env);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getUserPool(config) {
|
|
100
|
+
var cfg = config.chunk_models || config.chunkModels || {};
|
|
101
|
+
if (cfg.pool && cfg.pool.length) return cfg.pool;
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function discoverPool(opts) {
|
|
106
|
+
opts = opts || {};
|
|
107
|
+
var config = opts.config || loadConfig();
|
|
108
|
+
var ollamaBase = (config.ollama && config.ollama.base_url) || 'http://localhost:11434';
|
|
109
|
+
var ollamaEnabled = !config.ollama || config.ollama.enabled !== false;
|
|
110
|
+
|
|
111
|
+
var pool = [];
|
|
112
|
+
|
|
113
|
+
if (ollamaEnabled) {
|
|
114
|
+
var available = await detectOllama(ollamaBase);
|
|
115
|
+
var ollamaModels = filterOllamaModels(available);
|
|
116
|
+
if (ollamaModels.length === 0 && available.length) {
|
|
117
|
+
// Fallback: use whatever Ollama has, treating all as standard effort.
|
|
118
|
+
for (var a = 0; a < available.length; a++) {
|
|
119
|
+
if (available[a].indexOf('embed') !== -1) continue;
|
|
120
|
+
ollamaModels.push({ provider: 'ollama', model: available[a], cost: 0, effort: ['trivial', 'standard'], context: 8192 });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
pool = pool.concat(ollamaModels);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
var userPool = getUserPool(config);
|
|
127
|
+
pool = pool.concat(userPool || DEFAULT_CLOUD_POOL);
|
|
128
|
+
|
|
129
|
+
return pool;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function filterPool(pool, opts) {
|
|
133
|
+
opts = opts || {};
|
|
134
|
+
var effort = opts.effort || 'standard';
|
|
135
|
+
var budget = opts.budget || 'auto'; // free, cheap, auto
|
|
136
|
+
var result = [];
|
|
137
|
+
|
|
138
|
+
for (var i = 0; i < pool.length; i++) {
|
|
139
|
+
var spec = pool[i];
|
|
140
|
+
if (spec.effort && spec.effort.indexOf(effort) === -1) continue;
|
|
141
|
+
if (!isKeyAvailable(spec)) continue;
|
|
142
|
+
|
|
143
|
+
if (budget === 'free' && spec.cost > 0) continue;
|
|
144
|
+
if (budget === 'cheap' && spec.cost > 0.001) continue; // exclude frontier
|
|
145
|
+
|
|
146
|
+
result.push(spec);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
result.sort(function(a, b) {
|
|
150
|
+
if (a.cost !== b.cost) return a.cost - b.cost;
|
|
151
|
+
if (a.context !== b.context) return b.context - a.context;
|
|
152
|
+
return 0;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function resolvePlannerModel(config) {
|
|
159
|
+
config = config || loadConfig();
|
|
160
|
+
var cfg = config.chunk_models || config.chunkModels || {};
|
|
161
|
+
|
|
162
|
+
if (cfg.planner && cfg.planner.provider) {
|
|
163
|
+
var spec = cfg.planner;
|
|
164
|
+
if (spec.provider === 'host') return { provider: 'host', model: spec.model || 'devi' };
|
|
165
|
+
if (isKeyAvailable({ key_env: keyEnvForProvider(spec.provider) })) return Object.assign({}, spec, { cost: spec.cost == null ? 0.003 : spec.cost });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Frontier preference order.
|
|
169
|
+
var frontier = [
|
|
170
|
+
{ provider: 'anthropic', model: 'claude-sonnet-4-20250514', key_env: 'ANTHROPIC_API_KEY' },
|
|
171
|
+
{ provider: 'openai', model: 'gpt-4o', key_env: 'OPENAI_API_KEY' },
|
|
172
|
+
{ provider: 'google', model: 'gemini-1.5-pro', key_env: 'GEMINI_API_KEY' },
|
|
173
|
+
{ provider: 'zai', model: 'glm-4.5-flash', key_env: 'ZAI_API_KEY' },
|
|
174
|
+
];
|
|
175
|
+
for (var i = 0; i < frontier.length; i++) {
|
|
176
|
+
if (isKeyAvailable(frontier[i])) return Object.assign({ cost: 0.003 }, frontier[i]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Fallback: host/Devi, with a warning that planner quality may degrade.
|
|
180
|
+
return { provider: 'host', model: 'devi', cost: 0, fallback_warning: true };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function resolveSynthesisModel(pool, opts) {
|
|
184
|
+
opts = opts || {};
|
|
185
|
+
var effort = opts.effort || 'standard';
|
|
186
|
+
var ranked = filterPool(pool, { effort: effort, budget: opts.budget || 'auto' });
|
|
187
|
+
if (ranked.length) return ranked[0];
|
|
188
|
+
// Fallback to planner-tier if no chunk model available.
|
|
189
|
+
return resolvePlannerModel(opts.config);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function keyEnvForProvider(provider) {
|
|
193
|
+
var map = {
|
|
194
|
+
anthropic: 'ANTHROPIC_API_KEY',
|
|
195
|
+
openai: 'OPENAI_API_KEY',
|
|
196
|
+
google: 'GEMINI_API_KEY',
|
|
197
|
+
zai: 'ZAI_API_KEY',
|
|
198
|
+
groq: 'GROQ_API_KEY',
|
|
199
|
+
together: 'TOGETHER_API_KEY',
|
|
200
|
+
openrouter: 'OPENROUTER_API_KEY',
|
|
201
|
+
deepseek: 'DEEPSEEK_API_KEY',
|
|
202
|
+
};
|
|
203
|
+
return map[provider];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function formatModelName(spec) {
|
|
207
|
+
return spec.provider + '/' + spec.model;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function estimateCost(spec, inputTokens, outputTokens) {
|
|
211
|
+
var cost = spec.cost || 0;
|
|
212
|
+
if (cost <= 0) return 0;
|
|
213
|
+
return (inputTokens / 1e6 + outputTokens / 1e6) * cost;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = {
|
|
217
|
+
DEFAULT_CLOUD_POOL: DEFAULT_CLOUD_POOL,
|
|
218
|
+
DEFAULT_OLLAMA_MODELS: DEFAULT_OLLAMA_MODELS,
|
|
219
|
+
discoverPool: discoverPool,
|
|
220
|
+
filterPool: filterPool,
|
|
221
|
+
resolvePlannerModel: resolvePlannerModel,
|
|
222
|
+
resolveSynthesisModel: resolveSynthesisModel,
|
|
223
|
+
isKeyAvailable: isKeyAvailable,
|
|
224
|
+
keyEnvForProvider: keyEnvForProvider,
|
|
225
|
+
formatModelName: formatModelName,
|
|
226
|
+
estimateCost: estimateCost,
|
|
227
|
+
detectOllama: detectOllama,
|
|
228
|
+
};
|