thumbgate 1.35.0 → 1.37.2

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 (204) hide show
  1. package/.agents/skills/cobble-hot-store-compare-not-clone/SKILL.md +80 -0
  2. package/.agents/skills/colab-compute-honesty-not-clone/SKILL.md +70 -0
  3. package/.agents/skills/cyberstrike-compare-not-clone/SKILL.md +36 -0
  4. package/.agents/skills/gitlab-sandbox-allowlist-not-trust/SKILL.md +77 -0
  5. package/.agents/skills/jit-harness-compare-not-clone/SKILL.md +34 -0
  6. package/.agents/skills/openui-catalog-compose-honesty/SKILL.md +64 -0
  7. package/.agents/skills/token-shunt-honesty-not-clone/SKILL.md +67 -0
  8. package/.agents/skills/typesafe-typed-questions-not-clone/SKILL.md +82 -0
  9. package/.agents/skills/zvec-grep-compare-not-clone/SKILL.md +34 -0
  10. package/.claude-plugin/plugin.json +1 -1
  11. package/.well-known/llms.txt +1 -0
  12. package/.well-known/mcp/server-card.json +1 -1
  13. package/CONTRIBUTING.md +95 -0
  14. package/README.md +195 -632
  15. package/THIRD_PARTY_NOTICES.md +89 -0
  16. package/adapters/claude/.mcp.json +2 -2
  17. package/adapters/forge/forge.yaml +3 -3
  18. package/adapters/future-agi/.mcp.json +8 -0
  19. package/adapters/future-agi/FUTURE_AGI.md +23 -0
  20. package/adapters/future-agi/config.toml +3 -0
  21. package/adapters/future-agi/future-agi-bridge.js +9 -0
  22. package/adapters/future-agi/opencode.json +8 -0
  23. package/adapters/herdr/herdr-plugin.toml +18 -0
  24. package/adapters/mcp/server-stdio.js +238 -25
  25. package/adapters/opencode/opencode.json +1 -1
  26. package/adapters/workos/WORKOS.md +52 -0
  27. package/bin/cli.js +465 -3
  28. package/bin/futureagi-bridge +9 -0
  29. package/config/gate-templates.json +629 -4
  30. package/config/gates/actor-critic-audit.json +34 -0
  31. package/config/gates/default.json +9 -3
  32. package/config/gates/five-walls-governance.json +34 -0
  33. package/config/gates/future-agi-guardrails.json +34 -0
  34. package/config/gates/radware-threat-defense-2026.json +61 -0
  35. package/config/gates/simatree-data-governance.json +33 -0
  36. package/config/mcp-allowlists.json +4 -0
  37. package/config/merge-quality-checks.json +6 -0
  38. package/config/model-candidates.json +312 -29
  39. package/config/model-tiers.json +18 -0
  40. package/config/post-deploy-marketing-pages.json +10 -0
  41. package/config/progressive/01-wire-only.json +11 -0
  42. package/config/progressive/02-dashboard-empty-ok.json +10 -0
  43. package/config/progressive/03-one-lesson.json +10 -0
  44. package/config/progressive/04-warn-fires.json +11 -0
  45. package/config/progressive/05-strict-optional.json +11 -0
  46. package/config/progressive/README.md +15 -0
  47. package/config/schemas/broker-execution-receipt.schema.json +139 -0
  48. package/config/schemas/provider-execution-attestation-v1.schema.json +58 -0
  49. package/conformance/provider-attestation/vectors.json +320 -0
  50. package/docs/specs/provider-execution-attestation-v1.md +69 -0
  51. package/openapi/openapi.yaml +15 -0
  52. package/package.json +408 -150
  53. package/public/about.html +2 -2
  54. package/public/ai-malpractice-prevention.html +7 -7
  55. package/public/blog/a-10-dollar-vps-is-not-a-computer.html +143 -0
  56. package/public/blog/a-receipt-is-not-world-state.html +388 -0
  57. package/public/blog/git-at-agent-scale.html +374 -0
  58. package/public/blog/no-llm-in-the-gate.html +133 -0
  59. package/public/blog.html +80 -0
  60. package/public/case-studies.html +16 -1
  61. package/public/compare.html +28 -0
  62. package/public/diagnostic.html +216 -7
  63. package/public/docs/connectors.html +39 -0
  64. package/public/federal.html +2 -2
  65. package/public/founders.html +639 -0
  66. package/public/index.html +87 -9
  67. package/public/install.html +8 -8
  68. package/public/learn.html +39 -0
  69. package/public/numbers.html +2 -2
  70. package/public/peter.html +310 -0
  71. package/public/platform-partners.html +119 -0
  72. package/public/pricing.html +24 -3
  73. package/public/privacy.html +117 -0
  74. package/public/pro.html +17 -0
  75. package/public/support.html +62 -0
  76. package/public/terms.html +130 -0
  77. package/public/third-party-notices.html +95 -0
  78. package/public/yt.html +351 -0
  79. package/scripts/action-receipts.js +133 -3
  80. package/scripts/adaptive-governance-arena.js +349 -0
  81. package/scripts/admin-override.js +205 -0
  82. package/scripts/agent-action-inventory.js +869 -0
  83. package/scripts/agent-audit-trace.js +42 -2
  84. package/scripts/agent-egress-policy.js +1117 -0
  85. package/scripts/agent-memory-lifecycle.js +141 -2
  86. package/scripts/agent-operations-planner.js +441 -1
  87. package/scripts/agent-readiness.js +68 -0
  88. package/scripts/agent-security-central.js +647 -0
  89. package/scripts/allowlist-bridge-honesty.js +417 -0
  90. package/scripts/async-job-runner.js +102 -11
  91. package/scripts/audit-trail.js +212 -0
  92. package/scripts/billing.js +1 -1
  93. package/scripts/broker-execution-receipts.js +719 -0
  94. package/scripts/budget-aware-gates-proof.js +423 -0
  95. package/scripts/claude-feedback-sync.js +29 -3
  96. package/scripts/claw-harness-production.js +237 -0
  97. package/scripts/cli-schema.js +203 -1
  98. package/scripts/cobble-hot-store-split.js +600 -0
  99. package/scripts/codex-runbook-flywheel.js +318 -0
  100. package/scripts/colab-compute-honesty.js +281 -0
  101. package/scripts/context-footprint.js +186 -0
  102. package/scripts/contextfs.js +143 -61
  103. package/scripts/dashboard.js +251 -32
  104. package/scripts/deepseek-v4-runtime-guardrails.js +72 -6
  105. package/scripts/docker-sandbox-planner.js +18 -0
  106. package/scripts/double-blind-eval-protocol.js +252 -0
  107. package/scripts/edotenv-rl-gateway.js +259 -0
  108. package/scripts/ensure-production-search-corpus.js +162 -0
  109. package/scripts/eval-holdout.js +311 -0
  110. package/scripts/feedback-aggregate.js +21 -2
  111. package/scripts/feedback-loop.js +87 -5
  112. package/scripts/feedback-quality.js +9 -0
  113. package/scripts/file-ledger-lock.js +4 -1
  114. package/scripts/financial-control-plane.js +41 -1
  115. package/scripts/find-dormant-requires.js +118 -0
  116. package/scripts/fs-utils.js +84 -8
  117. package/scripts/gates-engine.js +826 -68
  118. package/scripts/generate-case-study-outreach.js +24 -15
  119. package/scripts/git-at-scale.js +628 -0
  120. package/scripts/governance-conflict-audit.js +1650 -0
  121. package/scripts/governance-difficulty-curriculum.js +328 -0
  122. package/scripts/graphrag-retrieval.js +275 -0
  123. package/scripts/gurobi-optimizer.js +324 -0
  124. package/scripts/gurobi_optimizer.py +485 -0
  125. package/scripts/harness-selector.js +82 -1
  126. package/scripts/hidden-entry-points.js +284 -0
  127. package/scripts/human-escalation.js +199 -1
  128. package/scripts/hybrid-feedback-context.js +152 -19
  129. package/scripts/intent-governed-execution.js +602 -0
  130. package/scripts/intervention-policy.js +123 -20
  131. package/scripts/jit-harness-compose.js +628 -0
  132. package/scripts/jsonl-watcher.js +10 -0
  133. package/scripts/lesson-embedding-index.js +95 -12
  134. package/scripts/lesson-retrieval.js +105 -19
  135. package/scripts/local-model-profile.js +19 -2
  136. package/scripts/mailer/resend-mailer.js +1 -1
  137. package/scripts/matryoshka-embedding.js +235 -0
  138. package/scripts/mcp-oauth.js +42 -4
  139. package/scripts/mcp-session-handles.js +1016 -0
  140. package/scripts/mcp-wiring-doctor.js +314 -0
  141. package/scripts/memory-firewall.js +115 -2
  142. package/scripts/memory-scope-readiness.js +299 -0
  143. package/scripts/memory-vs-rag-route.js +161 -0
  144. package/scripts/model-tier-router.js +148 -21
  145. package/scripts/nvidia-specdecode-al-doctor.js +536 -0
  146. package/scripts/openui-catalog-compose-honesty.js +593 -0
  147. package/scripts/operational-integrity.js +19 -1
  148. package/scripts/override-audit.js +213 -0
  149. package/scripts/package-manager-honesty-doctor.js +458 -0
  150. package/scripts/pr-manager.js +63 -1
  151. package/scripts/prove-herdr-adapter.js +52 -0
  152. package/scripts/prove-memory-pyramid-and-symbolic-canvas.js +95 -0
  153. package/scripts/prove-workos.js +73 -0
  154. package/scripts/provider-attestation-conformance.js +192 -0
  155. package/scripts/provider-receipt-contract.js +136 -0
  156. package/scripts/qwen38-max-cost-optimizer.js +401 -0
  157. package/scripts/radware-threat-defense.js +280 -0
  158. package/scripts/rag-embedding-identity.js +221 -0
  159. package/scripts/rag-precision-guardrails.js +112 -2
  160. package/scripts/remote-feedback-capture.js +159 -0
  161. package/scripts/research-agent-harness.js +256 -0
  162. package/scripts/rsi-safety-hillclimb.js +200 -0
  163. package/scripts/rule-sprawl.js +188 -0
  164. package/scripts/schedule-manager.js +147 -0
  165. package/scripts/self-heal.js +8 -0
  166. package/scripts/session-lease.js +415 -0
  167. package/scripts/simatree-data-governance.js +347 -0
  168. package/scripts/slo-alert-engine.js +172 -7
  169. package/scripts/solver-parity.js +539 -0
  170. package/scripts/stealth-memory-injection-gate.js +333 -0
  171. package/scripts/switchyard-router.js +366 -0
  172. package/scripts/telemetry-analytics.js +84 -27
  173. package/scripts/temporal-decay-weighting.js +138 -0
  174. package/scripts/test-all.js +165 -0
  175. package/scripts/token-savings.js +42 -0
  176. package/scripts/token-shunt-honesty.js +502 -0
  177. package/scripts/tool-kpi-tracker.js +108 -5
  178. package/scripts/tool-registry.js +193 -5
  179. package/scripts/typesafe-typed-questions.js +813 -0
  180. package/scripts/universal-claim-evaluator.js +14 -2
  181. package/scripts/vector-store.js +279 -9
  182. package/scripts/workflow-notebook.js +391 -0
  183. package/scripts/workflow-sentinel.js +111 -12
  184. package/scripts/workos-production-guard.js +260 -0
  185. package/scripts/workspace-search-route.js +515 -0
  186. package/server.json +2 -2
  187. package/src/agent-identity-boundary.js +76 -0
  188. package/src/agent-retrieval-cache.js +155 -0
  189. package/src/alert-noise-ledger.js +502 -0
  190. package/src/api/server.js +724 -153
  191. package/src/git-fast-cache.js +220 -0
  192. package/src/git-wal-sync.js +156 -0
  193. package/src/hash-anchored-edit.js +82 -0
  194. package/src/hermes-platform-protocol.js +475 -0
  195. package/src/hermes-sync-plane.js +241 -0
  196. package/src/index.js +30 -1
  197. package/src/iso42001-compliance-guard.js +97 -0
  198. package/src/latency-budget.js +244 -0
  199. package/src/mcp-writeguard.js +316 -0
  200. package/src/miminions-adapter.js +106 -0
  201. package/src/pipeline-compass.js +104 -0
  202. package/src/ppl-alert-pipeline.js +284 -0
  203. package/src/rendezvous-router.js +90 -0
  204. package/src/security-questionnaire.js +195 -0
@@ -32,11 +32,18 @@ function loadConfig() {
32
32
  const TIERS = {
33
33
  nano: { label: 'nano', costMultiplier: 0.1, maxContext: 32000 },
34
34
  mini: { label: 'mini', costMultiplier: 0.4, maxContext: 200000 },
35
+ // Cheap OpenAI-compatible cloud flagship (Qwen3.8-Max class) for steady
36
+ // high-output workloads. Inert until its endpoint env vars are configured.
37
+ bulkCloud: { label: 'bulk-cloud', costMultiplier: 0.25, maxContext: 1000000 },
35
38
  frontier: { label: 'frontier', costMultiplier: 1.0, maxContext: 1000000 },
36
39
  // Self-hosted open-source frontier (e.g. GLM 5.1). Zero marginal cost.
37
40
  localFrontier: { label: 'local-frontier', costMultiplier: 0.0, maxContext: 1000000 },
38
41
  };
39
42
 
43
+ // Routing priority for task-type matching. Config-declared tiers missing from
44
+ // this list are appended after, before the frontier fallback semantics apply.
45
+ const TIER_ROUTING_ORDER = ['nano', 'mini', 'bulkCloud', 'frontier'];
46
+
40
47
  // ---------------------------------------------------------------------------
41
48
  // Task classification → tier mapping
42
49
  // ---------------------------------------------------------------------------
@@ -91,8 +98,10 @@ function classifyTask(task = {}) {
91
98
  // --- Normal tier routing by task type ---
92
99
 
93
100
  const tiers = config.tiers;
94
- for (const tierName of ['nano', 'mini', 'frontier']) {
95
- if (tiers[tierName].taskTypes.includes(type)) {
101
+ const declaredOrder = TIER_ROUTING_ORDER.filter((name) => tiers[name])
102
+ .concat(Object.keys(tiers).filter((name) => !TIER_ROUTING_ORDER.includes(name)));
103
+ for (const tierName of declaredOrder) {
104
+ if (Array.isArray(tiers[tierName].taskTypes) && tiers[tierName].taskTypes.includes(type)) {
96
105
  return {
97
106
  tier: tierName,
98
107
  reason: `task type "${type}" mapped to ${tierName}`,
@@ -269,6 +278,19 @@ class FrontierBudget {
269
278
  }
270
279
  }
271
280
 
281
+ function isSwitchyardEnabled(task = {}, env = process.env) {
282
+ const flag = String(env.THUMBGATE_SWITCHYARD || '').trim().toLowerCase();
283
+ if (flag === '1' || flag === 'true' || flag === 'yes' || flag === 'on') return true;
284
+ const tags = Array.isArray(task.tags) ? task.tags.map((t) => String(t).toLowerCase()) : [];
285
+ return Boolean(
286
+ Array.isArray(task.steps) && task.steps.length > 0
287
+ || tags.includes('switchyard')
288
+ || tags.includes('multi-model')
289
+ || tags.includes('nemotron')
290
+ || tags.includes('always-on'),
291
+ );
292
+ }
293
+
272
294
  function recommendExecutionPlan(task = {}, env = process.env) {
273
295
  const classification = classifyTask(task);
274
296
  const inference = recommendInferenceBackend(task, env);
@@ -280,7 +302,7 @@ function recommendExecutionPlan(task = {}, env = process.env) {
280
302
  ? 'localFrontier'
281
303
  : classification.tier;
282
304
 
283
- return {
305
+ const plan = {
284
306
  architecture: 'risk-aware-model-routing',
285
307
  mixtureOfExperts: false,
286
308
  tier: effectiveTier,
@@ -295,6 +317,52 @@ function recommendExecutionPlan(task = {}, env = process.env) {
295
317
  indexCacheEnabled: inference.backend.indexCacheEnabled,
296
318
  reason: `${classification.reason}; ${inference.reason}`,
297
319
  };
320
+
321
+ // Optional NeMo Switchyard–style multi-model step plan (opt-in).
322
+ if (isSwitchyardEnabled(task, env)) {
323
+ try {
324
+ const {
325
+ routeAgentSteps,
326
+ buildAlwaysOnAgentPlan,
327
+ } = require('./switchyard-router');
328
+ const steps = Array.isArray(task.steps) && task.steps.length
329
+ ? task.steps
330
+ : buildAlwaysOnAgentPlan({
331
+ sensitive: task.privacyRoute === 'local' || task.sensitive,
332
+ highVolume: task.highVolume || task.costPriority === 'primary',
333
+ riskLevel: task.riskLevel,
334
+ actType: task.type,
335
+ });
336
+ const switchyard = routeAgentSteps(steps);
337
+ plan.switchyard = switchyard;
338
+ plan.multiModel = switchyard.multiModel;
339
+ plan.reason = `${plan.reason}; switchyard models=${switchyard.distinctModels.join(',')}`;
340
+ } catch {
341
+ // Switchyard optional; never break baseline routing.
342
+ }
343
+ }
344
+
345
+ // Optional dual-stack cost lane (Qwen volume / Claude quality). Opt-in via
346
+ // THUMBGATE_COST_ROUTE_QWEN=1 or task tags cost-sensitive|high-volume|bulk.
347
+ if (isCostRouteQwenEnabled(task, env)) {
348
+ try {
349
+ const { recommendCostQualitySplit } = require('./qwen38-max-cost-optimizer');
350
+ const split = recommendCostQualitySplit(task, {
351
+ useTokenPlanPromo: String(env.THUMBGATE_QWEN_TOKEN_PLAN_PROMO || '').toLowerCase() === '1'
352
+ || String(env.THUMBGATE_QWEN_TOKEN_PLAN_PROMO || '').toLowerCase() === 'true',
353
+ localModel: env.THUMBGATE_LOCAL_MODEL || null,
354
+ });
355
+ plan.costQualitySplit = split;
356
+ plan.costLane = split.lane;
357
+ plan.preferredProvider = split.primaryProvider;
358
+ plan.preferredModel = split.primaryModel;
359
+ plan.reason = `${plan.reason}; cost-lane=${split.lane}→${split.primaryProvider}/${split.primaryModel}`;
360
+ } catch {
361
+ // Cost module optional at runtime; never fail routing.
362
+ }
363
+ }
364
+
365
+ return plan;
298
366
  }
299
367
 
300
368
  // ---------------------------------------------------------------------------
@@ -371,23 +439,46 @@ function resolveGenerationTarget(plan, task, tierConfig, options, env) {
371
439
 
372
440
  const model = modelOverride || tierConfig.modelId;
373
441
  if (!model) throw new Error(`missing modelId for tier "${plan.tier}"`);
374
- return {
442
+ const target = {
375
443
  model,
376
444
  provider: providerOverride || tierConfig.provider || inferProvider(model, plan.tier),
377
445
  };
446
+ if (tierConfig.endpoint && typeof tierConfig.endpoint === 'object') {
447
+ const { baseUrlEnv, apiKeyEnv } = tierConfig.endpoint;
448
+ const baseUrl = baseUrlEnv ? env[baseUrlEnv] : undefined;
449
+ const apiKey = apiKeyEnv ? env[apiKeyEnv] : undefined;
450
+ if (!baseUrl || !apiKey) {
451
+ const missing = [!baseUrl && baseUrlEnv, !apiKey && apiKeyEnv].filter(Boolean).join(', ');
452
+ throw new Error(`tier "${plan.tier}" requires endpoint configuration; set ${missing}`);
453
+ }
454
+ target.endpoint = { baseUrl, apiKey };
455
+ }
456
+ return target;
457
+ }
458
+
459
+ /**
460
+ * Estimate the USD cost of a generation for a tier that declares
461
+ * pricingUsdPerMTok in config. Returns null when pricing is not declared.
462
+ */
463
+ function estimateTierCostUsd(tierName, inputTokens, outputTokens, config = loadConfig()) {
464
+ const pricing = config?.tiers?.[tierName]?.pricingUsdPerMTok;
465
+ if (!pricing || !Number.isFinite(pricing.input) || !Number.isFinite(pricing.output)) return null;
466
+ const inTok = Number.isFinite(inputTokens) ? Math.max(0, inputTokens) : 0;
467
+ const outTok = Number.isFinite(outputTokens) ? Math.max(0, outputTokens) : 0;
468
+ return (inTok * pricing.input + outTok * pricing.output) / 1_000_000;
378
469
  }
379
470
 
380
471
  function createOpenAiCompatibleAdapter(options = {}) {
381
472
  const env = options.env || process.env;
382
473
  const fetchImpl = options.fetchImpl || global.fetch;
383
474
 
384
- return async function openAiCompatibleAdapter({ request, model, provider }) {
475
+ return async function openAiCompatibleAdapter({ request, model, provider, endpoint }) {
385
476
  if (typeof fetchImpl !== 'function') throw new Error('fetch is unavailable');
386
477
  const local = provider === 'openai-compatible';
387
- const baseUrl = local
388
- ? env.THUMBGATE_LOCAL_MODEL_BASE_URL
389
- : (env.OPENAI_BASE_URL || 'https://api.openai.com/v1');
390
- const apiKey = local ? env.THUMBGATE_LOCAL_MODEL_API_KEY : env.OPENAI_API_KEY;
478
+ const baseUrl = endpoint?.baseUrl
479
+ || (local ? env.THUMBGATE_LOCAL_MODEL_BASE_URL : (env.OPENAI_BASE_URL || 'https://api.openai.com/v1'));
480
+ const apiKey = endpoint?.apiKey
481
+ || (local ? env.THUMBGATE_LOCAL_MODEL_API_KEY : env.OPENAI_API_KEY);
391
482
  if (!baseUrl) throw new Error('THUMBGATE_LOCAL_MODEL_BASE_URL is required for local routing');
392
483
  if (!local && !apiKey) throw new Error('OPENAI_API_KEY is required for OpenAI routing');
393
484
 
@@ -443,38 +534,56 @@ async function executeRoutedGeneration(task = {}, request = {}, options = {}) {
443
534
  const plan = recommendExecutionPlan(task, env);
444
535
  const tierConfig = config.tiers[plan.tier];
445
536
  if (!tierConfig) throw new Error(`missing configuration for tier "${plan.tier}"`);
446
- const { model, provider } = resolveGenerationTarget(plan, task, tierConfig, options, env);
447
- const adapters = options.adapters || createDefaultGenerationAdapters({ env, fetchImpl: options.fetchImpl });
448
- const adapter = adapters[plan.tier] || adapters[provider];
449
- if (typeof adapter !== 'function') throw new Error(`no generation adapter registered for provider "${provider}"`);
450
-
451
537
  const now = options.now || (() => Date.now());
452
538
  const startedAt = now();
539
+ // Resolution can fail (e.g. an unconfigured tier endpoint), so the base event
540
+ // starts from tier config defaults and target resolution happens inside the
541
+ // telemetry try block — misconfiguration emits an error event, not silence.
453
542
  const baseEvent = {
454
543
  timestamp: new Date().toISOString(),
455
544
  architecture: plan.architecture,
456
545
  taskType: task.type || 'unknown',
457
546
  riskLevel: task.riskLevel || 'unspecified',
458
547
  tier: plan.tier,
459
- provider,
460
- model,
548
+ provider: tierConfig.provider || null,
549
+ model: tierConfig.modelId || null,
461
550
  escalated: plan.escalated,
462
551
  routeReason: plan.reason,
463
552
  };
464
553
 
465
554
  try {
466
- const raw = await adapter({ task, request, plan, model, provider });
555
+ const { model, provider, endpoint } = resolveGenerationTarget(plan, task, tierConfig, options, env);
556
+ baseEvent.provider = provider;
557
+ baseEvent.model = model;
558
+ const adapters = options.adapters || createDefaultGenerationAdapters({ env, fetchImpl: options.fetchImpl });
559
+ const adapter = adapters[plan.tier] || adapters[provider];
560
+ if (typeof adapter !== 'function') throw new Error(`no generation adapter registered for provider "${provider}"`);
561
+ const raw = await adapter({ task, request, plan, model, provider, endpoint });
467
562
  const result = normalizeGenerationResult(raw, { model, provider });
563
+ const inputTokens = Number(result.usage?.input_tokens || result.usage?.prompt_tokens || 0) || null;
564
+ const outputTokens = Number(result.usage?.output_tokens || result.usage?.completion_tokens || 0) || null;
565
+ // When the adapter reports no cost but the tier declares per-token pricing,
566
+ // derive real cost from usage so telemetry and holdout cost math stay live.
567
+ // Never derive when usage is entirely unmeasured (an unmeasured request is
568
+ // unknown-cost, not free) or when execution actually ran on a local backend
569
+ // (tier pricing describes the cloud endpoint, not local inference).
570
+ let costCents = result.costCents;
571
+ const usageMeasured = inputTokens !== null || outputTokens !== null;
572
+ if ((costCents === null || costCents === undefined) && usageMeasured && plan.providerMode !== 'local') {
573
+ const estimatedUsd = estimateTierCostUsd(plan.tier, inputTokens || 0, outputTokens || 0, config);
574
+ costCents = estimatedUsd === null ? null : Number((estimatedUsd * 100).toFixed(4));
575
+ }
576
+ if (costCents === undefined) costCents = null;
468
577
  const event = {
469
578
  ...baseEvent,
470
579
  latencyMs: Math.max(0, now() - startedAt),
471
- inputTokens: Number(result.usage?.input_tokens || result.usage?.prompt_tokens || 0) || null,
472
- outputTokens: Number(result.usage?.output_tokens || result.usage?.completion_tokens || 0) || null,
473
- costCents: result.costCents,
580
+ inputTokens,
581
+ outputTokens,
582
+ costCents,
474
583
  outcome: 'success',
475
584
  };
476
585
  if (options.telemetrySink) await options.telemetrySink(event);
477
- return { ...result, route: plan, telemetry: event };
586
+ return { ...result, costCents, route: plan, telemetry: event };
478
587
  } catch (error) {
479
588
  const event = {
480
589
  ...baseEvent,
@@ -564,12 +673,30 @@ async function evaluateRoutingHoldout(cases, options = {}) {
564
673
  // Exports
565
674
  // ---------------------------------------------------------------------------
566
675
 
676
+ function isCostRouteQwenEnabled(task = {}, env = process.env) {
677
+ const flag = String(env.THUMBGATE_COST_ROUTE_QWEN || '').trim().toLowerCase();
678
+ if (flag === '1' || flag === 'true' || flag === 'yes' || flag === 'on') return true;
679
+ const tags = Array.isArray(task.tags) ? task.tags.map((t) => String(t).toLowerCase()) : [];
680
+ return Boolean(
681
+ task.costPriority === 'primary'
682
+ || task.highVolume
683
+ || tags.includes('cost-sensitive')
684
+ || tags.includes('high-volume')
685
+ || tags.includes('bulk')
686
+ || tags.includes('qwen-volume'),
687
+ );
688
+ }
689
+
567
690
  module.exports = {
691
+ isCostRouteQwenEnabled,
568
692
  TIERS,
693
+ TIER_ROUTING_ORDER,
569
694
  classifyTask,
695
+ estimateTierCostUsd,
570
696
  shouldEscalate,
571
697
  FrontierBudget,
572
698
  recommendExecutionPlan,
699
+ isSwitchyardEnabled,
573
700
  inferProvider,
574
701
  normalizeGenerationResult,
575
702
  resolveGenerationTarget,