theorum 1.0.0 → 1.1.3

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/README.md +81 -25
  2. package/esm/mod.d.ts +48 -29
  3. package/esm/mod.js +37 -24
  4. package/esm/src/cli/commands/bench.js +13 -16
  5. package/esm/src/cli/commands/fuzz-canary.js +25 -29
  6. package/esm/src/cli/commands/guardrails-eval.d.ts +14 -0
  7. package/esm/src/cli/commands/guardrails-eval.js +15 -0
  8. package/esm/src/cli/commands/profile.js +12 -3
  9. package/esm/src/cli/commands/run.js +8 -9
  10. package/esm/src/cli/commands/test.js +8 -8
  11. package/esm/src/cli/event-log.d.ts +1 -1
  12. package/esm/src/cli/index.js +12 -0
  13. package/esm/src/cli/matrix/synthesizer.d.ts +6 -6
  14. package/esm/src/cli/matrix/synthesizer.js +23 -22
  15. package/esm/src/guardrails/canary-gate.d.ts +2 -1
  16. package/esm/src/guardrails/canary-gate.js +2 -1
  17. package/esm/src/guardrails/canary.d.ts +9 -1
  18. package/esm/src/guardrails/canary.js +21 -10
  19. package/esm/src/guardrails/corpus/canary-egress-attacks.d.ts +1 -0
  20. package/esm/src/guardrails/corpus/fuzz-inbound.js +12 -15
  21. package/esm/src/guardrails/corpus/inbound-payloads.js +1 -0
  22. package/esm/src/guardrails/corpus/live-attacks.js +0 -1
  23. package/esm/src/guardrails/corpus/mod.d.ts +1 -0
  24. package/esm/src/guardrails/corpus/secrets.d.ts +1 -0
  25. package/esm/src/guardrails/corpus/secrets.js +1 -0
  26. package/esm/src/guardrails/corpus/strings.d.ts +1 -0
  27. package/esm/src/guardrails/corpus/strings.js +1 -0
  28. package/esm/src/guardrails/corpus/types.d.ts +1 -0
  29. package/esm/src/guardrails/egress.d.ts +26 -3
  30. package/esm/src/guardrails/egress.js +69 -18
  31. package/esm/src/guardrails/error.d.ts +14 -23
  32. package/esm/src/guardrails/error.js +86 -77
  33. package/esm/src/guardrails/eval/corpus.d.ts +108 -0
  34. package/esm/src/guardrails/eval/corpus.js +978 -0
  35. package/esm/src/guardrails/eval/mod.d.ts +51 -0
  36. package/esm/src/guardrails/eval/mod.js +133 -0
  37. package/esm/src/guardrails/eval/score.d.ts +66 -0
  38. package/esm/src/guardrails/eval/score.js +114 -0
  39. package/esm/src/guardrails/events.d.ts +25 -0
  40. package/esm/src/guardrails/events.js +56 -0
  41. package/esm/src/guardrails/hits.d.ts +24 -0
  42. package/esm/src/guardrails/hits.js +45 -0
  43. package/esm/src/guardrails/lexicon.d.ts +39 -0
  44. package/esm/src/guardrails/lexicon.js +200 -0
  45. package/esm/src/guardrails/live-outbound-gate.d.ts +15 -13
  46. package/esm/src/guardrails/live-outbound-gate.js +131 -89
  47. package/esm/src/guardrails/mod.d.ts +19 -4
  48. package/esm/src/guardrails/mod.js +12 -3
  49. package/esm/src/guardrails/network.d.ts +19 -0
  50. package/esm/src/guardrails/network.js +234 -0
  51. package/esm/src/guardrails/policy.d.ts +35 -0
  52. package/esm/src/guardrails/policy.js +50 -0
  53. package/esm/src/guardrails/progressive-yield.d.ts +51 -0
  54. package/esm/src/guardrails/progressive-yield.js +98 -0
  55. package/esm/src/guardrails/quota.d.ts +17 -3
  56. package/esm/src/guardrails/quota.js +18 -4
  57. package/esm/src/guardrails/sanitize.d.ts +36 -6
  58. package/esm/src/guardrails/sanitize.js +152 -60
  59. package/esm/src/guardrails/serialize.d.ts +35 -0
  60. package/esm/src/guardrails/serialize.js +58 -0
  61. package/esm/src/guardrails/testing.d.ts +7 -0
  62. package/esm/src/guardrails/testing.js +4 -0
  63. package/esm/src/guardrails/theorum-error.d.ts +12 -0
  64. package/esm/src/guardrails/theorum-error.js +15 -0
  65. package/esm/src/guardrails/tool-directives.d.ts +48 -0
  66. package/esm/src/guardrails/tool-directives.js +124 -0
  67. package/esm/src/guardrails/tool-result.d.ts +93 -0
  68. package/esm/src/guardrails/tool-result.js +276 -0
  69. package/esm/src/guardrails/types.d.ts +291 -0
  70. package/esm/src/guardrails/types.js +72 -0
  71. package/esm/src/host/client-turn.js +4 -0
  72. package/esm/src/host/mint-trace.d.ts +1 -1
  73. package/esm/src/kernel/auth/crypto.d.ts +42 -0
  74. package/esm/src/kernel/auth/crypto.js +106 -0
  75. package/esm/src/kernel/auth/mod.d.ts +11 -0
  76. package/esm/src/kernel/auth/mod.js +11 -0
  77. package/esm/src/kernel/auth/oauth.d.ts +47 -0
  78. package/esm/src/kernel/auth/oauth.js +278 -0
  79. package/esm/src/kernel/auth/types.d.ts +133 -0
  80. package/esm/src/kernel/auth/types.js +13 -0
  81. package/esm/src/kernel/engine/delta.js +43 -2
  82. package/esm/src/kernel/engine/live-inbound.d.ts +15 -3
  83. package/esm/src/kernel/engine/live-inbound.js +22 -8
  84. package/esm/src/kernel/engine/live-ingress.d.ts +19 -0
  85. package/esm/src/kernel/engine/live-ingress.js +47 -0
  86. package/esm/src/kernel/engine/repair.js +13 -12
  87. package/esm/src/kernel/engine/runner/gates.js +127 -40
  88. package/esm/src/kernel/engine/runner/mod.d.ts +6 -4
  89. package/esm/src/kernel/engine/runner/mod.js +182 -43
  90. package/esm/src/kernel/engine/runner/schema-validation.js +3 -3
  91. package/esm/src/kernel/engine/runner/stages.d.ts +39 -0
  92. package/esm/src/kernel/engine/runner/stages.js +89 -0
  93. package/esm/src/kernel/engine/runner/state.d.ts +24 -0
  94. package/esm/src/kernel/engine/runner/steps.js +128 -61
  95. package/esm/src/kernel/engine/runner/stream.d.ts +9 -2
  96. package/esm/src/kernel/engine/runner/stream.js +114 -55
  97. package/esm/src/kernel/engine/session/mod.d.ts +3 -1
  98. package/esm/src/kernel/engine/session/mod.js +386 -34
  99. package/esm/src/kernel/interaction-parts.d.ts +14 -0
  100. package/esm/src/kernel/interaction-parts.js +23 -0
  101. package/esm/src/kernel/mod.d.ts +19 -7
  102. package/esm/src/kernel/mod.js +10 -5
  103. package/esm/src/kernel/profile-graph.d.ts +159 -0
  104. package/esm/src/kernel/profile-graph.js +156 -0
  105. package/esm/src/kernel/registry/attachments.d.ts +10 -10
  106. package/esm/src/kernel/registry/attachments.js +29 -26
  107. package/esm/src/kernel/registry/catalog.d.ts +25 -11
  108. package/esm/src/kernel/registry/catalog.js +59 -24
  109. package/esm/src/kernel/registry/ingress.js +34 -22
  110. package/esm/src/kernel/registry/profile-outputs.d.ts +4 -0
  111. package/esm/src/kernel/registry/profile-outputs.js +8 -0
  112. package/esm/src/kernel/registry/profiles.d.ts +34 -14
  113. package/esm/src/kernel/registry/profiles.js +306 -59
  114. package/esm/src/kernel/registry/provider-request.js +2 -0
  115. package/esm/src/kernel/registry/resolve.d.ts +7 -6
  116. package/esm/src/kernel/registry/resolve.js +102 -117
  117. package/esm/src/kernel/registry/schemas.js +1 -1
  118. package/esm/src/kernel/registry/sole-model.d.ts +8 -0
  119. package/esm/src/kernel/registry/sole-model.js +10 -0
  120. package/esm/src/kernel/registry/system-prompt.d.ts +10 -0
  121. package/esm/src/kernel/registry/system-prompt.js +40 -0
  122. package/esm/src/kernel/registry/system-role.d.ts +8 -0
  123. package/esm/src/kernel/registry/system-role.js +14 -0
  124. package/esm/src/kernel/registry/vault.d.ts +2 -2
  125. package/esm/src/kernel/registry/vault.js +4 -4
  126. package/esm/src/kernel/schema.d.ts +97 -11
  127. package/esm/src/kernel/schema.js +256 -111
  128. package/esm/src/kernel/stages.d.ts +175 -0
  129. package/esm/src/kernel/stages.js +476 -0
  130. package/esm/src/kernel/stop.d.ts +65 -11
  131. package/esm/src/kernel/stop.js +46 -11
  132. package/esm/src/kernel/tools/events.d.ts +41 -0
  133. package/esm/src/kernel/tools/events.js +71 -0
  134. package/esm/src/kernel/tools/execute.d.ts +63 -24
  135. package/esm/src/kernel/tools/execute.js +475 -224
  136. package/esm/src/kernel/tools/harness.js +16 -14
  137. package/esm/src/kernel/tools/invoke.js +26 -8
  138. package/esm/src/kernel/tools/mod.d.ts +3 -1
  139. package/esm/src/kernel/tools/mod.js +2 -1
  140. package/esm/src/kernel/tools/permission.d.ts +15 -0
  141. package/esm/src/kernel/tools/permission.js +47 -0
  142. package/esm/src/kernel/tools/project.js +5 -3
  143. package/esm/src/kernel/tools/registry.js +20 -10
  144. package/esm/src/kernel/tools/remote.d.ts +94 -0
  145. package/esm/src/kernel/tools/remote.js +577 -0
  146. package/esm/src/kernel/tools/resolve.d.ts +17 -7
  147. package/esm/src/kernel/tools/resolve.js +38 -17
  148. package/esm/src/kernel/tools/schema.d.ts +2 -0
  149. package/esm/src/kernel/tools/schema.js +23 -6
  150. package/esm/src/kernel/tools/stage-run.d.ts +105 -0
  151. package/esm/src/kernel/tools/stage-run.js +155 -0
  152. package/esm/src/kernel/tools/types.d.ts +198 -20
  153. package/esm/src/kernel/types.d.ts +253 -114
  154. package/esm/src/kernel/util/find-last.d.ts +2 -0
  155. package/esm/src/kernel/util/find-last.js +10 -0
  156. package/esm/src/observability/destinations.d.ts +31 -0
  157. package/esm/src/observability/destinations.js +67 -0
  158. package/esm/src/observability/mod.d.ts +10 -3
  159. package/esm/src/observability/mod.js +6 -2
  160. package/esm/src/observability/policy.d.ts +27 -0
  161. package/esm/src/observability/policy.js +80 -0
  162. package/esm/src/observability/resolve-policy.d.ts +16 -0
  163. package/esm/src/observability/resolve-policy.js +64 -0
  164. package/esm/src/observability/trace-attach.d.ts +3 -1
  165. package/esm/src/observability/trace-attach.js +17 -10
  166. package/esm/src/observability/trace-record.d.ts +12 -4
  167. package/esm/src/observability/trace-record.js +63 -19
  168. package/esm/src/observability/trace-sink.d.ts +19 -0
  169. package/esm/src/observability/trace-sink.js +10 -0
  170. package/esm/src/observability/trace.d.ts +14 -11
  171. package/esm/src/observability/trace.js +23 -14
  172. package/esm/src/observability/types.d.ts +113 -0
  173. package/esm/src/observability/types.js +11 -0
  174. package/esm/src/providers/create-provider.d.ts +7 -4
  175. package/esm/src/providers/create-provider.js +21 -7
  176. package/esm/src/providers/google/interactions/framing.d.ts +8 -0
  177. package/esm/src/providers/google/interactions/framing.js +74 -13
  178. package/esm/src/providers/google/live/framing.d.ts +5 -2
  179. package/esm/src/providers/google/live/framing.js +49 -12
  180. package/esm/src/providers/google/live/openapi-schema.js +3 -2
  181. package/esm/src/providers/google/live/stream.d.ts +9 -0
  182. package/esm/src/providers/google/live/stream.js +16 -3
  183. package/esm/src/providers/openrouter/cache-control.d.ts +24 -0
  184. package/esm/src/providers/openrouter/cache-control.js +23 -0
  185. package/esm/src/providers/openrouter/chat.d.ts +21 -1
  186. package/esm/src/providers/openrouter/chat.js +51 -6
  187. package/esm/src/providers/openrouter/image.js +8 -19
  188. package/esm/src/providers/openrouter/openai/chat-payload.js +23 -1
  189. package/esm/src/providers/openrouter/openai/compat.d.ts +7 -1
  190. package/esm/src/providers/openrouter/openai/compat.js +23 -4
  191. package/esm/src/providers/openrouter/openai/image-payload.js +8 -2
  192. package/esm/src/providers/openrouter/openai/sdk-messages.d.ts +1 -1
  193. package/esm/src/providers/openrouter/openai/sdk-messages.js +32 -5
  194. package/package.json +1 -1
  195. package/esm/src/interface/blocks.d.ts +0 -20
  196. package/esm/src/interface/blocks.js +0 -180
  197. package/esm/src/interface/from-profile.d.ts +0 -14
  198. package/esm/src/interface/from-profile.js +0 -104
  199. package/esm/src/interface/inputs.d.ts +0 -8
  200. package/esm/src/interface/inputs.js +0 -127
  201. package/esm/src/interface/mod.d.ts +0 -10
  202. package/esm/src/interface/mod.js +0 -9
  203. package/esm/src/interface/types.d.ts +0 -153
  204. package/esm/src/interface/types.js +0 -13
@@ -7,7 +7,7 @@
7
7
  * @module
8
8
  */
9
9
  import { TheorumError } from '../../../guardrails/error.js';
10
- import { buildGeminiLiveClientContent, buildGeminiLiveRealtimeInput, buildGeminiLiveSetupMessage, foldGeminiLiveServerMessage, parseGeminiLiveMessage, } from './framing.js';
10
+ import { buildGeminiLiveClientContent, buildGeminiLiveRealtimeInput, buildGeminiLiveSetupMessage, foldGeminiLiveServerMessage, parseGeminiLiveMessage, readLiveInteractionStatus, } from './framing.js';
11
11
  const SETUP_TIMEOUT_MS = 20_000;
12
12
  export async function readMessageData(data) {
13
13
  if (typeof data === 'string')
@@ -143,12 +143,25 @@ export function createLiveQueue() {
143
143
  },
144
144
  };
145
145
  }
146
- function turnPhaseFromMessage(message, events) {
146
+ /**
147
+ * Conversational cycle boundary.
148
+ *
149
+ * When the server reports `interactionStatus`, that is authoritative: `IDLE`
150
+ * closes the cycle and `IN_PROGRESS` keeps it open even across `turnComplete`
151
+ * (background reasoning / async tools may still produce output). Without the
152
+ * field, `turnComplete` is the boundary as before.
153
+ */
154
+ export function turnPhaseFromMessage(message, events) {
147
155
  const interrupted = events.some((ev) => ev.type === 'done' && ev.interrupted === true);
148
156
  if (interrupted)
149
157
  return 'abort';
158
+ const status = readLiveInteractionStatus(message);
159
+ if (status === 'IDLE')
160
+ return 'complete';
161
+ if (status === 'IN_PROGRESS')
162
+ return 'streaming';
150
163
  const serverContent = message.serverContent;
151
- if (serverContent?.turnComplete)
164
+ if (serverContent?.turnComplete || serverContent?.turn_complete)
152
165
  return 'complete';
153
166
  return 'streaming';
154
167
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Shared OpenRouter `cache_control` directive from a profile CacheSpec.
3
+ *
4
+ * Used by AI SDK `providerOptionsFor` and REST `toOpenAiChatPayload` so both
5
+ * paths share one wire shape.
6
+ *
7
+ * @module
8
+ */
9
+ import type { CacheTtl } from '../../kernel/schema.js';
10
+ import type { CacheSpec } from '../../kernel/types.js';
11
+ /** Anthropic / OpenRouter ephemeral cache_control object. */
12
+ export interface CacheControlDirective {
13
+ type: 'ephemeral';
14
+ ttl?: CacheTtl;
15
+ }
16
+ /** Map a profile CacheSpec to the upstream cache_control body fragment. */
17
+ export declare function cacheControlFromSpec(spec: CacheSpec): CacheControlDirective;
18
+ /**
19
+ * Plain JSON object for AI SDK `providerOptions` (needs string index signature).
20
+ * Same values as `cacheControlFromSpec`.
21
+ */
22
+ export declare function cacheControlJson(spec: CacheSpec): {
23
+ [key: string]: string;
24
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared OpenRouter `cache_control` directive from a profile CacheSpec.
3
+ *
4
+ * Used by AI SDK `providerOptionsFor` and REST `toOpenAiChatPayload` so both
5
+ * paths share one wire shape.
6
+ *
7
+ * @module
8
+ */
9
+ /** Map a profile CacheSpec to the upstream cache_control body fragment. */
10
+ export function cacheControlFromSpec(spec) {
11
+ if (spec.ttl) {
12
+ return { type: 'ephemeral', ttl: spec.ttl };
13
+ }
14
+ return { type: 'ephemeral' };
15
+ }
16
+ /**
17
+ * Plain JSON object for AI SDK `providerOptions` (needs string index signature).
18
+ * Same values as `cacheControlFromSpec`.
19
+ */
20
+ export function cacheControlJson(spec) {
21
+ const directive = cacheControlFromSpec(spec);
22
+ return directive.ttl ? { type: directive.type, ttl: directive.ttl } : { type: directive.type };
23
+ }
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * @module
9
9
  */
10
- import { type TextStreamPart, type ToolSet } from 'ai';
10
+ import { type ModelMessage, type TextStreamPart, type ToolSet } from 'ai';
11
11
  import type { ModelProvider, ProviderCompleteRequest, TurnEvent, TurnTokens, WireFunctionTool } from '../../kernel/types.js';
12
12
  import type { OpenAiGatewayConfig } from '../types.js';
13
13
  export interface StreamAccumulator {
@@ -37,6 +37,11 @@ export declare function tokensFromUsage(usage: {
37
37
  inputTokens?: number | null;
38
38
  outputTokens?: number | null;
39
39
  totalTokens?: number | null;
40
+ inputTokenDetails?: {
41
+ cacheReadTokens?: number | null;
42
+ cacheWriteTokens?: number | null;
43
+ } | null;
44
+ cachedInputTokens?: number | null;
40
45
  }): TurnTokens | undefined;
41
46
  export declare function rawRecord(value: unknown): Record<string, unknown> | undefined;
42
47
  export declare function stringArray(value: unknown): string[] | undefined;
@@ -66,6 +71,11 @@ export declare function tokenEvent(part: {
66
71
  inputTokens?: number | null;
67
72
  outputTokens?: number | null;
68
73
  totalTokens?: number | null;
74
+ inputTokenDetails?: {
75
+ cacheReadTokens?: number | null;
76
+ cacheWriteTokens?: number | null;
77
+ } | null;
78
+ cachedInputTokens?: number | null;
69
79
  };
70
80
  }): TurnEvent | undefined;
71
81
  export declare function providerMetadataEvent(part: TextStreamPart<ToolSet>, acc: StreamAccumulator): TurnEvent | undefined;
@@ -77,9 +87,19 @@ export declare function finishEvent(part: {
77
87
  inputTokens?: number | null;
78
88
  outputTokens?: number | null;
79
89
  totalTokens?: number | null;
90
+ inputTokenDetails?: {
91
+ cacheReadTokens?: number | null;
92
+ cacheWriteTokens?: number | null;
93
+ } | null;
94
+ cachedInputTokens?: number | null;
80
95
  };
81
96
  }, acc: StreamAccumulator): TurnEvent | undefined;
82
97
  export declare function finalEvents(req: ProviderCompleteRequest, acc: StreamAccumulator): Generator<TurnEvent>;
98
+ /** System via instructions XOR a cache-marked system message — never both. */
99
+ export declare function systemDelivery(req: ProviderCompleteRequest): {
100
+ instructions?: string;
101
+ systemMessage?: ModelMessage;
102
+ };
83
103
  export declare function missingOpenRouterKey(): TurnEvent;
84
104
  export declare function providerOptionsFor(req: ProviderCompleteRequest): ProviderOptions | undefined;
85
105
  /** Create a `ModelProvider` backed by OpenRouter through AI SDK Core. */
@@ -8,10 +8,11 @@
8
8
  * @module
9
9
  */
10
10
  import { createOpenRouter } from '@openrouter/ai-sdk-provider';
11
- import { jsonSchema, streamText, tool } from 'ai';
11
+ import { jsonSchema, streamText, tool, } from 'ai';
12
12
  import { isAbortError, TheorumError, toErrorEvent } from '../../guardrails/error.js';
13
- import { parseStructuredOutput } from '../../kernel/engine/delta.js';
13
+ import { extractUsageTokens, parseStructuredOutput } from '../../kernel/engine/delta.js';
14
14
  import { turnStopFromOpenAiFinishReason } from '../../kernel/stop.js';
15
+ import { cacheControlJson } from './cache-control.js';
15
16
  import { resolveOpenRouterPlugins } from './openai/chat-payload.js';
16
17
  import { openAiGatewayHeaders, resolveResponseFormat } from './openai/compat.js';
17
18
  import { buildAiSdkMessages } from './openai/sdk-messages.js';
@@ -80,13 +81,21 @@ export function tokensFromUsage(usage) {
80
81
  const input = usage.inputTokens ?? 0;
81
82
  const output = usage.outputTokens ?? 0;
82
83
  const total = usage.totalTokens ?? input + output;
83
- if (input === 0 && output === 0 && total === 0) {
84
+ const cached = usage.inputTokenDetails?.cacheReadTokens ?? usage.cachedInputTokens ?? undefined;
85
+ const cacheWrite = usage.inputTokenDetails?.cacheWriteTokens ?? undefined;
86
+ if (input === 0 &&
87
+ output === 0 &&
88
+ total === 0 &&
89
+ !(cached && cached > 0) &&
90
+ !(cacheWrite && cacheWrite > 0)) {
84
91
  return undefined;
85
92
  }
86
93
  return {
87
94
  input,
88
95
  output,
89
96
  total,
97
+ ...(cached && cached > 0 ? { cached } : {}),
98
+ ...(cacheWrite && cacheWrite > 0 ? { cacheWrite } : {}),
90
99
  };
91
100
  }
92
101
  export function rawRecord(value) {
@@ -211,6 +220,13 @@ export function rawEvents(raw, acc) {
211
220
  if (messageEvidence) {
212
221
  events.push(messageEvidence);
213
222
  }
223
+ if (!acc.emittedTokens) {
224
+ const usage = extractUsageTokens(record.usage);
225
+ if (usage) {
226
+ acc.emittedTokens = true;
227
+ events.push({ type: 'tokens', tokens: usage });
228
+ }
229
+ }
214
230
  const choices = Array.isArray(record.choices) ? record.choices : [];
215
231
  for (const choice of choices) {
216
232
  const row = rawRecord(choice);
@@ -338,11 +354,34 @@ function createStreamContext(req, config, apiKey) {
338
354
  modelName: req.apiId,
339
355
  };
340
356
  }
357
+ /** System via instructions XOR a cache-marked system message — never both. */
358
+ export function systemDelivery(req) {
359
+ if (!req.system) {
360
+ return {};
361
+ }
362
+ if (req.cache?.mode === 'system') {
363
+ return {
364
+ systemMessage: {
365
+ role: 'system',
366
+ content: req.system,
367
+ providerOptions: {
368
+ openrouter: { cacheControl: cacheControlJson(req.cache) },
369
+ },
370
+ },
371
+ };
372
+ }
373
+ return { instructions: req.system };
374
+ }
341
375
  function streamTextOptions(req, context) {
376
+ const delivery = systemDelivery(req);
377
+ const messages = buildAiSdkMessages(req);
378
+ if (delivery.systemMessage) {
379
+ messages.unshift(delivery.systemMessage);
380
+ }
342
381
  return {
343
382
  model: context.openrouter.chat(context.modelName, openRouterSettings(req)),
344
- instructions: req.system,
345
- messages: buildAiSdkMessages(req),
383
+ instructions: delivery.instructions,
384
+ messages,
346
385
  allowSystemInMessages: true,
347
386
  temperature: req.temperature,
348
387
  maxOutputTokens: req.maxOutputTokens,
@@ -395,13 +434,19 @@ async function* streamOpenRouter(req, config) {
395
434
  }
396
435
  export function providerOptionsFor(req) {
397
436
  const openrouter = {};
398
- if (req.thinking !== 'none') {
437
+ if (req.thinking && req.thinking !== 'none') {
399
438
  openrouter.reasoning = { effort: req.thinking };
400
439
  }
401
440
  const responseFormat = resolveResponseFormat(req.structured);
402
441
  if (responseFormat) {
403
442
  openrouter.response_format = responseFormat;
404
443
  }
444
+ if (req.cache?.mode === 'automatic') {
445
+ openrouter.cacheControl = cacheControlJson(req.cache);
446
+ }
447
+ if (req.sessionId) {
448
+ openrouter.session_id = req.sessionId;
449
+ }
405
450
  if (Object.keys(openrouter).length === 0)
406
451
  return undefined;
407
452
  return { openrouter };
@@ -11,6 +11,7 @@
11
11
  * @module
12
12
  */
13
13
  import { toErrorEvent } from '../../guardrails/error.js';
14
+ import { extractUsageTokens } from '../../kernel/engine/delta.js';
14
15
  import { bytesToBase64 } from '../shared/pcm.js';
15
16
  import { buildChatMessages, openAiGatewayHeaders } from './openai/compat.js';
16
17
  import { buildImagesPayload, extractPromptText, imageToolParameters, } from './openai/image-payload.js';
@@ -32,24 +33,12 @@ export function buildImageHeaders(apiKey, config) {
32
33
  function baseUrl(config) {
33
34
  return config.baseUrl?.replace(/\/+$/, '') ?? 'https://openrouter.ai/api/v1';
34
35
  }
35
- function usageFromRecord(raw) {
36
- if (!raw || typeof raw !== 'object') {
37
- return null;
38
- }
39
- const usage = raw;
40
- const input = typeof usage.prompt_tokens === 'number' ? usage.prompt_tokens : 0;
41
- const output = typeof usage.completion_tokens === 'number' ? usage.completion_tokens : 0;
42
- const total = typeof usage.total_tokens === 'number' ? usage.total_tokens : input + output;
43
- if (input + output + total === 0) {
44
- return null;
45
- }
46
- return { input, output, total };
47
- }
48
- function* yieldUsage(usage) {
49
- if (!usage) {
36
+ function* yieldUsage(raw) {
37
+ const tokens = extractUsageTokens(raw);
38
+ if (!tokens) {
50
39
  return;
51
40
  }
52
- yield { type: 'tokens', tokens: usage };
41
+ yield { type: 'tokens', tokens };
53
42
  }
54
43
  export function mediaFromImagesResponse(body, fallbackMime) {
55
44
  const data = body.data;
@@ -176,7 +165,7 @@ export function buildInterleavedChatPayload(req) {
176
165
  messages: buildChatMessages(req),
177
166
  temperature: req.temperature,
178
167
  max_tokens: req.maxOutputTokens,
179
- reasoning: { effort: req.thinking },
168
+ ...(req.thinking && req.thinking !== 'none' ? { reasoning: { effort: req.thinking } } : {}),
180
169
  tools: [
181
170
  {
182
171
  type: OPENROUTER_IMAGE_TOOL,
@@ -234,7 +223,7 @@ export async function* yieldInterleavedChat(req, config, apiKey) {
234
223
  yield toErrorEvent('no image returned from chat image generation');
235
224
  return;
236
225
  }
237
- yield* yieldUsage(usageFromRecord(body.usage));
226
+ yield* yieldUsage(body.usage);
238
227
  yield { type: 'done' };
239
228
  }
240
229
  export async function* yieldImagesEndpoint(req, config, apiKey) {
@@ -251,7 +240,7 @@ export async function* yieldImagesEndpoint(req, config, apiKey) {
251
240
  return;
252
241
  }
253
242
  yield { type: 'media', media };
254
- yield* yieldUsage(usageFromRecord(body.usage));
243
+ yield* yieldUsage(body.usage);
255
244
  yield { type: 'done' };
256
245
  }
257
246
  export async function* streamImage(req, config = {}) {
@@ -11,10 +11,24 @@
11
11
  * @module
12
12
  */
13
13
  import { getTool } from '../../../kernel/tools/registry.js';
14
+ import { cacheControlFromSpec } from '../cache-control.js';
14
15
  import { buildChatMessages, resolveResponseFormat, wireTools } from './compat.js';
15
16
  /** Convert a provider-neutral request into an OpenAI chat completion payload. */
16
17
  function toOpenAiChatPayload(req) {
17
18
  const messages = buildChatMessages(req);
19
+ if (req.cache?.mode === 'system' && req.system) {
20
+ const directive = cacheControlFromSpec(req.cache);
21
+ const systemIdx = messages.findIndex((m) => m.role === 'system');
22
+ if (systemIdx >= 0) {
23
+ const text = typeof messages[systemIdx].content === 'string'
24
+ ? messages[systemIdx].content
25
+ : '';
26
+ messages[systemIdx] = {
27
+ role: 'system',
28
+ content: [{ type: 'text', text, cache_control: directive }],
29
+ };
30
+ }
31
+ }
18
32
  const payload = {
19
33
  model: req.apiId,
20
34
  stream: true,
@@ -22,7 +36,15 @@ function toOpenAiChatPayload(req) {
22
36
  temperature: req.temperature,
23
37
  max_tokens: req.maxOutputTokens,
24
38
  };
25
- payload.reasoning = { effort: req.thinking };
39
+ if (req.thinking && req.thinking !== 'none') {
40
+ payload.reasoning = { effort: req.thinking };
41
+ }
42
+ if (req.cache?.mode === 'automatic') {
43
+ payload.cache_control = cacheControlFromSpec(req.cache);
44
+ }
45
+ if (req.sessionId) {
46
+ payload.session_id = req.sessionId;
47
+ }
26
48
  const responseFormat = resolveResponseFormat(req.structured);
27
49
  if (responseFormat) {
28
50
  payload.response_format = responseFormat;
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * @module
11
11
  */
12
- import type { ProviderCompleteRequest, StructuredSchemaId, WireFunctionTool } from '../../../kernel/types.js';
12
+ import type { InteractionPart, ProviderCompleteRequest, StructuredSchemaId, WireFunctionTool } from '../../../kernel/types.js';
13
13
  /** Subset of provider config used for OpenAI-gateway HTTP headers. */
14
14
  interface GatewayHeaderConfig {
15
15
  siteUrl?: string;
@@ -18,6 +18,12 @@ interface GatewayHeaderConfig {
18
18
  declare function stringDefault(value: string | undefined, fallback: string): string;
19
19
  declare function fallbackToolCallId(name?: string): string;
20
20
  declare function parseToolInput(raw: string): Record<string, unknown>;
21
+ /**
22
+ * Map InteractionPart[] to OpenAI-compat message content.
23
+ * Text-only inputs are joined as a plain string; mixed inputs produce a
24
+ * content-part array (text, image_url, input_audio, file).
25
+ */
26
+ export declare function wireMessageContent(parts: InteractionPart[]): unknown;
21
27
  /**
22
28
  * Build the complete `messages` array for an OpenAI chat completion request.
23
29
  * Assembles: system → history → user input.
@@ -10,6 +10,7 @@
10
10
  * @module
11
11
  */
12
12
  import { TheorumError } from '../../../guardrails/error.js';
13
+ import { isMediaRefPart } from '../../../kernel/interaction-parts.js';
13
14
  import { getStructured } from '../../../kernel/registry/schemas.js';
14
15
  import { parseToolArgumentsObject } from '../../shared/tool-args.js';
15
16
  // ── shared tool helpers ─────────────────────────────
@@ -27,6 +28,11 @@ function parseToolInput(raw) {
27
28
  return parsed.value;
28
29
  }
29
30
  // ── content wire format ─────────────────────────────
31
+ function rejectMediaRef(part) {
32
+ if (isMediaRefPart(part)) {
33
+ throw new TheorumError('media references are not supported on openAi');
34
+ }
35
+ }
30
36
  function wireAudioPart(part) {
31
37
  let format = 'mp3';
32
38
  if (part.mimeType.includes('wav')) {
@@ -43,9 +49,9 @@ function wireAudioPart(part) {
43
49
  /**
44
50
  * Map InteractionPart[] to OpenAI-compat message content.
45
51
  * Text-only inputs are joined as a plain string; mixed inputs produce a
46
- * content-part array (text, image_url, input_audio).
52
+ * content-part array (text, image_url, input_audio, file).
47
53
  */
48
- function wireMessageContent(parts) {
54
+ export function wireMessageContent(parts) {
49
55
  const isAllText = parts.every((p) => p.type === 'text');
50
56
  if (isAllText) {
51
57
  return parts
@@ -62,6 +68,7 @@ function wireMessageContent(parts) {
62
68
  if (part.type === 'text') {
63
69
  return { type: 'text', text: part.text };
64
70
  }
71
+ rejectMediaRef(part);
65
72
  if (part.type === 'image') {
66
73
  return {
67
74
  type: 'image_url',
@@ -71,7 +78,14 @@ function wireMessageContent(parts) {
71
78
  if (part.type === 'audio') {
72
79
  return wireAudioPart(part);
73
80
  }
74
- return { type: 'text', text: '' };
81
+ // video + document file part (OpenAI-compat / OpenRouter)
82
+ return {
83
+ type: 'file',
84
+ file: {
85
+ filename: part.type === 'video' ? 'clip.bin' : 'document.bin',
86
+ file_data: `data:${part.mimeType};base64,${part.data}`,
87
+ },
88
+ };
75
89
  });
76
90
  }
77
91
  // ── history message wire format ─────────────────────
@@ -82,11 +96,16 @@ function wireMessageContent(parts) {
82
96
  */
83
97
  function wireHistoryMessage(msg) {
84
98
  if (msg.role === 'tool') {
99
+ let content = msg.content ?? '';
100
+ if (msg.parts && msg.parts.length > 0) {
101
+ // Prefer multimodal parts; if only text parts, wireMessageContent collapses to string.
102
+ content = wireMessageContent(msg.parts);
103
+ }
85
104
  return {
86
105
  role: 'tool',
87
106
  tool_call_id: msg.tool_call_id ?? fallbackToolCallId(msg.name),
88
107
  name: msg.name,
89
- content: msg.content ?? '',
108
+ content,
90
109
  };
91
110
  }
92
111
  let content = msg.content ?? '';
@@ -6,6 +6,8 @@
6
6
  *
7
7
  * @module
8
8
  */
9
+ import { TheorumError } from '../../../guardrails/error.js';
10
+ import { isMediaRefPart } from '../../../kernel/interaction-parts.js';
9
11
  export function extractPromptText(input) {
10
12
  return input
11
13
  .filter((part) => part.type === 'text')
@@ -32,9 +34,13 @@ export function wireInputReference(part) {
32
34
  export function wireInputReferences(input) {
33
35
  const references = [];
34
36
  for (const part of input) {
35
- if (part.type === 'image') {
36
- references.push(wireInputReference(part));
37
+ if (part.type !== 'image') {
38
+ continue;
37
39
  }
40
+ if (isMediaRefPart(part)) {
41
+ throw new TheorumError('media references are not supported on openAi');
42
+ }
43
+ references.push(wireInputReference(part));
38
44
  }
39
45
  return references;
40
46
  }
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import type { ModelMessage } from 'ai';
10
10
  import type { InteractionPart, ProviderCompleteRequest, TurnHistoryMessage } from '../../../kernel/types.js';
11
- export declare function sdkPart(part: InteractionPart): Record<string, unknown>;
11
+ export declare function sdkPart(input: InteractionPart): Record<string, unknown>;
12
12
  export declare function sdkContentFromParts(parts: InteractionPart[]): string | Array<Record<string, unknown>>;
13
13
  export declare function sdkContentFromOptionalParts(parts: InteractionPart[] | undefined, text: string | undefined): string | Array<Record<string, unknown>>;
14
14
  export declare function toolResultMessage(msg: TurnHistoryMessage): ModelMessage;
@@ -6,14 +6,23 @@
6
6
  *
7
7
  * @module
8
8
  */
9
+ import { TheorumError } from '../../../guardrails/error.js';
10
+ import { isMediaRefPart } from '../../../kernel/interaction-parts.js';
9
11
  import { fallbackToolCallId, parseToolInput } from './compat.js';
12
+ function inlineMediaPart(part) {
13
+ if (isMediaRefPart(part)) {
14
+ throw new TheorumError('media references are not supported on openAi');
15
+ }
16
+ return part;
17
+ }
10
18
  function stringDefault(value, fallback) {
11
19
  return value === undefined ? fallback : value;
12
20
  }
13
- export function sdkPart(part) {
14
- if (part.type === 'text') {
15
- return { type: 'text', text: part.text };
21
+ export function sdkPart(input) {
22
+ if (input.type === 'text') {
23
+ return { type: 'text', text: input.text };
16
24
  }
25
+ const part = inlineMediaPart(input);
17
26
  if (part.type === 'image') {
18
27
  return {
19
28
  type: 'image',
@@ -39,15 +48,33 @@ export function sdkContentFromOptionalParts(parts, text) {
39
48
  }
40
49
  export function toolResultMessage(msg) {
41
50
  const toolName = stringDefault(msg.name, 'tool');
51
+ const toolCallId = stringDefault(msg.tool_call_id, fallbackToolCallId(msg.name));
52
+ const output = msg.parts && msg.parts.length > 0
53
+ ? {
54
+ type: 'content',
55
+ value: msg.parts.map((input) => {
56
+ if (input.type === 'text') {
57
+ return { type: 'text', text: input.text };
58
+ }
59
+ // image / audio / video / document — AI SDK tool-result file parts
60
+ const part = inlineMediaPart(input);
61
+ return {
62
+ type: 'file',
63
+ mediaType: part.mimeType,
64
+ data: { type: 'data', data: part.data },
65
+ };
66
+ }),
67
+ }
68
+ : { type: 'text', value: stringDefault(msg.content, '') };
42
69
  // AI SDK ToolModelMessage is a branded union; structural tool-result is correct at runtime.
43
70
  return {
44
71
  role: 'tool',
45
72
  content: [
46
73
  {
47
74
  type: 'tool-result',
48
- toolCallId: stringDefault(msg.tool_call_id, fallbackToolCallId(msg.name)),
75
+ toolCallId,
49
76
  toolName,
50
- output: { type: 'text', value: stringDefault(msg.content, '') },
77
+ output,
51
78
  },
52
79
  ],
53
80
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "theorum",
3
- "version": "1.0.0",
3
+ "version": "1.1.3",
4
4
  "description": "A flat TypeScript agent kernel for typed profiles, deterministic turn execution, registered tools, provider adapters, guardrails, and host-injected traces.",
5
5
  "keywords": [
6
6
  "agent",
@@ -1,20 +0,0 @@
1
- /**
2
- * Turn event folding — map kernel `TurnEvent` streams to transcript blocks.
3
- *
4
- * @module
5
- */
6
- import type { TurnEvent } from '../kernel/types.js';
7
- import type { FoldTurnEventsOptions, TranscriptBlock, UserTurnDraft } from './types.js';
8
- declare function resetBlockIds(): void;
9
- /** Build transcript blocks for a user-authored turn. */
10
- declare function buildUserTurnBlocks(draft: UserTurnDraft, idPrefix?: string): TranscriptBlock[];
11
- /**
12
- * Fold a single assistant turn's `TurnEvent` stream into ordered transcript blocks.
13
- *
14
- * Merges consecutive `text` and `thought` deltas, upserts tool calls by id, and
15
- * skips kernel bookkeeping events (`tokens`, `session`) unless folded into `turn-done`.
16
- */
17
- declare function foldTurnEvents(events: readonly TurnEvent[], options?: FoldTurnEventsOptions): TranscriptBlock[];
18
- /** User draft blocks followed by folded assistant turn events. */
19
- declare function foldConversationTurn(draft: UserTurnDraft, assistantEvents: readonly TurnEvent[], options?: FoldTurnEventsOptions): TranscriptBlock[];
20
- export { buildUserTurnBlocks, foldConversationTurn, foldTurnEvents, resetBlockIds };