thumbgate 1.30.0 → 1.34.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.
Files changed (92) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.well-known/mcp/server-card.json +1 -1
  3. package/README.md +54 -16
  4. package/adapters/claude/.mcp.json +2 -2
  5. package/adapters/forge/forge.yaml +3 -3
  6. package/adapters/mcp/server-stdio.js +105 -10
  7. package/adapters/opencode/opencode.json +1 -1
  8. package/bench/observability-eval-suite.json +2 -2
  9. package/bin/cli.js +168 -31
  10. package/config/evals/generation-quality-golden.json +95 -0
  11. package/config/evals/rag-answer-quality-golden.json +91 -0
  12. package/config/evals/retrieval-hybrid-ablation.json +66 -0
  13. package/config/evals/retrieval-ranking-golden.json +522 -0
  14. package/config/gates/claim-verifiers.example.json +42 -0
  15. package/config/gates/claim-verifiers.json +25 -0
  16. package/config/gates/default.json +217 -50
  17. package/config/mcp-allowlists.json +233 -206
  18. package/config/model-tiers.json +7 -2
  19. package/glama.json +6 -0
  20. package/hooks/hooks.json +1 -1
  21. package/package.json +69 -12
  22. package/public/assets/diagrams/before-after.svg +17 -16
  23. package/public/assets/diagrams/hero-thumbs.svg +68 -0
  24. package/public/assets/diagrams/loop.svg +19 -13
  25. package/public/assets/diagrams/self-improving-thumbs-loop.svg +105 -0
  26. package/public/compare.html +1 -0
  27. package/public/dashboard.html +126 -28
  28. package/public/evaluations.html +1 -1
  29. package/public/index.html +142 -13
  30. package/public/numbers.html +3 -2
  31. package/public/pricing.html +143 -30
  32. package/scripts/a-plus-evidence-scorecard.js +303 -0
  33. package/scripts/agent-readiness.js +110 -0
  34. package/scripts/async-eval-observability.js +36 -11
  35. package/scripts/audit-trail.js +37 -1
  36. package/scripts/auto-promote-gates.js +149 -34
  37. package/scripts/auto-wire-hooks.js +20 -8
  38. package/scripts/cli-schema.js +14 -0
  39. package/scripts/colbert-style-maxsim.js +236 -0
  40. package/scripts/cross-encoder-reranker.js +356 -126
  41. package/scripts/dashboard-chat.js +350 -17
  42. package/scripts/document-intake.js +283 -7
  43. package/scripts/eval-quality-suite.js +204 -0
  44. package/scripts/feedback-loop.js +115 -7
  45. package/scripts/feedback-paths.js +32 -13
  46. package/scripts/feedback-quality.js +53 -0
  47. package/scripts/feedback-schema.js +3 -0
  48. package/scripts/file-ledger-lock.js +130 -0
  49. package/scripts/filesystem-search.js +17 -7
  50. package/scripts/financial-control-plane.js +1514 -0
  51. package/scripts/gates-engine.js +202 -7
  52. package/scripts/gemini-embedding-policy.js +1 -0
  53. package/scripts/harness-tool-names.js +70 -0
  54. package/scripts/hook-runtime.js +15 -3
  55. package/scripts/hook-stop-anti-claim.js +63 -3
  56. package/scripts/human-escalation.js +353 -41
  57. package/scripts/lesson-db.js +16 -5
  58. package/scripts/lesson-embedding-index.js +67 -20
  59. package/scripts/lesson-embedding-maintenance.js +177 -0
  60. package/scripts/lesson-reranker.js +55 -9
  61. package/scripts/lesson-retrieval.js +305 -29
  62. package/scripts/lesson-search.js +22 -8
  63. package/scripts/llm-client.js +304 -15
  64. package/scripts/model-tier-router.js +593 -0
  65. package/scripts/pragmatic-hybrid-search.js +379 -0
  66. package/scripts/provider-action-normalizer.js +11 -4
  67. package/scripts/rag-document-pipeline.js +461 -0
  68. package/scripts/rag-structured-output.js +441 -0
  69. package/scripts/ragas-style-metrics.js +351 -0
  70. package/scripts/request-envelope.js +178 -0
  71. package/scripts/rerank-pipeline.js +370 -0
  72. package/scripts/rerank-quality-eval.js +155 -0
  73. package/scripts/retrieval-hybrid-ablation.js +120 -0
  74. package/scripts/retrieval-quality-tier.js +118 -0
  75. package/scripts/secret-scanner.js +395 -4
  76. package/scripts/self-distill-agent.js +7 -1
  77. package/scripts/self-healing-check.js +25 -0
  78. package/scripts/skill-packs.js +183 -0
  79. package/scripts/slow-loop.js +72 -0
  80. package/scripts/statusline-links.js +1 -1
  81. package/scripts/statusline.sh +8 -1
  82. package/scripts/telemetry-analytics.js +13 -1
  83. package/scripts/thumbgate-search.js +98 -6
  84. package/scripts/tier-budget-guard.js +186 -0
  85. package/scripts/tool-registry.js +141 -5
  86. package/scripts/universal-claim-evaluator.js +767 -0
  87. package/scripts/vector-store.js +154 -17
  88. package/scripts/verify-marketing-pages-deployed.js +85 -3
  89. package/scripts/workflow-sentinel.js +77 -11
  90. package/server.json +44 -0
  91. package/smithery.yaml +17 -0
  92. package/src/api/server.js +196 -13
@@ -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 isAvailable() {
22
- return Boolean(process.env.ANTHROPIC_API_KEY);
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
- if (!apiKey || typeof fetch !== 'function') return null;
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: options.model || getZaiModel(env),
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) return null;
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
- return {
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 || options.model || getZaiModel(env),
401
+ model: json?.model || model,
204
402
  };
205
- } catch {
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) return null;
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
- return {
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
- console.error('Gemini/Vertex AI execution error:', JSON.stringify(buildSafeProviderError(err)));
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
- if (!client) return null;
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
- return {
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 || options.model || DEFAULT_MODEL,
574
+ model: response?.model || model,
314
575
  };
315
- } catch {
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
  };