thumbgate 1.29.2 → 1.31.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/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +54 -16
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/mcp/server-stdio.js +143 -14
- package/adapters/opencode/opencode.json +1 -1
- package/bench/observability-eval-suite.json +2 -2
- package/bin/cli.js +154 -36
- package/config/evals/generation-quality-golden.json +95 -0
- package/config/evals/rag-answer-quality-golden.json +91 -0
- package/config/evals/retrieval-hybrid-ablation.json +66 -0
- package/config/evals/retrieval-ranking-golden.json +522 -0
- package/config/gates/default.json +217 -50
- package/config/mcp-allowlists.json +215 -185
- package/config/model-tiers.json +7 -2
- package/config/post-deploy-marketing-pages.json +26 -1
- package/glama.json +6 -0
- package/package.json +94 -11
- package/public/architecture.html +130 -0
- package/public/assets/diagrams/agent-integration.png +0 -0
- package/public/assets/diagrams/before-after.svg +22 -0
- package/public/assets/diagrams/decision.svg +36 -0
- package/public/assets/diagrams/feedback-pipeline.png +0 -0
- package/public/assets/diagrams/hero-thumbs.svg +68 -0
- package/public/assets/diagrams/loop.svg +40 -0
- package/public/assets/diagrams/plugin-topology.png +0 -0
- package/public/assets/diagrams/pre-action-gate-loop.svg +59 -0
- package/public/assets/diagrams/self-improving-thumbs-loop.svg +105 -0
- package/public/assets/diagrams/stack.svg +18 -0
- package/public/assets/diagrams/thumbgate-architecture.png +0 -0
- package/public/case-studies.html +151 -0
- package/public/compare.html +1 -0
- package/public/dashboard.html +126 -28
- package/public/eval-scorecard.html +195 -0
- package/public/eval-scorecard.json +18 -0
- package/public/evaluations.html +168 -0
- package/public/index.html +143 -13
- package/public/numbers.html +3 -2
- package/public/pricing.html +143 -30
- package/public/whitepaper.html +189 -0
- package/scripts/a-plus-evidence-scorecard.js +303 -0
- package/scripts/activation-quickstart.js +1 -0
- package/scripts/agent-outcome-monitor.js +71 -1
- package/scripts/async-eval-observability.js +36 -11
- package/scripts/audit-trail.js +37 -1
- package/scripts/auto-promote-gates.js +149 -34
- package/scripts/billing.js +3 -1
- package/scripts/claude-feedback-sync.js +3 -2
- package/scripts/cli-feedback.js +13 -7
- package/scripts/colbert-style-maxsim.js +236 -0
- package/scripts/cross-encoder-reranker.js +359 -126
- package/scripts/dashboard-chat.js +350 -17
- package/scripts/document-intake.js +283 -7
- package/scripts/eval-quality-suite.js +204 -0
- package/scripts/feedback-aggregate.js +5 -2
- package/scripts/feedback-loop.js +359 -189
- package/scripts/feedback-paths.js +32 -13
- package/scripts/feedback-quality.js +53 -0
- package/scripts/filesystem-search.js +17 -7
- package/scripts/gates-engine.js +98 -4
- package/scripts/generate-case-study-outreach.js +253 -0
- package/scripts/generate-eval-scorecard.js +276 -0
- package/scripts/growth-campaigns.js +183 -0
- package/scripts/harness-tool-names.js +70 -0
- package/scripts/hook-runtime.js +10 -3
- package/scripts/jsonl-watcher.js +1 -0
- package/scripts/lesson-db.js +16 -5
- package/scripts/lesson-embedding-index.js +67 -20
- package/scripts/lesson-embedding-maintenance.js +177 -0
- package/scripts/lesson-inference.js +23 -4
- package/scripts/lesson-reranker.js +55 -9
- package/scripts/lesson-retrieval.js +375 -32
- package/scripts/lesson-search.js +48 -11
- package/scripts/llm-client.js +304 -15
- package/scripts/mcp-config.js +26 -5
- package/scripts/mcp-oauth.js +37 -2
- package/scripts/model-eval.js +308 -0
- package/scripts/model-tier-router.js +593 -0
- package/scripts/parallel-workflow-orchestrator.js +86 -22
- package/scripts/pragmatic-hybrid-search.js +379 -0
- package/scripts/published-cli.js +11 -1
- package/scripts/rag-document-pipeline.js +461 -0
- package/scripts/rag-structured-output.js +441 -0
- package/scripts/ragas-style-metrics.js +351 -0
- package/scripts/refresh-proof-pack.js +261 -0
- package/scripts/request-envelope.js +178 -0
- package/scripts/rerank-pipeline.js +370 -0
- package/scripts/rerank-quality-eval.js +155 -0
- package/scripts/retrieval-hybrid-ablation.js +120 -0
- package/scripts/retrieval-quality-tier.js +118 -0
- package/scripts/risk-scorer.js +144 -15
- package/scripts/secret-scanner.js +395 -4
- package/scripts/self-distill-agent.js +7 -1
- package/scripts/self-healing-check.js +25 -0
- package/scripts/skill-packs.js +183 -0
- package/scripts/slow-loop.js +72 -0
- package/scripts/statusline-links.js +1 -1
- package/scripts/statusline-local-stats.js +1 -1
- package/scripts/statusline.sh +8 -1
- package/scripts/telemetry-analytics.js +13 -1
- package/scripts/thumbgate-bench.js +13 -0
- package/scripts/thumbgate-search.js +98 -6
- package/scripts/tier-budget-guard.js +186 -0
- package/scripts/tool-kpi-tracker.js +124 -0
- package/scripts/tool-registry.js +95 -1
- package/scripts/vector-store.js +108 -4
- package/scripts/verify-marketing-pages-deployed.js +85 -3
- package/server.json +44 -0
- package/smithery.yaml +17 -0
- package/src/api/server.js +424 -99
package/scripts/lesson-search.js
CHANGED
|
@@ -4,6 +4,7 @@ const path = require('node:path');
|
|
|
4
4
|
const { readJSONL, getFeedbackPaths } = require('./feedback-loop');
|
|
5
5
|
const { buildMemoryLifecycleView, scoreHybridMemoryMatch } = require('./agent-memory-lifecycle');
|
|
6
6
|
const { loadOptionalModule } = require('./private-core-boundary');
|
|
7
|
+
const { selectRetrievalMemories } = require('./lesson-retrieval');
|
|
7
8
|
|
|
8
9
|
const HIGH_RISK_TAGS = new Set([
|
|
9
10
|
'billing',
|
|
@@ -402,6 +403,8 @@ function scoreLesson(queryText, memory, parsed, sourceFeedback) {
|
|
|
402
403
|
if (!queryText) {
|
|
403
404
|
return {
|
|
404
405
|
score: recencyScore(memory.timestamp),
|
|
406
|
+
evidenceScore: 0,
|
|
407
|
+
priorScore: recencyScore(memory.timestamp),
|
|
405
408
|
matchedTokens: [],
|
|
406
409
|
};
|
|
407
410
|
}
|
|
@@ -409,14 +412,16 @@ function scoreLesson(queryText, memory, parsed, sourceFeedback) {
|
|
|
409
412
|
const lessonText = buildLessonQuery(memory, parsed, sourceFeedback);
|
|
410
413
|
const queryTokens = tokenize(queryText);
|
|
411
414
|
const lessonTokens = tokenize(lessonText);
|
|
412
|
-
const
|
|
413
|
-
+ substringBoost(queryText, lessonText)
|
|
414
|
-
|
|
415
|
+
const evidenceScore = jaccardSimilarity(queryTokens, lessonTokens)
|
|
416
|
+
+ substringBoost(queryText, lessonText);
|
|
417
|
+
const priorScore = recencyScore(memory.timestamp)
|
|
415
418
|
+ (memory.category === 'error' ? 0.05 : 0)
|
|
416
419
|
+ Math.min(0.2, scoreHybridMemoryMatch(queryText, memory).score * 0.1);
|
|
417
420
|
|
|
418
421
|
return {
|
|
419
|
-
score,
|
|
422
|
+
score: evidenceScore + priorScore,
|
|
423
|
+
evidenceScore,
|
|
424
|
+
priorScore,
|
|
420
425
|
matchedTokens: unique(queryTokens.filter((token) => lessonTokens.includes(token))),
|
|
421
426
|
};
|
|
422
427
|
}
|
|
@@ -426,7 +431,7 @@ function buildLessonResult(memory, sourceFeedback, options = {}) {
|
|
|
426
431
|
const lessonQuery = buildLessonQuery(memory, parsed, sourceFeedback);
|
|
427
432
|
const ruleMatches = readPreventionRuleMatches(lessonQuery, Number(options.ruleLimit || 3), options);
|
|
428
433
|
const gateMatches = buildGateMatches(memory, parsed, Number(options.gateLimit || 3), options);
|
|
429
|
-
const { score, matchedTokens } = scoreLesson(options.query || '', memory, parsed, sourceFeedback);
|
|
434
|
+
const { score, evidenceScore, priorScore, matchedTokens } = scoreLesson(options.query || '', memory, parsed, sourceFeedback);
|
|
430
435
|
const harnessRecommendations = buildHarnessRecommendations(memory, parsed, sourceFeedback, ruleMatches, gateMatches);
|
|
431
436
|
const lifecycle = buildLifecycle(memory, parsed, sourceFeedback, ruleMatches, gateMatches, harnessRecommendations);
|
|
432
437
|
const memoryLifecycle = buildMemoryLifecycleView(memory, { query: options.query || '' });
|
|
@@ -440,6 +445,8 @@ function buildLessonResult(memory, sourceFeedback, options = {}) {
|
|
|
440
445
|
timestamp: memory.timestamp || null,
|
|
441
446
|
sourceFeedbackId: memory.sourceFeedbackId || null,
|
|
442
447
|
score: Number(score.toFixed(4)),
|
|
448
|
+
evidenceScore: Number(evidenceScore.toFixed(4)),
|
|
449
|
+
priorScore: Number(priorScore.toFixed(4)),
|
|
443
450
|
matchedTokens,
|
|
444
451
|
lesson: {
|
|
445
452
|
summary: parsed.summary,
|
|
@@ -481,7 +488,8 @@ function searchLessons(query = '', options = {}) {
|
|
|
481
488
|
const sqliteResults = tryFts5Search(query, options);
|
|
482
489
|
if (sqliteResults) return sqliteResults;
|
|
483
490
|
|
|
484
|
-
const
|
|
491
|
+
const allMemories = readJSONL(MEMORY_LOG_PATH);
|
|
492
|
+
const memories = selectRetrievalMemories(allMemories, options);
|
|
485
493
|
const feedbackEntries = readJSONL(FEEDBACK_LOG_PATH);
|
|
486
494
|
const feedbackById = new Map(feedbackEntries.map((entry) => [entry.id, entry]));
|
|
487
495
|
const parsedLimit = Number(options.limit || 10);
|
|
@@ -508,7 +516,10 @@ function searchLessons(query = '', options = {}) {
|
|
|
508
516
|
results = results.filter((entry) => requiredTags.every((tag) => entry.tags.includes(tag)));
|
|
509
517
|
}
|
|
510
518
|
if (query) {
|
|
511
|
-
|
|
519
|
+
// Recency, error severity, and lifecycle priors can reorder candidates,
|
|
520
|
+
// but they are never evidence that a lesson answers the query. Returning a
|
|
521
|
+
// recent unrelated incident is worse than an honest empty result.
|
|
522
|
+
results = results.filter((entry) => entry.evidenceScore > 0);
|
|
512
523
|
}
|
|
513
524
|
|
|
514
525
|
results.sort((a, b) => {
|
|
@@ -516,7 +527,7 @@ function searchLessons(query = '', options = {}) {
|
|
|
516
527
|
return String(b.timestamp || '').localeCompare(String(a.timestamp || ''));
|
|
517
528
|
});
|
|
518
529
|
|
|
519
|
-
//
|
|
530
|
+
// Field-aware BM25F reranking: when a query is present, rerank the top-50 first-stage
|
|
520
531
|
// candidates using field-weighted BM25 so the most relevant lessons surface first.
|
|
521
532
|
if (query && results.length > 1) {
|
|
522
533
|
const { rerankLessons } = loadOptionalModule('./lesson-reranker', () => ({
|
|
@@ -534,9 +545,12 @@ function searchLessons(query = '', options = {}) {
|
|
|
534
545
|
filters: {
|
|
535
546
|
category: category || null,
|
|
536
547
|
tags: requiredTags,
|
|
548
|
+
scope: options.scope || null,
|
|
549
|
+
requireScope: options.requireScope === true,
|
|
537
550
|
},
|
|
538
551
|
feedbackDir: FEEDBACK_DIR,
|
|
539
552
|
totalLessons: memories.length,
|
|
553
|
+
excludedLessons: allMemories.length - memories.length,
|
|
540
554
|
returned: Math.min(limit, results.length),
|
|
541
555
|
results: results.slice(0, limit),
|
|
542
556
|
backend: 'jsonl-jaccard',
|
|
@@ -548,10 +562,15 @@ function searchLessons(query = '', options = {}) {
|
|
|
548
562
|
* or not opted in. Set LESSON_DB_SEARCH=1 to enable FTS5 as primary backend.
|
|
549
563
|
*/
|
|
550
564
|
function tryFts5Search(query, options) {
|
|
565
|
+
// The SQLite index does not currently carry the complete four-field scope
|
|
566
|
+
// contract. Fall back to JSONL whenever isolation is requested rather than
|
|
567
|
+
// silently searching across tenants or sessions.
|
|
568
|
+
if (options.scope || options.requireScope) return null;
|
|
551
569
|
if (!process.env.LESSON_DB_SEARCH && !options.useFts5) return null;
|
|
570
|
+
let db = null;
|
|
552
571
|
try {
|
|
553
572
|
const { initDB, searchLessons: fts5Search, getStats } = require('./lesson-db');
|
|
554
|
-
|
|
573
|
+
db = initDB();
|
|
555
574
|
const stats = getStats(db);
|
|
556
575
|
|
|
557
576
|
// If DB is empty, skip (not yet backfilled)
|
|
@@ -567,11 +586,24 @@ function tryFts5Search(query, options) {
|
|
|
567
586
|
.map((tag) => tag.trim())
|
|
568
587
|
.filter(Boolean);
|
|
569
588
|
|
|
570
|
-
const
|
|
571
|
-
limit,
|
|
589
|
+
const candidateRows = fts5Search(db, query || '', {
|
|
590
|
+
limit: Math.max(limit * 5, 50),
|
|
572
591
|
signal,
|
|
573
592
|
tags: requiredTags.length > 0 ? requiredTags : undefined,
|
|
574
593
|
});
|
|
594
|
+
const retrievableRows = selectRetrievalMemories(
|
|
595
|
+
candidateRows.map((row) => ({
|
|
596
|
+
...row,
|
|
597
|
+
title: row.context || '',
|
|
598
|
+
content: [
|
|
599
|
+
row.whatWentWrong,
|
|
600
|
+
row.whatToChange,
|
|
601
|
+
row.whatWorked,
|
|
602
|
+
].filter(Boolean).join('\n'),
|
|
603
|
+
})),
|
|
604
|
+
options,
|
|
605
|
+
);
|
|
606
|
+
const rows = retrievableRows.slice(0, limit);
|
|
575
607
|
|
|
576
608
|
return {
|
|
577
609
|
query: String(query || ''),
|
|
@@ -581,6 +613,7 @@ function tryFts5Search(query, options) {
|
|
|
581
613
|
tags: requiredTags,
|
|
582
614
|
},
|
|
583
615
|
totalLessons: stats.total,
|
|
616
|
+
excludedLessons: candidateRows.length - retrievableRows.length,
|
|
584
617
|
returned: rows.length,
|
|
585
618
|
results: rows.map((row) => ({
|
|
586
619
|
id: row.id,
|
|
@@ -606,6 +639,10 @@ function tryFts5Search(query, options) {
|
|
|
606
639
|
};
|
|
607
640
|
} catch (_err) {
|
|
608
641
|
return null; // SQLite unavailable — fall through to JSONL
|
|
642
|
+
} finally {
|
|
643
|
+
if (db) {
|
|
644
|
+
try { db.close(); } catch { /* best-effort close */ }
|
|
645
|
+
}
|
|
609
646
|
}
|
|
610
647
|
}
|
|
611
648
|
|
package/scripts/llm-client.js
CHANGED
|
@@ -14,12 +14,51 @@ const DEFAULT_MAX_TOKENS = 1024;
|
|
|
14
14
|
const DEFAULT_CACHE_TTL = '5m';
|
|
15
15
|
const DEFAULT_ZAI_BASE_URL = 'https://api.z.ai/api/paas/v4';
|
|
16
16
|
const DEFAULT_ZAI_MODEL = 'glm-5.2-flash';
|
|
17
|
+
const DEFAULT_GATEWAY_MODEL = 'glm-5.2';
|
|
18
|
+
const GATEWAY_TIMEOUT_MS = 30000;
|
|
17
19
|
|
|
18
20
|
let _anthropicClient = null;
|
|
19
21
|
let _geminiClient = null;
|
|
20
22
|
|
|
21
|
-
function
|
|
22
|
-
|
|
23
|
+
function getGatewayConfig(env = process.env) {
|
|
24
|
+
const baseUrl = String(env.THUMBGATE_LLM_GATEWAY_URL || '').trim();
|
|
25
|
+
if (!baseUrl) return null;
|
|
26
|
+
return {
|
|
27
|
+
baseUrl: baseUrl.replace(/\/+$/, ''),
|
|
28
|
+
model: String(env.THUMBGATE_LLM_GATEWAY_MODEL || DEFAULT_GATEWAY_MODEL).trim(),
|
|
29
|
+
credentialEnvVar: 'THUMBGATE_LLM_GATEWAY_TOKEN',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isGatewayConfigured(env = process.env) {
|
|
34
|
+
return Boolean(getGatewayConfig(env));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveGatewayModel(options = {}, config = getGatewayConfig()) {
|
|
38
|
+
const explicitGatewayModel = String(options.gatewayModel || '').trim();
|
|
39
|
+
if (explicitGatewayModel) return explicitGatewayModel;
|
|
40
|
+
return String(config?.model || DEFAULT_GATEWAY_MODEL).trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function describeInferenceAvailability(env = process.env) {
|
|
44
|
+
if (env.ANTHROPIC_API_KEY) return { available: true, provider: 'anthropic' };
|
|
45
|
+
const gateway = getGatewayConfig(env);
|
|
46
|
+
if (gateway) {
|
|
47
|
+
return {
|
|
48
|
+
available: true,
|
|
49
|
+
provider: 'gateway',
|
|
50
|
+
model: gateway.model,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
available: false,
|
|
55
|
+
provider: 'none',
|
|
56
|
+
reason: 'no ANTHROPIC_API_KEY and no THUMBGATE_LLM_GATEWAY_URL',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isAvailable(env = process.env) {
|
|
61
|
+
return describeInferenceAvailability(env).available;
|
|
23
62
|
}
|
|
24
63
|
|
|
25
64
|
function getClient() {
|
|
@@ -155,6 +194,144 @@ function buildSafeProviderError(error) {
|
|
|
155
194
|
return summary;
|
|
156
195
|
}
|
|
157
196
|
|
|
197
|
+
function normalizeUsageTelemetry(usage = null) {
|
|
198
|
+
if (!usage || typeof usage !== 'object') {
|
|
199
|
+
return {
|
|
200
|
+
inputTokens: null,
|
|
201
|
+
outputTokens: null,
|
|
202
|
+
cacheReadInputTokens: null,
|
|
203
|
+
cacheWriteInputTokens: null,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const finiteOrNull = (...values) => {
|
|
207
|
+
const value = values.map(Number).find(Number.isFinite);
|
|
208
|
+
return value === undefined ? null : value;
|
|
209
|
+
};
|
|
210
|
+
return {
|
|
211
|
+
inputTokens: finiteOrNull(usage.input_tokens, usage.prompt_tokens, usage.promptTokenCount),
|
|
212
|
+
outputTokens: finiteOrNull(usage.output_tokens, usage.completion_tokens, usage.candidatesTokenCount),
|
|
213
|
+
cacheReadInputTokens: finiteOrNull(usage.cache_read_input_tokens),
|
|
214
|
+
cacheWriteInputTokens: finiteOrNull(usage.cache_creation_input_tokens),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function buildLlmTrace(options = {}, event = {}) {
|
|
219
|
+
const usage = normalizeUsageTelemetry(event.usage);
|
|
220
|
+
return {
|
|
221
|
+
timestamp: new Date().toISOString(),
|
|
222
|
+
traceId: String(options.traceId || options.metadata?.traceId || '').trim() || null,
|
|
223
|
+
provider: event.provider || 'unknown',
|
|
224
|
+
model: event.model || options.model || null,
|
|
225
|
+
outcome: event.outcome || 'unknown',
|
|
226
|
+
latencyMs: Number.isFinite(event.latencyMs) ? Math.max(0, event.latencyMs) : null,
|
|
227
|
+
...usage,
|
|
228
|
+
stopReason: event.stopReason || null,
|
|
229
|
+
requestId: event.requestId || null,
|
|
230
|
+
httpStatus: Number.isFinite(event.httpStatus) ? event.httpStatus : null,
|
|
231
|
+
errorCode: event.errorCode == null ? null : String(event.errorCode).slice(0, 80),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function emitLlmTrace(options = {}, event = {}) {
|
|
236
|
+
if (typeof options.onTrace !== 'function') return;
|
|
237
|
+
try {
|
|
238
|
+
await options.onTrace(buildLlmTrace(options, event));
|
|
239
|
+
} catch {
|
|
240
|
+
// Observability must never break the generation path.
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function gatewayRequestHeaders(config, env = process.env) {
|
|
245
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
246
|
+
const token = env[config.credentialEnvVar];
|
|
247
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
248
|
+
return headers;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function buildGatewayMessages(options = {}) {
|
|
252
|
+
const supplied = Array.isArray(options.messages)
|
|
253
|
+
? options.messages.filter(Boolean)
|
|
254
|
+
: [];
|
|
255
|
+
const messages = [];
|
|
256
|
+
if (options.systemPrompt) {
|
|
257
|
+
messages.push({ role: 'system', content: options.systemPrompt });
|
|
258
|
+
}
|
|
259
|
+
if (supplied.length > 0) messages.push(...supplied);
|
|
260
|
+
else messages.push({ role: 'user', content: options.userPrompt || '' });
|
|
261
|
+
return messages;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function callGatewayInternal(options = {}, env = process.env) {
|
|
265
|
+
const config = getGatewayConfig(env);
|
|
266
|
+
if (!config || typeof fetch !== 'function') return null;
|
|
267
|
+
const model = resolveGatewayModel(options, config);
|
|
268
|
+
const controller = new AbortController();
|
|
269
|
+
const timer = setTimeout(() => controller.abort(), GATEWAY_TIMEOUT_MS);
|
|
270
|
+
const startedAt = Date.now();
|
|
271
|
+
try {
|
|
272
|
+
const response = await fetch(`${config.baseUrl}/chat/completions`, {
|
|
273
|
+
method: 'POST',
|
|
274
|
+
headers: gatewayRequestHeaders(config, env),
|
|
275
|
+
body: JSON.stringify({
|
|
276
|
+
model,
|
|
277
|
+
messages: buildGatewayMessages(options),
|
|
278
|
+
max_tokens: options.maxTokens || DEFAULT_MAX_TOKENS,
|
|
279
|
+
temperature: Number.isFinite(options.temperature) ? options.temperature : 0,
|
|
280
|
+
}),
|
|
281
|
+
signal: controller.signal,
|
|
282
|
+
});
|
|
283
|
+
if (!response.ok) {
|
|
284
|
+
await emitLlmTrace(options, {
|
|
285
|
+
provider: 'gateway',
|
|
286
|
+
model,
|
|
287
|
+
outcome: 'error',
|
|
288
|
+
latencyMs: Date.now() - startedAt,
|
|
289
|
+
httpStatus: response.status,
|
|
290
|
+
errorCode: 'http_error',
|
|
291
|
+
});
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
const payload = await response.json();
|
|
295
|
+
const choice = payload?.choices?.[0] || {};
|
|
296
|
+
const message = choice.message || {};
|
|
297
|
+
const truncated = choice.finish_reason === 'length';
|
|
298
|
+
const raw = message.content || (truncated ? '' : message.reasoning_content) || '';
|
|
299
|
+
const text = stripCodeFences(raw);
|
|
300
|
+
if (!text) return null;
|
|
301
|
+
const result = {
|
|
302
|
+
text,
|
|
303
|
+
usage: payload?.usage || null,
|
|
304
|
+
stopReason: choice.finish_reason || null,
|
|
305
|
+
id: payload?.id || null,
|
|
306
|
+
model: payload?.model || model,
|
|
307
|
+
provider: 'gateway',
|
|
308
|
+
};
|
|
309
|
+
await emitLlmTrace(options, {
|
|
310
|
+
provider: 'gateway',
|
|
311
|
+
model: result.model,
|
|
312
|
+
outcome: 'success',
|
|
313
|
+
latencyMs: Date.now() - startedAt,
|
|
314
|
+
usage: result.usage,
|
|
315
|
+
stopReason: result.stopReason,
|
|
316
|
+
requestId: result.id,
|
|
317
|
+
});
|
|
318
|
+
return result;
|
|
319
|
+
} catch (error) {
|
|
320
|
+
const safe = buildSafeProviderError(error);
|
|
321
|
+
await emitLlmTrace(options, {
|
|
322
|
+
provider: 'gateway',
|
|
323
|
+
model,
|
|
324
|
+
outcome: 'error',
|
|
325
|
+
latencyMs: Date.now() - startedAt,
|
|
326
|
+
httpStatus: safe.status,
|
|
327
|
+
errorCode: safe.code || safe.name,
|
|
328
|
+
});
|
|
329
|
+
return null;
|
|
330
|
+
} finally {
|
|
331
|
+
clearTimeout(timer);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
158
335
|
function getZaiApiKey(env = process.env) {
|
|
159
336
|
return env.ZAI_API_KEY || env.THUMBGATE_ZAI_API_KEY || '';
|
|
160
337
|
}
|
|
@@ -169,7 +346,18 @@ function getZaiModel(env = process.env) {
|
|
|
169
346
|
|
|
170
347
|
async function callZaiInternal(options = {}, env = process.env) {
|
|
171
348
|
const apiKey = getZaiApiKey(env);
|
|
172
|
-
|
|
349
|
+
const model = options.model || getZaiModel(env);
|
|
350
|
+
const startedAt = Date.now();
|
|
351
|
+
if (!apiKey || typeof fetch !== 'function') {
|
|
352
|
+
await emitLlmTrace(options, {
|
|
353
|
+
provider: 'zai',
|
|
354
|
+
model,
|
|
355
|
+
outcome: 'unavailable',
|
|
356
|
+
latencyMs: Date.now() - startedAt,
|
|
357
|
+
errorCode: !apiKey ? 'missing_api_key' : 'fetch_unavailable',
|
|
358
|
+
});
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
173
361
|
|
|
174
362
|
const messages = Array.isArray(options.messages) && options.messages.length > 0
|
|
175
363
|
? options.messages
|
|
@@ -186,23 +374,52 @@ async function callZaiInternal(options = {}, env = process.env) {
|
|
|
186
374
|
'Content-Type': 'application/json',
|
|
187
375
|
},
|
|
188
376
|
body: JSON.stringify({
|
|
189
|
-
model
|
|
377
|
+
model,
|
|
190
378
|
messages,
|
|
191
379
|
max_tokens: options.maxTokens || DEFAULT_MAX_TOKENS,
|
|
192
380
|
temperature: Number.isFinite(options.temperature) ? options.temperature : 0,
|
|
193
381
|
}),
|
|
194
382
|
});
|
|
195
383
|
|
|
196
|
-
if (!response.ok)
|
|
384
|
+
if (!response.ok) {
|
|
385
|
+
await emitLlmTrace(options, {
|
|
386
|
+
provider: 'zai',
|
|
387
|
+
model,
|
|
388
|
+
outcome: 'error',
|
|
389
|
+
latencyMs: Date.now() - startedAt,
|
|
390
|
+
httpStatus: response.status,
|
|
391
|
+
errorCode: 'http_error',
|
|
392
|
+
});
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
197
395
|
const json = await response.json();
|
|
198
|
-
|
|
396
|
+
const result = {
|
|
199
397
|
text: stripCodeFences(json?.choices?.[0]?.message?.content || ''),
|
|
200
398
|
usage: json?.usage || null,
|
|
201
399
|
stopReason: json?.choices?.[0]?.finish_reason || null,
|
|
202
400
|
id: json?.id || null,
|
|
203
|
-
model: json?.model ||
|
|
401
|
+
model: json?.model || model,
|
|
204
402
|
};
|
|
205
|
-
|
|
403
|
+
await emitLlmTrace(options, {
|
|
404
|
+
provider: 'zai',
|
|
405
|
+
model: result.model,
|
|
406
|
+
outcome: 'success',
|
|
407
|
+
latencyMs: Date.now() - startedAt,
|
|
408
|
+
usage: result.usage,
|
|
409
|
+
stopReason: result.stopReason,
|
|
410
|
+
requestId: result.id,
|
|
411
|
+
});
|
|
412
|
+
return result;
|
|
413
|
+
} catch (error) {
|
|
414
|
+
const safe = buildSafeProviderError(error);
|
|
415
|
+
await emitLlmTrace(options, {
|
|
416
|
+
provider: 'zai',
|
|
417
|
+
model,
|
|
418
|
+
outcome: 'error',
|
|
419
|
+
latencyMs: Date.now() - startedAt,
|
|
420
|
+
httpStatus: safe.status,
|
|
421
|
+
errorCode: safe.code || safe.name,
|
|
422
|
+
});
|
|
206
423
|
return null;
|
|
207
424
|
}
|
|
208
425
|
}
|
|
@@ -211,8 +428,19 @@ async function callGeminiInternal(options = {}) {
|
|
|
211
428
|
const env = process.env;
|
|
212
429
|
const { detectInferenceBackend } = require('./local-model-profile');
|
|
213
430
|
const providerMode = detectInferenceBackend(env).providerMode;
|
|
431
|
+
const provider = providerMode === 'vertex' ? 'vertex' : 'gemini';
|
|
432
|
+
const startedAt = Date.now();
|
|
214
433
|
|
|
215
|
-
if (providerMode !== 'vertex' && !env.GEMINI_API_KEY)
|
|
434
|
+
if (providerMode !== 'vertex' && !env.GEMINI_API_KEY) {
|
|
435
|
+
await emitLlmTrace(options, {
|
|
436
|
+
provider,
|
|
437
|
+
model: options.model,
|
|
438
|
+
outcome: 'unavailable',
|
|
439
|
+
latencyMs: Date.now() - startedAt,
|
|
440
|
+
errorCode: 'missing_api_key',
|
|
441
|
+
});
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
216
444
|
|
|
217
445
|
try {
|
|
218
446
|
const { GoogleGenAI } = require('@google/genai');
|
|
@@ -251,7 +479,7 @@ async function callGeminiInternal(options = {}) {
|
|
|
251
479
|
config,
|
|
252
480
|
}));
|
|
253
481
|
|
|
254
|
-
|
|
482
|
+
const result = {
|
|
255
483
|
text: response.text || '',
|
|
256
484
|
usage: response.usageMetadata ? {
|
|
257
485
|
input_tokens: response.usageMetadata.promptTokenCount,
|
|
@@ -261,8 +489,27 @@ async function callGeminiInternal(options = {}) {
|
|
|
261
489
|
id: null,
|
|
262
490
|
model: options.model,
|
|
263
491
|
};
|
|
492
|
+
await emitLlmTrace(options, {
|
|
493
|
+
provider,
|
|
494
|
+
model: result.model,
|
|
495
|
+
outcome: 'success',
|
|
496
|
+
latencyMs: Date.now() - startedAt,
|
|
497
|
+
usage: result.usage,
|
|
498
|
+
stopReason: result.stopReason,
|
|
499
|
+
requestId: result.id,
|
|
500
|
+
});
|
|
501
|
+
return result;
|
|
264
502
|
} catch (err) {
|
|
265
|
-
|
|
503
|
+
const safe = buildSafeProviderError(err);
|
|
504
|
+
console.error('Gemini/Vertex AI execution error:', JSON.stringify(safe));
|
|
505
|
+
await emitLlmTrace(options, {
|
|
506
|
+
provider,
|
|
507
|
+
model: options.model,
|
|
508
|
+
outcome: 'error',
|
|
509
|
+
latencyMs: Date.now() - startedAt,
|
|
510
|
+
httpStatus: safe.status,
|
|
511
|
+
errorCode: safe.code || safe.name,
|
|
512
|
+
});
|
|
266
513
|
return null;
|
|
267
514
|
}
|
|
268
515
|
}
|
|
@@ -296,7 +543,21 @@ async function callClaudeInternal(options = {}) {
|
|
|
296
543
|
}
|
|
297
544
|
|
|
298
545
|
const client = getClient();
|
|
299
|
-
|
|
546
|
+
const model = options.model || DEFAULT_MODEL;
|
|
547
|
+
const startedAt = Date.now();
|
|
548
|
+
if (!client) {
|
|
549
|
+
if (isGatewayConfigured()) {
|
|
550
|
+
return callGatewayInternal(options);
|
|
551
|
+
}
|
|
552
|
+
await emitLlmTrace(options, {
|
|
553
|
+
provider: 'anthropic',
|
|
554
|
+
model,
|
|
555
|
+
outcome: 'unavailable',
|
|
556
|
+
latencyMs: Date.now() - startedAt,
|
|
557
|
+
errorCode: 'missing_client_or_api_key',
|
|
558
|
+
});
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
300
561
|
|
|
301
562
|
try {
|
|
302
563
|
const response = await runStep('llm.callClaude', {
|
|
@@ -305,14 +566,33 @@ async function callClaudeInternal(options = {}) {
|
|
|
305
566
|
}, async () => client.messages.create(buildClaudeRequest(options)));
|
|
306
567
|
|
|
307
568
|
const text = stripCodeFences(extractTextContent(response));
|
|
308
|
-
|
|
569
|
+
const result = {
|
|
309
570
|
text,
|
|
310
571
|
usage: response?.usage || null,
|
|
311
572
|
stopReason: response?.stop_reason || null,
|
|
312
573
|
id: response?.id || null,
|
|
313
|
-
model: response?.model ||
|
|
574
|
+
model: response?.model || model,
|
|
314
575
|
};
|
|
315
|
-
|
|
576
|
+
await emitLlmTrace(options, {
|
|
577
|
+
provider: 'anthropic',
|
|
578
|
+
model: result.model,
|
|
579
|
+
outcome: 'success',
|
|
580
|
+
latencyMs: Date.now() - startedAt,
|
|
581
|
+
usage: result.usage,
|
|
582
|
+
stopReason: result.stopReason,
|
|
583
|
+
requestId: result.id,
|
|
584
|
+
});
|
|
585
|
+
return result;
|
|
586
|
+
} catch (error) {
|
|
587
|
+
const safe = buildSafeProviderError(error);
|
|
588
|
+
await emitLlmTrace(options, {
|
|
589
|
+
provider: 'anthropic',
|
|
590
|
+
model,
|
|
591
|
+
outcome: 'error',
|
|
592
|
+
latencyMs: Date.now() - startedAt,
|
|
593
|
+
httpStatus: safe.status,
|
|
594
|
+
errorCode: safe.code || safe.name,
|
|
595
|
+
});
|
|
316
596
|
return null;
|
|
317
597
|
}
|
|
318
598
|
}
|
|
@@ -372,6 +652,12 @@ async function callZaiJson(options = {}) {
|
|
|
372
652
|
|
|
373
653
|
module.exports = {
|
|
374
654
|
isAvailable,
|
|
655
|
+
getGatewayConfig,
|
|
656
|
+
isGatewayConfigured,
|
|
657
|
+
resolveGatewayModel,
|
|
658
|
+
describeInferenceAvailability,
|
|
659
|
+
callGatewayInternal,
|
|
660
|
+
buildGatewayMessages,
|
|
375
661
|
callClaude,
|
|
376
662
|
callClaudeJson,
|
|
377
663
|
callZaiJson,
|
|
@@ -383,5 +669,8 @@ module.exports = {
|
|
|
383
669
|
normalizeCacheOptions,
|
|
384
670
|
buildClaudeRequest,
|
|
385
671
|
buildSafeProviderError,
|
|
672
|
+
normalizeUsageTelemetry,
|
|
673
|
+
buildLlmTrace,
|
|
674
|
+
emitLlmTrace,
|
|
386
675
|
MODELS,
|
|
387
676
|
};
|
package/scripts/mcp-config.js
CHANGED
|
@@ -193,17 +193,37 @@ function publishedCliAvailable(pkgVersion) {
|
|
|
193
193
|
return cliAvailabilityCache.get(pkgVersion);
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Project-scope entries land in COMMITTED, SHARED config (.mcp.json / .cursor/mcp.json —
|
|
198
|
+
* init's own banner says the file serves every agent on the repo). A machine-absolute path
|
|
199
|
+
* there is a bug by construction: run init on machine A (or a Cowork sandbox with a home
|
|
200
|
+
* like /Users/busy-clever-newton) and the committed config breaks for every other machine,
|
|
201
|
+
* teammate, and CI runner. Observed for real on 2026-07-29.
|
|
202
|
+
*
|
|
203
|
+
* So: absolute paths may only ever go to HOME-scope config (machine-local by definition).
|
|
204
|
+
* Project scope gets a repo-relative path when the project IS the ThumbGate checkout
|
|
205
|
+
* (dogfooding unpublished source still works — project MCP servers launch with cwd at the
|
|
206
|
+
* project root), and the portable npx launcher otherwise.
|
|
207
|
+
*/
|
|
208
|
+
function relativeLocalMcpEntry(pkgRoot, targetDir) {
|
|
209
|
+
const rel = path.relative(targetDir, resolveLocalServerPath(pkgRoot, 'project'));
|
|
210
|
+
// Committed config must be separator-portable too.
|
|
211
|
+
return { command: 'node', args: [rel.split(path.sep).join('/')] };
|
|
212
|
+
}
|
|
213
|
+
|
|
196
214
|
function resolveMcpEntry({ pkgRoot, pkgVersion, scope = 'project', targetDir = pkgRoot }) {
|
|
197
215
|
if (!isSourceCheckout(pkgRoot)) {
|
|
198
216
|
return codexAutoUpdateMcpEntry();
|
|
199
217
|
}
|
|
200
|
-
if (scope === 'home'
|
|
201
|
-
return codexAutoUpdateMcpEntry();
|
|
218
|
+
if (scope === 'home') {
|
|
219
|
+
if (publishedCliAvailable(pkgVersion)) return codexAutoUpdateMcpEntry();
|
|
220
|
+
return localMcpEntry(pkgRoot, scope);
|
|
202
221
|
}
|
|
203
|
-
|
|
204
|
-
|
|
222
|
+
// scope === 'project': this is going into shared, committed config.
|
|
223
|
+
if (isSameCheckoutFamily(pkgRoot, targetDir)) {
|
|
224
|
+
return relativeLocalMcpEntry(pkgRoot, targetDir);
|
|
205
225
|
}
|
|
206
|
-
return
|
|
226
|
+
return codexAutoUpdateMcpEntry();
|
|
207
227
|
}
|
|
208
228
|
|
|
209
229
|
module.exports = {
|
|
@@ -214,6 +234,7 @@ module.exports = {
|
|
|
214
234
|
localMcpEntry,
|
|
215
235
|
parseWorktreePaths,
|
|
216
236
|
portableMcpEntry,
|
|
237
|
+
relativeLocalMcpEntry,
|
|
217
238
|
resolveGitCommonDir,
|
|
218
239
|
resolveLocalServerPath,
|
|
219
240
|
resolveMcpEntry,
|