ai-runtime-engine 1.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.
Files changed (269) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/LICENSE +15 -0
  3. package/README.md +489 -0
  4. package/dist/artifacts/artifacts.d.ts +28 -0
  5. package/dist/artifacts/artifacts.js +46 -0
  6. package/dist/benchmark/benchmark.d.ts +23 -0
  7. package/dist/benchmark/benchmark.js +40 -0
  8. package/dist/cli/cli.d.ts +6 -0
  9. package/dist/cli/cli.js +161 -0
  10. package/dist/cli/commands/cleanup.d.ts +47 -0
  11. package/dist/cli/commands/cleanup.js +96 -0
  12. package/dist/cli/commands/config.d.ts +8 -0
  13. package/dist/cli/commands/config.js +28 -0
  14. package/dist/cli/commands/doctor.d.ts +57 -0
  15. package/dist/cli/commands/doctor.js +86 -0
  16. package/dist/cli/commands/executions.d.ts +9 -0
  17. package/dist/cli/commands/executions.js +25 -0
  18. package/dist/cli/commands/info.d.ts +43 -0
  19. package/dist/cli/commands/info.js +53 -0
  20. package/dist/cli/commands/init.d.ts +5 -0
  21. package/dist/cli/commands/init.js +75 -0
  22. package/dist/cli/commands/inspect.d.ts +16 -0
  23. package/dist/cli/commands/inspect.js +60 -0
  24. package/dist/cli/commands/phase2.d.ts +22 -0
  25. package/dist/cli/commands/phase2.js +83 -0
  26. package/dist/cli/commands/route.d.ts +14 -0
  27. package/dist/cli/commands/route.js +49 -0
  28. package/dist/cli/commands/run.d.ts +11 -0
  29. package/dist/cli/commands/run.js +37 -0
  30. package/dist/cli/commands/setup.d.ts +34 -0
  31. package/dist/cli/commands/setup.js +104 -0
  32. package/dist/cli/commands/skills.d.ts +28 -0
  33. package/dist/cli/commands/skills.js +48 -0
  34. package/dist/cli/commands/test.d.ts +7 -0
  35. package/dist/cli/commands/test.js +29 -0
  36. package/dist/cli/context.d.ts +12 -0
  37. package/dist/cli/context.js +16 -0
  38. package/dist/cli/interactive/repl.d.ts +6 -0
  39. package/dist/cli/interactive/repl.js +45 -0
  40. package/dist/cli/interactive/session.d.ts +36 -0
  41. package/dist/cli/interactive/session.js +356 -0
  42. package/dist/cli/prompt.d.ts +6 -0
  43. package/dist/cli/prompt.js +18 -0
  44. package/dist/cli/render.d.ts +7 -0
  45. package/dist/cli/render.js +14 -0
  46. package/dist/comparison/analysis.d.ts +46 -0
  47. package/dist/comparison/analysis.js +177 -0
  48. package/dist/comparison/comparator.d.ts +46 -0
  49. package/dist/comparison/comparator.js +270 -0
  50. package/dist/comparison/comparison.d.ts +140 -0
  51. package/dist/comparison/comparison.js +9 -0
  52. package/dist/comparison/render.d.ts +7 -0
  53. package/dist/comparison/render.js +66 -0
  54. package/dist/config/defaults.d.ts +52 -0
  55. package/dist/config/defaults.js +56 -0
  56. package/dist/config/load.d.ts +17 -0
  57. package/dist/config/load.js +50 -0
  58. package/dist/config/providerDefaults.d.ts +17 -0
  59. package/dist/config/providerDefaults.js +61 -0
  60. package/dist/config/schema.d.ts +9 -0
  61. package/dist/config/schema.js +78 -0
  62. package/dist/context/budget.d.ts +13 -0
  63. package/dist/context/budget.js +17 -0
  64. package/dist/context/compiler.d.ts +61 -0
  65. package/dist/context/compiler.js +125 -0
  66. package/dist/context/tokens.d.ts +19 -0
  67. package/dist/context/tokens.js +38 -0
  68. package/dist/conversations/conversations.d.ts +38 -0
  69. package/dist/conversations/conversations.js +64 -0
  70. package/dist/core/capabilities/evidence.d.ts +40 -0
  71. package/dist/core/capabilities/evidence.js +102 -0
  72. package/dist/core/capabilities/overlay.d.ts +15 -0
  73. package/dist/core/capabilities/overlay.js +0 -0
  74. package/dist/core/capabilities/taxonomy.d.ts +19 -0
  75. package/dist/core/capabilities/taxonomy.js +25 -0
  76. package/dist/core/fallback/errors.d.ts +30 -0
  77. package/dist/core/fallback/errors.js +80 -0
  78. package/dist/core/fallback/fallback.d.ts +40 -0
  79. package/dist/core/fallback/fallback.js +82 -0
  80. package/dist/core/fallback/retryPolicy.d.ts +11 -0
  81. package/dist/core/fallback/retryPolicy.js +14 -0
  82. package/dist/core/health/health.d.ts +3 -0
  83. package/dist/core/health/health.js +5 -0
  84. package/dist/core/health/monitor.d.ts +23 -0
  85. package/dist/core/health/monitor.js +82 -0
  86. package/dist/core/policies/budget.d.ts +19 -0
  87. package/dist/core/policies/budget.js +37 -0
  88. package/dist/core/registry/builtinTasks.d.ts +8 -0
  89. package/dist/core/registry/builtinTasks.js +54 -0
  90. package/dist/core/registry/registry.d.ts +18 -0
  91. package/dist/core/registry/registry.js +33 -0
  92. package/dist/core/registry/taskRegistry.d.ts +15 -0
  93. package/dist/core/registry/taskRegistry.js +30 -0
  94. package/dist/core/router/confidence.d.ts +7 -0
  95. package/dist/core/router/confidence.js +20 -0
  96. package/dist/core/router/dimensions.d.ts +16 -0
  97. package/dist/core/router/dimensions.js +60 -0
  98. package/dist/core/router/executor.d.ts +16 -0
  99. package/dist/core/router/executor.js +25 -0
  100. package/dist/core/router/filter.d.ts +34 -0
  101. package/dist/core/router/filter.js +113 -0
  102. package/dist/core/router/normalize.d.ts +30 -0
  103. package/dist/core/router/normalize.js +119 -0
  104. package/dist/core/router/request.d.ts +4 -0
  105. package/dist/core/router/request.js +21 -0
  106. package/dist/core/router/router.d.ts +32 -0
  107. package/dist/core/router/router.js +195 -0
  108. package/dist/core/router/routingPrefs.d.ts +11 -0
  109. package/dist/core/router/routingPrefs.js +30 -0
  110. package/dist/core/router/scorer.d.ts +19 -0
  111. package/dist/core/router/scorer.js +50 -0
  112. package/dist/core/router/weights.d.ts +9 -0
  113. package/dist/core/router/weights.js +31 -0
  114. package/dist/core/validation/validator.d.ts +16 -0
  115. package/dist/core/validation/validator.js +33 -0
  116. package/dist/discovery/modelCatalog.d.ts +28 -0
  117. package/dist/discovery/modelCatalog.js +105 -0
  118. package/dist/discovery/openapi.d.ts +25 -0
  119. package/dist/discovery/openapi.js +76 -0
  120. package/dist/executions/checkpoint.d.ts +26 -0
  121. package/dist/executions/checkpoint.js +114 -0
  122. package/dist/executions/execution.d.ts +51 -0
  123. package/dist/executions/execution.js +8 -0
  124. package/dist/executions/store.d.ts +52 -0
  125. package/dist/executions/store.js +124 -0
  126. package/dist/generation/generateAdapter.d.ts +17 -0
  127. package/dist/generation/generateAdapter.js +30 -0
  128. package/dist/index.d.ts +147 -0
  129. package/dist/index.js +107 -0
  130. package/dist/learning/feedback.d.ts +9 -0
  131. package/dist/learning/feedback.js +18 -0
  132. package/dist/learning/learningStore.d.ts +68 -0
  133. package/dist/learning/learningStore.js +138 -0
  134. package/dist/learning/performanceStore.d.ts +27 -0
  135. package/dist/learning/performanceStore.js +0 -0
  136. package/dist/marketplace/presets.d.ts +24 -0
  137. package/dist/marketplace/presets.js +52 -0
  138. package/dist/mcp/mcp.d.ts +31 -0
  139. package/dist/mcp/mcp.js +54 -0
  140. package/dist/memory/bm25.d.ts +16 -0
  141. package/dist/memory/bm25.js +56 -0
  142. package/dist/memory/classifier.d.ts +14 -0
  143. package/dist/memory/classifier.js +17 -0
  144. package/dist/memory/memory.d.ts +80 -0
  145. package/dist/memory/memory.js +191 -0
  146. package/dist/orchestration/executor.d.ts +35 -0
  147. package/dist/orchestration/executor.js +65 -0
  148. package/dist/orchestration/orchestrator.d.ts +42 -0
  149. package/dist/orchestration/orchestrator.js +63 -0
  150. package/dist/orchestration/plan.d.ts +37 -0
  151. package/dist/orchestration/plan.js +70 -0
  152. package/dist/orchestration/planner.d.ts +29 -0
  153. package/dist/orchestration/planner.js +69 -0
  154. package/dist/plugin/ai.d.ts +82 -0
  155. package/dist/plugin/ai.js +167 -0
  156. package/dist/probing/probe.d.ts +25 -0
  157. package/dist/probing/probe.js +63 -0
  158. package/dist/providers/factory.d.ts +18 -0
  159. package/dist/providers/factory.js +54 -0
  160. package/dist/providers/httpClient.d.ts +34 -0
  161. package/dist/providers/httpClient.js +80 -0
  162. package/dist/providers/httpProvider.d.ts +49 -0
  163. package/dist/providers/httpProvider.js +135 -0
  164. package/dist/providers/mock/demo.d.ts +13 -0
  165. package/dist/providers/mock/demo.js +58 -0
  166. package/dist/providers/mock/mockProvider.d.ts +35 -0
  167. package/dist/providers/mock/mockProvider.js +121 -0
  168. package/dist/providers/mock/scenarios.d.ts +44 -0
  169. package/dist/providers/mock/scenarios.js +30 -0
  170. package/dist/providers/provider.d.ts +26 -0
  171. package/dist/providers/provider.js +11 -0
  172. package/dist/providers/wire/anthropicWire.d.ts +6 -0
  173. package/dist/providers/wire/anthropicWire.js +83 -0
  174. package/dist/providers/wire/openaiWire.d.ts +7 -0
  175. package/dist/providers/wire/openaiWire.js +81 -0
  176. package/dist/providers/wire/registry.d.ts +8 -0
  177. package/dist/providers/wire/registry.js +20 -0
  178. package/dist/providers/wire/types.d.ts +39 -0
  179. package/dist/providers/wire/types.js +24 -0
  180. package/dist/runtime/config.d.ts +31 -0
  181. package/dist/runtime/config.js +121 -0
  182. package/dist/runtime/context.d.ts +34 -0
  183. package/dist/runtime/context.js +11 -0
  184. package/dist/runtime/events.d.ts +99 -0
  185. package/dist/runtime/events.js +82 -0
  186. package/dist/runtime/host.d.ts +27 -0
  187. package/dist/runtime/host.js +7 -0
  188. package/dist/runtime/intent/classifier.d.ts +30 -0
  189. package/dist/runtime/intent/classifier.js +60 -0
  190. package/dist/runtime/intent/signals.d.ts +19 -0
  191. package/dist/runtime/intent/signals.js +46 -0
  192. package/dist/runtime/modes/availability.d.ts +11 -0
  193. package/dist/runtime/modes/availability.js +17 -0
  194. package/dist/runtime/modes/chat.d.ts +18 -0
  195. package/dist/runtime/modes/chat.js +67 -0
  196. package/dist/runtime/modes/modeResolver.d.ts +43 -0
  197. package/dist/runtime/modes/modeResolver.js +78 -0
  198. package/dist/runtime/policy.d.ts +72 -0
  199. package/dist/runtime/policy.js +59 -0
  200. package/dist/runtime/providerView.d.ts +62 -0
  201. package/dist/runtime/providerView.js +105 -0
  202. package/dist/runtime/routing.d.ts +26 -0
  203. package/dist/runtime/routing.js +65 -0
  204. package/dist/runtime/runtime.d.ts +191 -0
  205. package/dist/runtime/runtime.js +718 -0
  206. package/dist/runtime/types.d.ts +153 -0
  207. package/dist/runtime/types.js +9 -0
  208. package/dist/runtime/workspace/detectors.d.ts +15 -0
  209. package/dist/runtime/workspace/detectors.js +57 -0
  210. package/dist/runtime/workspace/workspace.d.ts +29 -0
  211. package/dist/runtime/workspace/workspace.js +116 -0
  212. package/dist/security/credentials.d.ts +26 -0
  213. package/dist/security/credentials.js +34 -0
  214. package/dist/security/redact.d.ts +16 -0
  215. package/dist/security/redact.js +57 -0
  216. package/dist/skills/builtins/fileAnalyzer.d.ts +7 -0
  217. package/dist/skills/builtins/fileAnalyzer.js +47 -0
  218. package/dist/skills/builtins/repositoryAnalyzer.d.ts +6 -0
  219. package/dist/skills/builtins/repositoryAnalyzer.js +47 -0
  220. package/dist/skills/discovery.d.ts +61 -0
  221. package/dist/skills/discovery.js +211 -0
  222. package/dist/skills/manifest.d.ts +30 -0
  223. package/dist/skills/manifest.js +75 -0
  224. package/dist/skills/registry.d.ts +15 -0
  225. package/dist/skills/registry.js +22 -0
  226. package/dist/skills/skill.d.ts +64 -0
  227. package/dist/skills/skill.js +8 -0
  228. package/dist/store/area.d.ts +54 -0
  229. package/dist/store/area.js +164 -0
  230. package/dist/store/paths.d.ts +15 -0
  231. package/dist/store/paths.js +48 -0
  232. package/dist/store/store.d.ts +59 -0
  233. package/dist/store/store.js +140 -0
  234. package/dist/telemetry/sinks/file.d.ts +12 -0
  235. package/dist/telemetry/sinks/file.js +28 -0
  236. package/dist/telemetry/telemetry.d.ts +36 -0
  237. package/dist/telemetry/telemetry.js +63 -0
  238. package/dist/tools/builtins/filesystem.d.ts +7 -0
  239. package/dist/tools/builtins/filesystem.js +53 -0
  240. package/dist/tools/builtins/git.d.ts +10 -0
  241. package/dist/tools/builtins/git.js +66 -0
  242. package/dist/tools/builtins/shell.d.ts +17 -0
  243. package/dist/tools/builtins/shell.js +91 -0
  244. package/dist/tools/jail.d.ts +12 -0
  245. package/dist/tools/jail.js +98 -0
  246. package/dist/tools/permissions.d.ts +25 -0
  247. package/dist/tools/permissions.js +24 -0
  248. package/dist/tools/registry.d.ts +10 -0
  249. package/dist/tools/registry.js +20 -0
  250. package/dist/tools/runner.d.ts +23 -0
  251. package/dist/tools/runner.js +64 -0
  252. package/dist/tools/tool.d.ts +53 -0
  253. package/dist/tools/tool.js +24 -0
  254. package/dist/tools/untrusted.d.ts +13 -0
  255. package/dist/tools/untrusted.js +30 -0
  256. package/dist/types.d.ts +460 -0
  257. package/dist/types.js +12 -0
  258. package/dist/util/clock.d.ts +6 -0
  259. package/dist/util/clock.js +4 -0
  260. package/dist/util/extractJson.d.ts +8 -0
  261. package/dist/util/extractJson.js +54 -0
  262. package/dist/verification/verify.d.ts +26 -0
  263. package/dist/verification/verify.js +67 -0
  264. package/docs/GUIDE.md +358 -0
  265. package/docs/README.md +21 -0
  266. package/docs/architecture.md +78 -0
  267. package/docs/router.md +376 -0
  268. package/docs/security.md +55 -0
  269. package/package.json +67 -0
@@ -0,0 +1,460 @@
1
+ /**
2
+ * Public, cross-cutting contracts for the AI Router. These are provider-agnostic on purpose:
3
+ * nothing here names a vendor. Provider adapters and the vendor catalog translate their reality
4
+ * into these shapes; `core/` only ever sees these.
5
+ */
6
+ import type { AgentCapability, BooleanCapabilityGroup, InputModality, IntelligenceSkill, OutputModality } from './core/capabilities/taxonomy.js';
7
+ /**
8
+ * How sure are we that a capability is real? Ordered from least to most trustworthy.
9
+ * INVARIANT: `unknown` and `unsupported` never satisfy a requirement, and nothing in the system
10
+ * may silently promote a lower rank to a higher one (see evidence.ts).
11
+ */
12
+ export type Evidence = 'unsupported' | 'unknown' | 'inferred' | 'documented' | 'verified';
13
+ export declare const EVIDENCE_RANK: Record<Evidence, number>;
14
+ export interface Capability<V = boolean> {
15
+ value: V;
16
+ evidence: Evidence;
17
+ note?: string;
18
+ }
19
+ export interface CapabilityProfile {
20
+ input: Partial<Record<InputModality, Capability>>;
21
+ output: Partial<Record<OutputModality, Capability>>;
22
+ intelligence: Partial<Record<IntelligenceSkill, Capability>>;
23
+ agent: Partial<Record<AgentCapability, Capability>>;
24
+ context: {
25
+ contextWindow: Capability<number>;
26
+ maxOutputTokens?: Capability<number>;
27
+ };
28
+ }
29
+ /** A single required capability, addressed by (group, key). Context is expressed via minContextWindow. */
30
+ export interface CapabilityRequirement {
31
+ group: BooleanCapabilityGroup;
32
+ key: string;
33
+ /** Minimum evidence rank that counts as "supported". Defaults to `documented`. */
34
+ minEvidence?: Evidence;
35
+ /** Optional per-requirement weight used by capabilityFit scoring. */
36
+ weight?: number;
37
+ }
38
+ export type OutputFormat = 'text' | 'json' | 'structured_output' | 'code';
39
+ export type InputPart = {
40
+ kind: 'text';
41
+ text: string;
42
+ } | {
43
+ kind: 'image' | 'audio' | 'video' | 'file' | 'pdf';
44
+ mime: string;
45
+ data: string;
46
+ };
47
+ export interface ToolSpec {
48
+ name: string;
49
+ description?: string;
50
+ parameters?: unknown;
51
+ }
52
+ export interface ToolCall {
53
+ id?: string;
54
+ name: string;
55
+ arguments: unknown;
56
+ }
57
+ /** Declared sensitivity of the input. `unknown` is treated as most-restrictive (fail-closed). */
58
+ export type Sensitivity = 'low' | 'high' | 'unknown';
59
+ /** What a provider adapter receives. `model` is filled in by the selector. */
60
+ export interface AIRequest {
61
+ taskId: string;
62
+ model: string;
63
+ input: {
64
+ text?: string;
65
+ parts?: InputPart[];
66
+ };
67
+ system?: string;
68
+ output?: {
69
+ format: OutputFormat;
70
+ schema?: unknown;
71
+ };
72
+ tools?: ToolSpec[];
73
+ params?: {
74
+ temperature?: number;
75
+ maxTokens?: number;
76
+ topP?: number;
77
+ stop?: string[];
78
+ };
79
+ timeoutMs: number;
80
+ signal?: AbortSignal;
81
+ sensitivity: Sensitivity;
82
+ metadata?: Record<string, unknown>;
83
+ }
84
+ export type FinishReason = 'stop' | 'length' | 'tool_calls' | 'content_filter' | 'error';
85
+ /** What every provider adapter returns — normalized so callers never parse vendor JSON. */
86
+ export interface AIResponse {
87
+ text?: string;
88
+ json?: unknown;
89
+ toolCalls?: ToolCall[];
90
+ finishReason: FinishReason;
91
+ usage?: {
92
+ inputTokens?: number;
93
+ outputTokens?: number;
94
+ totalTokens?: number;
95
+ };
96
+ providerId: string;
97
+ model: string;
98
+ latencyMs: number;
99
+ /** Raw provider payload — OFF by default and redacted when on. */
100
+ raw?: unknown;
101
+ }
102
+ export type ErrorCategory = 'AUTHENTICATION' | 'AUTHORIZATION' | 'RATE_LIMIT' | 'QUOTA' | 'MODEL_NOT_FOUND' | 'CAPABILITY' | 'INVALID_REQUEST' | 'TIMEOUT' | 'NETWORK' | 'PROVIDER' | 'RESPONSE_VALIDATION' | 'BUDGET' | 'PRIVACY_VIOLATION' | 'NO_CAPABLE_PROVIDER' | 'ALL_PROVIDERS_FAILED' | 'CONFIG' | 'UNKNOWN';
103
+ export interface RouterErrorInfo {
104
+ category: ErrorCategory;
105
+ message: string;
106
+ retryable: boolean;
107
+ providerId?: string;
108
+ model?: string;
109
+ }
110
+ export type HealthState = 'NOT_CONFIGURED' | 'CONFIGURED' | 'AVAILABLE' | 'DEGRADED' | 'RATE_LIMITED' | 'QUOTA_EXCEEDED' | 'AUTH_FAILED' | 'NETWORK_ERROR' | 'MODEL_UNAVAILABLE' | 'CAPABILITY_MISMATCH' | 'DISABLED' | 'UNKNOWN';
111
+ export interface HealthStatus {
112
+ providerId: string;
113
+ state: HealthState;
114
+ routable: boolean;
115
+ checkedAt: number;
116
+ latencyMs?: number;
117
+ cooldownUntil?: number;
118
+ /** Human-readable detail — always redacted before it is stored or emitted. */
119
+ detail?: string;
120
+ }
121
+ export interface Pricing {
122
+ inputPerMTok?: number;
123
+ outputPerMTok?: number;
124
+ currency?: string;
125
+ }
126
+ export type QualityTier = 'frontier' | 'strong' | 'mid' | 'small';
127
+ export interface ModelMetadata {
128
+ providerId: string;
129
+ id: string;
130
+ family?: string;
131
+ displayName?: string;
132
+ capabilities: CapabilityProfile;
133
+ contextWindow?: number;
134
+ maxOutputTokens?: number;
135
+ pricing?: Pricing;
136
+ quality?: {
137
+ tier?: QualityTier;
138
+ score?: number;
139
+ evidence: Evidence;
140
+ };
141
+ /** Observed/assumed reliability 0..1 with its evidence. */
142
+ reliability?: {
143
+ successRate: number;
144
+ evidence: Evidence;
145
+ };
146
+ /** Observed/assumed latency in ms with its evidence. */
147
+ latency?: {
148
+ p50Ms?: number;
149
+ p95Ms?: number;
150
+ evidence: Evidence;
151
+ };
152
+ enabled: boolean;
153
+ source: Evidence;
154
+ }
155
+ export type ProviderKind = 'openai-compatible' | 'gemini' | 'groq' | 'anthropic' | 'ollama' | 'custom' | 'mock';
156
+ export interface ProviderMetadata {
157
+ id: string;
158
+ name: string;
159
+ kind: ProviderKind;
160
+ /** baseUrl ORIGIN only — never a full request URL, never a key-in-query. */
161
+ baseOrigin?: string;
162
+ authMode: 'env' | 'os-store' | 'none';
163
+ supportsModelListing: boolean;
164
+ defaultModel?: string;
165
+ models: ModelMetadata[];
166
+ privacyClass: PrivacyClass;
167
+ }
168
+ export interface ExecutionEstimate {
169
+ providerId: string;
170
+ model: string;
171
+ estInputTokens?: number;
172
+ estOutputTokens?: number;
173
+ estCost?: {
174
+ amount: number;
175
+ currency: string;
176
+ evidence: Evidence;
177
+ };
178
+ estLatencyMs?: {
179
+ p50?: number;
180
+ p95?: number;
181
+ evidence: Evidence;
182
+ };
183
+ contextFits: boolean;
184
+ }
185
+ /** `local` may receive high-sensitivity input; `cloud` is untrusted/external (the fail-closed default). */
186
+ export type PrivacyClass = 'local' | 'cloud';
187
+ export interface PrivacyRequirement {
188
+ /** Force a floor regardless of task input, e.g. a task that must stay local. */
189
+ requireLocal?: boolean;
190
+ }
191
+ export interface TaskDefinition {
192
+ id: string;
193
+ aliases?: string[];
194
+ description?: string;
195
+ required: CapabilityRequirement[];
196
+ preferred?: CapabilityRequirement[];
197
+ minContextWindow?: number;
198
+ output?: {
199
+ format: OutputFormat;
200
+ schema?: unknown;
201
+ };
202
+ defaultStrategy?: Strategy;
203
+ privacy?: PrivacyRequirement;
204
+ qualityFloor?: QualityTier;
205
+ }
206
+ export interface NormalizedTask {
207
+ id: string;
208
+ required: CapabilityRequirement[];
209
+ preferred: CapabilityRequirement[];
210
+ minContextWindow?: number;
211
+ output?: {
212
+ format: OutputFormat;
213
+ schema?: unknown;
214
+ };
215
+ strategy: Strategy;
216
+ sensitivity: Sensitivity;
217
+ requireLocal: boolean;
218
+ /** Low when the task was unknown and requirements had to be inferred. */
219
+ confidence: number;
220
+ }
221
+ export type Strategy = 'best' | 'fastest' | 'cheapest' | 'highest-quality' | 'local-only' | 'cloud-only' | 'privacy-first' | 'provider-specific' | 'fallback-only';
222
+ export interface ScoreWeights {
223
+ capabilityFit: number;
224
+ quality: number;
225
+ reliability: number;
226
+ historicalSuccess: number;
227
+ latency: number;
228
+ cost: number;
229
+ userPreference: number;
230
+ }
231
+ export type ScoreBreakdown = Record<keyof ScoreWeights, number>;
232
+ /** A (provider, model) pair under consideration, carrying everything the scorer needs. */
233
+ export interface Candidate {
234
+ providerId: string;
235
+ model: ModelMetadata;
236
+ }
237
+ export interface ExclusionReason {
238
+ providerId: string;
239
+ model: string;
240
+ reason: string;
241
+ }
242
+ export interface RankedCandidate {
243
+ providerId: string;
244
+ model: string;
245
+ score: number;
246
+ breakdown: ScoreBreakdown;
247
+ }
248
+ export interface AttemptRecord {
249
+ providerId: string;
250
+ model: string;
251
+ outcome: 'success' | 'retryable' | 'non-retryable';
252
+ category?: ErrorCategory;
253
+ latencyMs: number;
254
+ }
255
+ export interface ValidationReport {
256
+ ok: boolean;
257
+ checks: Array<{
258
+ name: string;
259
+ ok: boolean;
260
+ why?: string;
261
+ }>;
262
+ }
263
+ export interface RoutingReport {
264
+ taskId: string;
265
+ strategy: Strategy;
266
+ weights: ScoreWeights;
267
+ requiredCapabilities: CapabilityRequirement[];
268
+ consideredCount: number;
269
+ filteredOut: ExclusionReason[];
270
+ ranked: RankedCandidate[];
271
+ selected?: {
272
+ providerId: string;
273
+ model: string;
274
+ };
275
+ attempts: AttemptRecord[];
276
+ validation?: ValidationReport;
277
+ /** True when the winning result's confidence fell below the configured minConfidence. */
278
+ belowConfidenceThreshold?: boolean;
279
+ /** Present when multi-model verification ran (config or per-run `verification`). */
280
+ verification?: VerificationReport;
281
+ }
282
+ export interface VerificationReport {
283
+ verifierProviderId: string;
284
+ verifierModel: string;
285
+ agreement: 'agree' | 'disagree' | 'inconclusive';
286
+ reason?: string;
287
+ }
288
+ export interface RunResult<T = AIResponse> {
289
+ ok: boolean;
290
+ response?: T;
291
+ error?: RouterErrorInfo;
292
+ confidence: number;
293
+ routing: RoutingReport;
294
+ }
295
+ export interface RunRequest {
296
+ task: string;
297
+ input: string | {
298
+ text?: string;
299
+ parts?: InputPart[];
300
+ };
301
+ system?: string;
302
+ requirements?: Partial<{
303
+ input: string[];
304
+ output: string[];
305
+ intelligence: string[];
306
+ agent: string[];
307
+ minContextWindow: number;
308
+ }>;
309
+ constraints?: {
310
+ maxCostUsd?: number;
311
+ maxLatencyMs?: number;
312
+ minimumConfidence?: number;
313
+ allowProviders?: string[];
314
+ denyProviders?: string[];
315
+ };
316
+ strategy?: Strategy;
317
+ /** Explicit override: pin a provider (and optionally model). Still passes through all filters. */
318
+ provider?: string;
319
+ model?: string;
320
+ /** User exclude/prefer routing controls. EXCLUDE is a hard filter; PREFER is a soft scoring nudge. */
321
+ routing?: RoutingPreferences;
322
+ sensitivity?: Sensitivity;
323
+ output?: {
324
+ format: OutputFormat;
325
+ schema?: unknown;
326
+ };
327
+ tools?: ToolSpec[];
328
+ params?: AIRequest['params'];
329
+ /** Per-run spend guardrail (overrides config.budget). */
330
+ budget?: {
331
+ maxCostUsd?: number;
332
+ maxCalls?: number;
333
+ };
334
+ /** Run a second model to verify the result before returning (overrides config.verification). */
335
+ verification?: boolean;
336
+ /** Attach tools from registered MCP sources to this run (requires a tool-calling model). */
337
+ mcp?: boolean;
338
+ }
339
+ /**
340
+ * User exclude/prefer routing (all optional). EXCLUDE is a HARD filter — an excluded candidate is never
341
+ * selected but stays visible in the routing trace (`⊘ excluded by user configuration`). PREFER is a SOFT
342
+ * scoring preference — a preferred-but-incapable/unhealthy/inaccessible candidate still loses. Model
343
+ * entries accept `provider:model` (exact) or a bare `model` id (any provider). Learning can never
344
+ * override an exclusion.
345
+ */
346
+ export interface RoutingPreferences {
347
+ excludeProviders?: string[];
348
+ excludeModels?: string[];
349
+ preferProviders?: string[];
350
+ preferModels?: string[];
351
+ }
352
+ export interface DeclaredCapabilities {
353
+ input?: string[];
354
+ output?: string[];
355
+ intelligence?: string[];
356
+ agent?: string[];
357
+ contextWindow?: number;
358
+ }
359
+ export interface ProviderConfig {
360
+ id: string;
361
+ kind: ProviderKind;
362
+ baseUrl?: string;
363
+ /** NAME of the environment variable that holds the key — never the value itself. */
364
+ apiKeyEnv?: string;
365
+ enabled?: boolean;
366
+ models?: string[] | 'auto';
367
+ defaultModel?: string;
368
+ /** Operator-asserted capabilities for providers not in the catalog (tagged `documented`). */
369
+ capabilities?: DeclaredCapabilities;
370
+ privacyClass?: PrivacyClass;
371
+ wireShape?: 'openai' | 'anthropic';
372
+ /** Non-secret headers only. */
373
+ headers?: Record<string, string>;
374
+ weightOverrides?: Partial<ScoreWeights>;
375
+ }
376
+ export interface TelemetryConfig {
377
+ enabled: boolean;
378
+ sink: 'memory' | 'file';
379
+ storePrompts: false;
380
+ path?: string;
381
+ }
382
+ export interface RouterConfig {
383
+ providers: ProviderConfig[];
384
+ strategy?: Strategy;
385
+ weights?: Partial<ScoreWeights>;
386
+ defaults?: {
387
+ timeoutMs?: number;
388
+ maxFallbacks?: number;
389
+ minEvidence?: Evidence;
390
+ minConfidence?: number;
391
+ };
392
+ privacy?: {
393
+ /** Cloud providers allowed at all (default true — keeps a single cloud provider working). */
394
+ allowCloud?: boolean;
395
+ allowLocal?: boolean;
396
+ /** May HIGH-sensitivity input go to cloud? Default false — the core "never silently leak" guarantee. */
397
+ sensitiveDataAllowedOnCloud?: boolean;
398
+ /** Strict opt-in: treat undeclared (`unknown`) sensitivity as sensitive (fail-closed). Default false. */
399
+ treatUnknownAsSensitive?: boolean;
400
+ defaultSensitivity?: Sensitivity;
401
+ };
402
+ telemetry?: Partial<TelemetryConfig>;
403
+ /** Learning-based routing: feed observed success/latency into scoring (default enabled). */
404
+ learning?: {
405
+ enabled?: boolean;
406
+ };
407
+ /** Multi-model verification default for high-risk tasks (per-run `verification` overrides). */
408
+ verification?: {
409
+ enabled?: boolean;
410
+ };
411
+ /** Spend guardrails applied per `run()` across primary + fallbacks + verification. */
412
+ budget?: {
413
+ maxCostUsd?: number;
414
+ maxCalls?: number;
415
+ };
416
+ /** Team policy: org-level guardrails enforced on EVERY run (merged with per-run constraints). */
417
+ policy?: {
418
+ allowProviders?: string[];
419
+ denyProviders?: string[];
420
+ requireLocal?: boolean;
421
+ maxCostUsd?: number;
422
+ strategy?: Strategy;
423
+ };
424
+ tasks?: TaskDefinition[];
425
+ }
426
+ export type TelemetryEvent = {
427
+ type: 'route.decision';
428
+ ts: number;
429
+ taskId: string;
430
+ strategy: Strategy;
431
+ considered: number;
432
+ selected?: {
433
+ providerId: string;
434
+ model: string;
435
+ };
436
+ topScore?: number;
437
+ } | {
438
+ type: 'route.attempt';
439
+ ts: number;
440
+ taskId: string;
441
+ providerId: string;
442
+ model: string;
443
+ outcome: 'success' | 'retryable' | 'non-retryable';
444
+ category?: ErrorCategory;
445
+ latencyMs: number;
446
+ } | {
447
+ type: 'route.result';
448
+ ts: number;
449
+ taskId: string;
450
+ ok: boolean;
451
+ confidence: number;
452
+ totalLatencyMs: number;
453
+ fallbackCount: number;
454
+ } | {
455
+ type: 'health.transition';
456
+ ts: number;
457
+ providerId: string;
458
+ from: HealthState;
459
+ to: HealthState;
460
+ };
package/dist/types.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Public, cross-cutting contracts for the AI Router. These are provider-agnostic on purpose:
3
+ * nothing here names a vendor. Provider adapters and the vendor catalog translate their reality
4
+ * into these shapes; `core/` only ever sees these.
5
+ */
6
+ export const EVIDENCE_RANK = {
7
+ unsupported: 0,
8
+ unknown: 1,
9
+ inferred: 2,
10
+ documented: 3,
11
+ verified: 4,
12
+ };
@@ -0,0 +1,6 @@
1
+ /** An injectable clock so timing-dependent logic (cooldowns, latency, backoff) is deterministic in tests. */
2
+ export interface Clock {
3
+ now(): number;
4
+ sleep(ms: number): Promise<void>;
5
+ }
6
+ export declare const systemClock: Clock;
@@ -0,0 +1,4 @@
1
+ export const systemClock = {
2
+ now: () => Date.now(),
3
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
4
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Pull the first balanced JSON object out of a model response.
3
+ *
4
+ * Models wrap JSON in prose or ``` fences often enough that failing on it would be a self-inflicted
5
+ * flake; anything beyond a single balanced top-level object is a hard error, not a heuristic guess.
6
+ * Pure and dependency-free, so it is trivially unit-testable.
7
+ */
8
+ export declare function extractJson(text: unknown): unknown;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Pull the first balanced JSON object out of a model response.
3
+ *
4
+ * Models wrap JSON in prose or ``` fences often enough that failing on it would be a self-inflicted
5
+ * flake; anything beyond a single balanced top-level object is a hard error, not a heuristic guess.
6
+ * Pure and dependency-free, so it is trivially unit-testable.
7
+ */
8
+ import { AIError } from '../core/fallback/errors.js';
9
+ export function extractJson(text) {
10
+ const raw = String(text ?? '').trim();
11
+ if (!raw)
12
+ throw new AIError('model returned an empty response', { category: 'RESPONSE_VALIDATION' });
13
+ const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i);
14
+ const body = (fenced ? fenced[1] : raw).trim();
15
+ const start = body.indexOf('{');
16
+ if (start === -1) {
17
+ throw new AIError('model response contained no JSON object', { category: 'RESPONSE_VALIDATION' });
18
+ }
19
+ let depth = 0;
20
+ let inString = false;
21
+ let escaped = false;
22
+ for (let i = start; i < body.length; i += 1) {
23
+ const ch = body[i];
24
+ if (inString) {
25
+ if (escaped)
26
+ escaped = false;
27
+ else if (ch === '\\')
28
+ escaped = true;
29
+ else if (ch === '"')
30
+ inString = false;
31
+ continue;
32
+ }
33
+ if (ch === '"')
34
+ inString = true;
35
+ else if (ch === '{')
36
+ depth += 1;
37
+ else if (ch === '}') {
38
+ depth -= 1;
39
+ if (depth === 0) {
40
+ const slice = body.slice(start, i + 1);
41
+ try {
42
+ return JSON.parse(slice);
43
+ }
44
+ catch (e) {
45
+ const why = e instanceof Error ? e.message : String(e);
46
+ throw new AIError(`model response was not valid JSON (${why})`, { category: 'RESPONSE_VALIDATION' });
47
+ }
48
+ }
49
+ }
50
+ }
51
+ throw new AIError('model response ended mid-JSON (likely truncated by max_tokens)', {
52
+ category: 'RESPONSE_VALIDATION',
53
+ });
54
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Multi-model verification. After a primary result, a SECOND model — preferably from a
3
+ * different provider — judges the answer and returns a structured verdict. Opt-in, because it at least
4
+ * doubles cost. The judge is provider-agnostic: it receives the task and the candidate answer and must
5
+ * reply with JSON `{ "valid": boolean, "reason": string }`. A parse failure or no distinct verifier is
6
+ * reported as `inconclusive` rather than blocking the result.
7
+ */
8
+ import type { AIResponse, NormalizedTask, VerificationReport } from '../types.js';
9
+ import type { ScoredCandidate } from '../core/router/scorer.js';
10
+ import type { RequestTemplate } from '../core/router/normalize.js';
11
+ import type { AIProvider } from '../providers/provider.js';
12
+ import type { BudgetTracker } from '../core/policies/budget.js';
13
+ export interface VerifyInput {
14
+ task: NormalizedTask;
15
+ template: RequestTemplate;
16
+ primary: {
17
+ providerId: string;
18
+ model: string;
19
+ response: AIResponse;
20
+ };
21
+ ranked: ScoredCandidate[];
22
+ getProvider: (id: string) => AIProvider | undefined;
23
+ budget?: BudgetTracker;
24
+ costOf?: (providerId: string, model: string) => Promise<number>;
25
+ }
26
+ export declare function runVerification(input: VerifyInput): Promise<VerificationReport | undefined>;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Multi-model verification. After a primary result, a SECOND model — preferably from a
3
+ * different provider — judges the answer and returns a structured verdict. Opt-in, because it at least
4
+ * doubles cost. The judge is provider-agnostic: it receives the task and the candidate answer and must
5
+ * reply with JSON `{ "valid": boolean, "reason": string }`. A parse failure or no distinct verifier is
6
+ * reported as `inconclusive` rather than blocking the result.
7
+ */
8
+ import { buildRequest } from '../core/router/request.js';
9
+ import { executeOnce } from '../core/router/executor.js';
10
+ import { extractJson } from '../util/extractJson.js';
11
+ function pickVerifier(ranked, primaryProviderId, primaryModel) {
12
+ // Prefer a different provider; otherwise a different model on the same provider.
13
+ return (ranked.find((c) => c.candidate.providerId !== primaryProviderId) ??
14
+ ranked.find((c) => !(c.candidate.providerId === primaryProviderId && c.candidate.model.id === primaryModel)));
15
+ }
16
+ function candidateAnswer(response) {
17
+ if (response.text)
18
+ return response.text;
19
+ if (response.json !== undefined)
20
+ return JSON.stringify(response.json);
21
+ return '(no answer)';
22
+ }
23
+ export async function runVerification(input) {
24
+ const verifier = pickVerifier(input.ranked, input.primary.providerId, input.primary.model);
25
+ if (!verifier || (verifier.candidate.providerId === input.primary.providerId && verifier.candidate.model.id === input.primary.model)) {
26
+ return { verifierProviderId: input.primary.providerId, verifierModel: input.primary.model, agreement: 'inconclusive', reason: 'no distinct verifier available' };
27
+ }
28
+ const provider = input.getProvider(verifier.candidate.providerId);
29
+ if (!provider)
30
+ return undefined;
31
+ const estCost = input.budget?.active && input.costOf ? await input.costOf(verifier.candidate.providerId, verifier.candidate.model.id) : 0;
32
+ if (input.budget && !input.budget.canSpend(estCost)) {
33
+ return { verifierProviderId: verifier.candidate.providerId, verifierModel: verifier.candidate.model.id, agreement: 'inconclusive', reason: 'budget exhausted before verification' };
34
+ }
35
+ const original = input.template.input.text ?? '(non-text input)';
36
+ const answer = candidateAnswer(input.primary.response);
37
+ const verifyTemplate = {
38
+ taskId: `${input.template.taskId}:verify`,
39
+ input: {
40
+ text: `You are a strict verifier. TASK "${input.task.id}":\n${original}\n\nCANDIDATE ANSWER:\n${answer}\n\nReply ONLY with JSON: {"valid": true|false, "reason": "<short>"}.`,
41
+ },
42
+ system: 'You verify whether a candidate answer correctly and completely satisfies the task. Respond only with the requested JSON.',
43
+ output: { format: 'json' },
44
+ timeoutMs: input.template.timeoutMs,
45
+ sensitivity: input.template.sensitivity,
46
+ };
47
+ const request = buildRequest(verifyTemplate, verifier.candidate.model.id);
48
+ const outcome = await executeOnce(provider, request);
49
+ input.budget?.recordCall(estCost);
50
+ if (!outcome.ok) {
51
+ return { verifierProviderId: verifier.candidate.providerId, verifierModel: verifier.candidate.model.id, agreement: 'inconclusive', reason: `verifier failed (${outcome.error.category})` };
52
+ }
53
+ let verdict;
54
+ try {
55
+ verdict = (outcome.response.json ?? extractJson(outcome.response.text ?? ''));
56
+ }
57
+ catch {
58
+ return { verifierProviderId: verifier.candidate.providerId, verifierModel: verifier.candidate.model.id, agreement: 'inconclusive', reason: 'verifier response was not parseable JSON' };
59
+ }
60
+ const agreement = verdict?.valid === true ? 'agree' : verdict?.valid === false ? 'disagree' : 'inconclusive';
61
+ return {
62
+ verifierProviderId: verifier.candidate.providerId,
63
+ verifierModel: verifier.candidate.model.id,
64
+ agreement,
65
+ ...(verdict?.reason ? { reason: verdict.reason } : {}),
66
+ };
67
+ }