theorum 0.1.3 → 0.1.4

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 (187) hide show
  1. package/README.md +30 -9
  2. package/deno.json +57 -0
  3. package/deno.lock +469 -0
  4. package/{esm/mod.d.ts → mod.ts} +57 -15
  5. package/package.json +35 -44
  6. package/src/cli/commands/profile.ts +57 -0
  7. package/src/cli/commands/run.ts +66 -0
  8. package/src/cli/commands/test.ts +195 -0
  9. package/src/cli/index.ts +161 -0
  10. package/src/cli/matrix/fixtures.ts +96 -0
  11. package/src/cli/matrix/synthesizer.ts +240 -0
  12. package/src/guardrails/error.ts +142 -0
  13. package/src/guardrails/injection.ts +254 -0
  14. package/src/guardrails/mod.ts +40 -0
  15. package/src/guardrails/quota.ts +87 -0
  16. package/src/guardrails/sanitize.ts +165 -0
  17. package/src/guardrails/sensitive.ts +96 -0
  18. package/src/host/mint-trace.ts +54 -0
  19. package/{esm/src/host/mod.js → src/host/mod.ts} +11 -3
  20. package/src/host/reply.ts +32 -0
  21. package/src/kernel/engine/assert.ts +29 -0
  22. package/src/kernel/engine/boundary.ts +75 -0
  23. package/src/kernel/engine/delta.ts +440 -0
  24. package/src/kernel/engine/hash.ts +11 -0
  25. package/src/kernel/engine/record.ts +8 -0
  26. package/src/kernel/engine/repair.ts +56 -0
  27. package/src/kernel/engine/runner/gates.ts +338 -0
  28. package/src/kernel/engine/runner/mod.ts +135 -0
  29. package/src/kernel/engine/runner/state.ts +31 -0
  30. package/src/kernel/engine/runner/steps.ts +183 -0
  31. package/src/kernel/engine/runner/stream.ts +83 -0
  32. package/src/kernel/engine/runner/tokens.ts +47 -0
  33. package/src/kernel/engine/runner/tools.ts +266 -0
  34. package/{esm/src/kernel/engine/runner.d.ts → src/kernel/engine/runner.ts} +2 -1
  35. package/src/kernel/engine/tree.ts +18 -0
  36. package/src/kernel/mod.ts +39 -0
  37. package/src/kernel/registry/attachments.ts +195 -0
  38. package/src/kernel/registry/catalog.ts +181 -0
  39. package/src/kernel/registry/ingress.ts +223 -0
  40. package/src/kernel/registry/profiles.ts +165 -0
  41. package/src/kernel/registry/provider-request.ts +31 -0
  42. package/src/kernel/registry/resolve.ts +290 -0
  43. package/src/kernel/registry/schemas.ts +29 -0
  44. package/src/kernel/registry/tools.ts +45 -0
  45. package/{esm/src/kernel/registry/vault.d.ts → src/kernel/registry/vault.ts} +17 -2
  46. package/src/kernel/types.ts +576 -0
  47. package/{esm/src/observability/mod.js → src/observability/mod.ts} +11 -2
  48. package/src/observability/spans.ts +70 -0
  49. package/src/observability/trace-attach.ts +96 -0
  50. package/src/observability/trace-record.ts +254 -0
  51. package/src/observability/trace-usage.ts +36 -0
  52. package/src/observability/trace.ts +143 -0
  53. package/src/presets/google.ts +147 -0
  54. package/src/presets/mod.ts +24 -0
  55. package/src/providers/create-provider.ts +64 -0
  56. package/src/providers/gemini-tape.ts +59 -0
  57. package/src/providers/google-tap.ts +57 -0
  58. package/src/providers/interactions.ts +182 -0
  59. package/src/providers/keys.ts +182 -0
  60. package/{esm/src/providers/mod.js → src/providers/mod.ts} +4 -2
  61. package/{esm/src/providers/openrouter-mod.js → src/providers/openrouter-mod.ts} +7 -3
  62. package/src/providers/openrouter-payload.ts +232 -0
  63. package/src/providers/openrouter.ts +758 -0
  64. package/src/providers/pcm.ts +37 -0
  65. package/src/providers/provider.ts +206 -0
  66. package/src/providers/speech.ts +180 -0
  67. package/src/providers/sse.ts +59 -0
  68. package/esm/_dnt.polyfills.d.ts +0 -110
  69. package/esm/_dnt.polyfills.js +0 -144
  70. package/esm/_dnt.shims.d.ts +0 -5
  71. package/esm/_dnt.shims.js +0 -61
  72. package/esm/mod.js +0 -49
  73. package/esm/package.json +0 -3
  74. package/esm/src/cli/commands/profile.d.ts +0 -2
  75. package/esm/src/cli/commands/profile.js +0 -53
  76. package/esm/src/cli/commands/run.d.ts +0 -10
  77. package/esm/src/cli/commands/run.js +0 -55
  78. package/esm/src/cli/commands/test.d.ts +0 -15
  79. package/esm/src/cli/commands/test.js +0 -140
  80. package/esm/src/cli/index.d.ts +0 -4
  81. package/esm/src/cli/index.js +0 -148
  82. package/esm/src/cli/matrix/fixtures.d.ts +0 -13
  83. package/esm/src/cli/matrix/fixtures.js +0 -77
  84. package/esm/src/cli/matrix/synthesizer.d.ts +0 -29
  85. package/esm/src/cli/matrix/synthesizer.js +0 -187
  86. package/esm/src/guardrails/error.d.ts +0 -33
  87. package/esm/src/guardrails/error.js +0 -114
  88. package/esm/src/guardrails/injection.d.ts +0 -12
  89. package/esm/src/guardrails/injection.js +0 -220
  90. package/esm/src/guardrails/mod.d.ts +0 -14
  91. package/esm/src/guardrails/mod.js +0 -14
  92. package/esm/src/guardrails/sanitize.d.ts +0 -22
  93. package/esm/src/guardrails/sanitize.js +0 -133
  94. package/esm/src/guardrails/sensitive.d.ts +0 -12
  95. package/esm/src/guardrails/sensitive.js +0 -88
  96. package/esm/src/host/mint-trace.d.ts +0 -27
  97. package/esm/src/host/mint-trace.js +0 -33
  98. package/esm/src/host/mod.d.ts +0 -13
  99. package/esm/src/host/reply.d.ts +0 -14
  100. package/esm/src/host/reply.js +0 -27
  101. package/esm/src/kernel/engine/boundary.d.ts +0 -10
  102. package/esm/src/kernel/engine/boundary.js +0 -55
  103. package/esm/src/kernel/engine/delta.d.ts +0 -8
  104. package/esm/src/kernel/engine/delta.js +0 -389
  105. package/esm/src/kernel/engine/hash.d.ts +0 -1
  106. package/esm/src/kernel/engine/hash.js +0 -9
  107. package/esm/src/kernel/engine/record.d.ts +0 -2
  108. package/esm/src/kernel/engine/record.js +0 -7
  109. package/esm/src/kernel/engine/repair.d.ts +0 -7
  110. package/esm/src/kernel/engine/repair.js +0 -38
  111. package/esm/src/kernel/engine/runner/gates.d.ts +0 -4
  112. package/esm/src/kernel/engine/runner/gates.js +0 -206
  113. package/esm/src/kernel/engine/runner/mod.d.ts +0 -14
  114. package/esm/src/kernel/engine/runner/mod.js +0 -103
  115. package/esm/src/kernel/engine/runner/state.d.ts +0 -16
  116. package/esm/src/kernel/engine/runner/state.js +0 -8
  117. package/esm/src/kernel/engine/runner/steps.d.ts +0 -15
  118. package/esm/src/kernel/engine/runner/steps.js +0 -119
  119. package/esm/src/kernel/engine/runner/stream.d.ts +0 -11
  120. package/esm/src/kernel/engine/runner/stream.js +0 -65
  121. package/esm/src/kernel/engine/runner/tokens.d.ts +0 -3
  122. package/esm/src/kernel/engine/runner/tokens.js +0 -38
  123. package/esm/src/kernel/engine/runner/tools.d.ts +0 -13
  124. package/esm/src/kernel/engine/runner/tools.js +0 -197
  125. package/esm/src/kernel/engine/runner.js +0 -6
  126. package/esm/src/kernel/engine/tree.d.ts +0 -2
  127. package/esm/src/kernel/engine/tree.js +0 -17
  128. package/esm/src/kernel/mod.d.ts +0 -18
  129. package/esm/src/kernel/mod.js +0 -16
  130. package/esm/src/kernel/registry/attachments.d.ts +0 -16
  131. package/esm/src/kernel/registry/attachments.js +0 -156
  132. package/esm/src/kernel/registry/catalog.d.ts +0 -31
  133. package/esm/src/kernel/registry/catalog.js +0 -135
  134. package/esm/src/kernel/registry/ingress.d.ts +0 -13
  135. package/esm/src/kernel/registry/ingress.js +0 -163
  136. package/esm/src/kernel/registry/profiles.d.ts +0 -34
  137. package/esm/src/kernel/registry/profiles.js +0 -120
  138. package/esm/src/kernel/registry/provider-request.d.ts +0 -4
  139. package/esm/src/kernel/registry/provider-request.js +0 -25
  140. package/esm/src/kernel/registry/resolve.d.ts +0 -19
  141. package/esm/src/kernel/registry/resolve.js +0 -217
  142. package/esm/src/kernel/registry/schemas.d.ts +0 -14
  143. package/esm/src/kernel/registry/schemas.js +0 -23
  144. package/esm/src/kernel/registry/tools.d.ts +0 -12
  145. package/esm/src/kernel/registry/tools.js +0 -36
  146. package/esm/src/kernel/registry/vault.js +0 -18
  147. package/esm/src/kernel/types.d.ts +0 -521
  148. package/esm/src/kernel/types.js +0 -10
  149. package/esm/src/observability/mod.d.ts +0 -12
  150. package/esm/src/observability/spans.d.ts +0 -16
  151. package/esm/src/observability/spans.js +0 -56
  152. package/esm/src/observability/trace-attach.d.ts +0 -16
  153. package/esm/src/observability/trace-attach.js +0 -67
  154. package/esm/src/observability/trace-record.d.ts +0 -112
  155. package/esm/src/observability/trace-record.js +0 -140
  156. package/esm/src/observability/trace-usage.d.ts +0 -3
  157. package/esm/src/observability/trace-usage.js +0 -32
  158. package/esm/src/observability/trace.d.ts +0 -23
  159. package/esm/src/observability/trace.js +0 -121
  160. package/esm/src/presets/google.d.ts +0 -50
  161. package/esm/src/presets/google.js +0 -96
  162. package/esm/src/presets/mod.d.ts +0 -11
  163. package/esm/src/presets/mod.js +0 -10
  164. package/esm/src/providers/create-provider.d.ts +0 -29
  165. package/esm/src/providers/create-provider.js +0 -38
  166. package/esm/src/providers/gemini-tape.d.ts +0 -2
  167. package/esm/src/providers/gemini-tape.js +0 -46
  168. package/esm/src/providers/google-tap.d.ts +0 -3
  169. package/esm/src/providers/google-tap.js +0 -48
  170. package/esm/src/providers/interactions.d.ts +0 -5
  171. package/esm/src/providers/interactions.js +0 -152
  172. package/esm/src/providers/keys.d.ts +0 -19
  173. package/esm/src/providers/keys.js +0 -129
  174. package/esm/src/providers/mod.d.ts +0 -12
  175. package/esm/src/providers/openrouter-mod.d.ts +0 -13
  176. package/esm/src/providers/openrouter-payload.d.ts +0 -32
  177. package/esm/src/providers/openrouter-payload.js +0 -179
  178. package/esm/src/providers/openrouter.d.ts +0 -15
  179. package/esm/src/providers/openrouter.js +0 -589
  180. package/esm/src/providers/pcm.d.ts +0 -7
  181. package/esm/src/providers/pcm.js +0 -35
  182. package/esm/src/providers/provider.d.ts +0 -15
  183. package/esm/src/providers/provider.js +0 -166
  184. package/esm/src/providers/speech.d.ts +0 -23
  185. package/esm/src/providers/speech.js +0 -125
  186. package/esm/src/providers/sse.d.ts +0 -7
  187. package/esm/src/providers/sse.js +0 -53
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Host provider factory — the single public door for binding a profile to a transport.
3
+ *
4
+ * Routes from `profile.model.protocol` / `provider` (and whether the profile is
5
+ * a speech role). Adapters under this folder are internal implementation.
6
+ *
7
+ * @module
8
+ */
9
+
10
+ import { TheorumError } from '../guardrails/error.ts';
11
+ import type { ModelProvider, Profile } from '../kernel/types.ts';
12
+ import type { GeminiTransport } from './keys.ts';
13
+ import { createOpenRouterProvider } from './openrouter.ts';
14
+ import type { OpenRouterConfig } from './openrouter-payload.ts';
15
+ import { createInteractionsProvider } from './provider.ts';
16
+ import { createSpeechProvider } from './speech.ts';
17
+
18
+ /** Credentials supplied by the host when creating a provider. */
19
+ export interface CreateProviderOptions {
20
+ /** Google Interactions (chat, image, and speech when protocol is geminiInteractions). */
21
+ gemini?: GeminiTransport;
22
+ /**
23
+ * OpenRouter-compatible credentials for `openAi` profiles.
24
+ * Used for chat completions or `/audio/speech` when the profile is a speech role.
25
+ * Optional `voice` is a fallback when `outputs.speech.voice` is omitted.
26
+ */
27
+ openRouter?: OpenRouterConfig & { voice?: string };
28
+ }
29
+
30
+ function isSpeechRole(profile: Profile): boolean {
31
+ return profile.outputs.speech !== undefined;
32
+ }
33
+
34
+ /**
35
+ * Create a `ModelProvider` for a profile.
36
+ * One call: protocol/provider (and speech role) pick the transport.
37
+ */
38
+ export function createProvider(
39
+ profile: Profile,
40
+ options: CreateProviderOptions = {},
41
+ ): ModelProvider {
42
+ const { protocol, provider } = profile.model;
43
+
44
+ if (protocol === 'geminiInteractions' && provider === 'google') {
45
+ if (!options.gemini) {
46
+ throw new TheorumError('createProvider requires gemini transport for google Interactions');
47
+ }
48
+ return createInteractionsProvider(options.gemini);
49
+ }
50
+
51
+ if (protocol === 'openAi' && provider === 'openrouter') {
52
+ if (!options.openRouter) {
53
+ throw new TheorumError('createProvider requires openRouter config for openAi/openrouter');
54
+ }
55
+ if (isSpeechRole(profile)) {
56
+ return createSpeechProvider(options.openRouter);
57
+ }
58
+ return createOpenRouterProvider(options.openRouter);
59
+ }
60
+
61
+ throw new TheorumError(
62
+ `createProvider: unsupported protocol/provider pair '${protocol}'/'${provider}'`,
63
+ );
64
+ }
@@ -0,0 +1,59 @@
1
+ import { OMIT_CANARY } from '../kernel/engine/boundary.ts';
2
+ import { sha256 } from '../kernel/engine/hash.ts';
3
+ import { mapStrings } from '../kernel/engine/tree.ts';
4
+
5
+ function isImageBlob(rec: Record<string, unknown>): boolean {
6
+ if (rec.type === 'image' || rec.type === 'media') {
7
+ return true;
8
+ }
9
+ if (typeof rec.mimeType === 'string' || typeof rec.mime_type === 'string') {
10
+ return true;
11
+ }
12
+ return false;
13
+ }
14
+
15
+ async function scrubEntry(
16
+ rec: Record<string, unknown>,
17
+ key: string,
18
+ nested: unknown,
19
+ ): Promise<[string, unknown]> {
20
+ if (key === 'data' && typeof nested === 'string' && isImageBlob(rec)) {
21
+ return [key, await sha256(nested)];
22
+ }
23
+ return [key, await scrubGemini(nested)];
24
+ }
25
+
26
+ function scrubRecord(rec: Record<string, unknown>): Promise<Record<string, unknown>> {
27
+ return Promise.all(Object.entries(rec).map(([key, nested]) => scrubEntry(rec, key, nested))).then(
28
+ (pairs) => {
29
+ const out = Object.fromEntries(pairs);
30
+ if (typeof rec.data === 'string' && isImageBlob(rec)) {
31
+ out.dataKind = 'sha256';
32
+ }
33
+ return out;
34
+ },
35
+ );
36
+ }
37
+
38
+ function scrubGemini(value: unknown): Promise<unknown> {
39
+ if (Array.isArray(value)) {
40
+ return Promise.all(value.map((item) => scrubGemini(item)));
41
+ }
42
+ if (value && typeof value === 'object') {
43
+ return scrubRecord(value as Record<string, unknown>);
44
+ }
45
+ return Promise.resolve(value);
46
+ }
47
+
48
+ function redactCanaryInTree(value: unknown, canary: string): unknown {
49
+ if (!canary) {
50
+ return value;
51
+ }
52
+ return mapStrings(value, (text) => text.replaceAll(canary, OMIT_CANARY));
53
+ }
54
+
55
+ async function tapeGemini(value: unknown, canary: string): Promise<unknown> {
56
+ return redactCanaryInTree(await scrubGemini(value), canary);
57
+ }
58
+
59
+ export { tapeGemini };
@@ -0,0 +1,57 @@
1
+ import type { ProviderCompleteRequest } from '../kernel/types.ts';
2
+
3
+ const SECRET_HEADER = /key|auth|cookie|secret|token/i;
4
+
5
+ function tapeHeaderValue(key: string, value: string): string {
6
+ if (SECRET_HEADER.test(key)) {
7
+ return '[redacted]';
8
+ }
9
+ return value;
10
+ }
11
+
12
+ function tapeHeaders(headers?: HeadersInit): Record<string, string> {
13
+ const out: Record<string, string> = {};
14
+ for (const [key, value] of new Headers(headers).entries()) {
15
+ out[key] = tapeHeaderValue(key, value);
16
+ }
17
+ return out;
18
+ }
19
+
20
+ function throwRow(err: unknown): Record<string, unknown> {
21
+ if (err instanceof Error) {
22
+ return { eventType: 'http_throw', name: err.name, message: err.message };
23
+ }
24
+ return { eventType: 'http_throw', name: 'Error', message: String(err) };
25
+ }
26
+
27
+ function tapFetch(
28
+ tap: ProviderCompleteRequest['tapGemini'],
29
+ send: typeof fetch = fetch,
30
+ ): typeof fetch {
31
+ return async (url, init) => {
32
+ const method = init?.method ?? 'GET';
33
+ tap?.({
34
+ eventType: 'http_request',
35
+ method,
36
+ url: String(url),
37
+ headers: tapeHeaders(init?.headers),
38
+ });
39
+ try {
40
+ const res = await send(url, init);
41
+ tap?.({
42
+ eventType: 'http_response',
43
+ status: res.status,
44
+ headers: tapeHeaders(res.headers),
45
+ });
46
+ if (!res.ok) {
47
+ tap?.({ eventType: 'http_error_body', body: await res.clone().text() });
48
+ }
49
+ return res;
50
+ } catch (err) {
51
+ tap?.(throwRow(err));
52
+ throw err;
53
+ }
54
+ };
55
+ }
56
+
57
+ export { tapFetch };
@@ -0,0 +1,182 @@
1
+ import { TheorumError } from '../guardrails/error.ts';
2
+ import { getTool } from '../kernel/registry/catalog.ts';
3
+ import { getStructured } from '../kernel/registry/schemas.ts';
4
+ import type { InteractionPart, ProviderCompleteRequest } from '../kernel/types.ts';
5
+
6
+ function camelToSnake(key: string): string {
7
+ return key.replaceAll(/[A-Z]/g, (ch) => `_${ch.toLowerCase()}`);
8
+ }
9
+
10
+ function toGoogleValue(value: unknown): unknown {
11
+ if (Array.isArray(value)) {
12
+ return value.map(toGoogleValue);
13
+ }
14
+ if (value && typeof value === 'object') {
15
+ const out: Record<string, unknown> = {};
16
+ for (const [key, nested] of Object.entries(value)) {
17
+ // JSON Schema property names must stay as authored (e.g. correctAnswer in
18
+ // both properties and required). Snake-casing breaks Gemini validation.
19
+ if (key === 'schema') {
20
+ out[camelToSnake(key)] = nested;
21
+ continue;
22
+ }
23
+ out[camelToSnake(key)] = toGoogleValue(nested);
24
+ }
25
+ return out;
26
+ }
27
+ return value;
28
+ }
29
+
30
+ function wirePart(part: InteractionPart): Record<string, string> {
31
+ if (part.type === 'text') {
32
+ return { type: 'text', text: part.text };
33
+ }
34
+ return { type: part.type, mimeType: part.mimeType, data: part.data };
35
+ }
36
+
37
+ const USER_INPUT = 'user_input';
38
+
39
+ function userInputStep(parts: InteractionPart[]): {
40
+ type: string;
41
+ content: Record<string, string>[];
42
+ } {
43
+ return { type: USER_INPUT, content: parts.map(wirePart) };
44
+ }
45
+
46
+ function historyStep(msg: import('../kernel/types.ts').TurnHistoryMessage): {
47
+ type: string;
48
+ content: Record<string, string>[];
49
+ } {
50
+ const isAssistant = msg.role === 'assistant';
51
+ const type = isAssistant ? 'model_turn' : 'user_input';
52
+ if (msg.parts && msg.parts.length > 0) {
53
+ return { type, content: msg.parts.map(wirePart) };
54
+ }
55
+ return { type, content: [{ type: 'text', text: msg.content ?? '' }] };
56
+ }
57
+
58
+ function systemHoldsUserInput(system: string, parts: InteractionPart[]): boolean {
59
+ for (const part of parts) {
60
+ if (part.type === 'text' && part.text && system.includes(part.text)) {
61
+ return true;
62
+ }
63
+ }
64
+ return false;
65
+ }
66
+
67
+ function jsonResponseFormat(schema: Record<string, unknown>): unknown[] {
68
+ return [{ type: 'text', mimeType: 'application/json', schema }];
69
+ }
70
+
71
+ function attachResponseFormat(req: ProviderCompleteRequest, camel: Record<string, unknown>): void {
72
+ if (req.speech) {
73
+ if (req.image) {
74
+ throw new TheorumError('cannot mix speech and image response formats');
75
+ }
76
+ if (req.structured) {
77
+ throw new TheorumError('cannot mix speech and structured response formats');
78
+ }
79
+ camel.responseFormat = { type: 'audio' };
80
+ return;
81
+ }
82
+ if (req.image) {
83
+ camel.responseFormat = {
84
+ type: 'image',
85
+ mimeType: req.image.mimeType,
86
+ aspectRatio: req.image.aspectRatio,
87
+ imageSize: req.image.size,
88
+ };
89
+ return;
90
+ }
91
+ if (!req.structured) {
92
+ return;
93
+ }
94
+ const spec = getStructured(req.structured);
95
+ if (spec.enforced !== 'responseFormat' || !spec.jsonSchema) {
96
+ return;
97
+ }
98
+ camel.responseFormat = jsonResponseFormat(spec.jsonSchema);
99
+ }
100
+
101
+ function attachSpeechConfig(
102
+ req: ProviderCompleteRequest,
103
+ generationConfig: Record<string, unknown>,
104
+ ): void {
105
+ if (!req.speech) {
106
+ return;
107
+ }
108
+ if (req.speech.voice) {
109
+ generationConfig.speechConfig = [{ voice: req.speech.voice }];
110
+ }
111
+ }
112
+
113
+ function inputStepsFromRequest(req: ProviderCompleteRequest): {
114
+ type: string;
115
+ content: Record<string, string>[];
116
+ }[] {
117
+ const inputSteps: { type: string; content: Record<string, string>[] }[] = [];
118
+ for (const h of req.history ?? []) {
119
+ inputSteps.push(historyStep(h));
120
+ }
121
+ if (req.input.length > 0 || inputSteps.length === 0) {
122
+ inputSteps.push(userInputStep(req.input));
123
+ }
124
+ return inputSteps;
125
+ }
126
+
127
+ function applyOptionalRequestFields(
128
+ req: ProviderCompleteRequest,
129
+ camel: Record<string, unknown>,
130
+ ): void {
131
+ if (req.store !== undefined) {
132
+ camel.store = req.store;
133
+ }
134
+ if (req.previousInteractionId) {
135
+ camel.previousInteractionId = req.previousInteractionId;
136
+ }
137
+ if (req.system) {
138
+ camel.systemInstruction = req.system;
139
+ }
140
+ if (req.builtins.length > 0) {
141
+ camel.tools = req.builtins.map((id) => {
142
+ const type = getTool(id)?.interactionsType;
143
+ if (!type) {
144
+ throw new TheorumError(`Builtin '${id}' has no Interactions wire type`);
145
+ }
146
+ return { type };
147
+ });
148
+ }
149
+ }
150
+
151
+ function baseInteractionsBody(req: ProviderCompleteRequest): Record<string, unknown> {
152
+ const generationConfig: Record<string, unknown> = {
153
+ temperature: req.temperature,
154
+ maxOutputTokens: req.maxOutputTokens,
155
+ };
156
+ if (req.speech) {
157
+ // TTS models reject chat thinking knobs; voice lives under speech_config.
158
+ attachSpeechConfig(req, generationConfig);
159
+ } else {
160
+ generationConfig.thinkingLevel = req.thinking;
161
+ generationConfig.thinkingSummaries = req.summaries;
162
+ }
163
+ return {
164
+ model: req.apiId,
165
+ stream: true,
166
+ input: inputStepsFromRequest(req),
167
+ generationConfig,
168
+ };
169
+ }
170
+
171
+ /** Interactions REST body for one complete() call (Google snake_case keys). */
172
+ function toInteractionsBody(req: ProviderCompleteRequest): Record<string, unknown> {
173
+ if (systemHoldsUserInput(req.system, req.input)) {
174
+ throw new TheorumError('user input cannot be placed in the system block');
175
+ }
176
+ const camel = baseInteractionsBody(req);
177
+ applyOptionalRequestFields(req, camel);
178
+ attachResponseFormat(req, camel);
179
+ return toGoogleValue(camel) as Record<string, unknown>;
180
+ }
181
+
182
+ export { camelToSnake, toInteractionsBody };
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Gemini key vault selection, quota overflow, and fetch retries.
3
+ *
4
+ * Host applications supply vault credentials through `GeminiTransport`.
5
+ * THEORUM does not read environment variables for these keys.
6
+ *
7
+ * @module
8
+ */
9
+
10
+ import { TheorumError, UPSTREAM_FAILED } from '../guardrails/error.ts';
11
+ import type { GeminiBucket } from '../kernel/types.ts';
12
+
13
+ type GeminiVault = Record<GeminiBucket, string | undefined>;
14
+
15
+ interface GeminiTransport {
16
+ vault: GeminiVault;
17
+ wait?: (ms: number) => Promise<void>;
18
+ fetch?: typeof fetch;
19
+ }
20
+
21
+ const ATTEMPTS = 3;
22
+ const LAST_ATTEMPT = ATTEMPTS - 1;
23
+ const BACKOFF_FIRST_MS = 1000;
24
+ const BACKOFF_SECOND_MS = 2000;
25
+ const BACKOFF_THIRD_MS = 4000;
26
+ const BACKOFF_MS = [BACKOFF_FIRST_MS, BACKOFF_SECOND_MS, BACKOFF_THIRD_MS];
27
+
28
+ const HTTP_TIMEOUT = 408;
29
+ const HTTP_QUOTA = 429;
30
+ const HTTP_SERVER = 500;
31
+ const HTTP_BAD_GATEWAY = 502;
32
+ const HTTP_UNAVAILABLE = 503;
33
+ const HTTP_GATEWAY_TIMEOUT = 504;
34
+
35
+ const QUOTA_RE = /quota/i;
36
+ const TRANSIENT_THROWN_RE =
37
+ /name resolution|dns|econnreset|econnrefused|etimedout|network|fetch failed|temporarily unavailable|socket|503|502|504/i;
38
+
39
+ function waitDefault(ms: number): Promise<void> {
40
+ return new Promise((resolve) => {
41
+ setTimeout(resolve, ms);
42
+ });
43
+ }
44
+
45
+ function isQuota(err: unknown): boolean {
46
+ const s = String(err);
47
+ return s.includes(String(HTTP_QUOTA)) || s.includes('RESOURCE_EXHAUSTED') || QUOTA_RE.test(s);
48
+ }
49
+
50
+ function isTransientHttp(status: number): boolean {
51
+ return (
52
+ status === HTTP_TIMEOUT ||
53
+ status === HTTP_QUOTA ||
54
+ status === HTTP_SERVER ||
55
+ status === HTTP_BAD_GATEWAY ||
56
+ status === HTTP_UNAVAILABLE ||
57
+ status === HTTP_GATEWAY_TIMEOUT
58
+ );
59
+ }
60
+
61
+ function isTransientThrown(err: unknown): boolean {
62
+ return TRANSIENT_THROWN_RE.test(String(err));
63
+ }
64
+
65
+ function requireKey(vault: GeminiVault, bucket: GeminiBucket): string {
66
+ const key = vault[bucket];
67
+ if (!key) {
68
+ throw new TheorumError(UPSTREAM_FAILED);
69
+ }
70
+ return key;
71
+ }
72
+
73
+ function backoffMs(attempt: number): number {
74
+ return BACKOFF_MS[attempt] ?? BACKOFF_SECOND_MS;
75
+ }
76
+
77
+ async function runWithBackoff<T>(
78
+ apiKey: string,
79
+ run: (apiKey: string) => Promise<T>,
80
+ wait: (ms: number) => Promise<void>,
81
+ attempt: number,
82
+ ): Promise<T> {
83
+ try {
84
+ return await run(apiKey);
85
+ } catch (err) {
86
+ if (!(isQuota(err) || isTransientThrown(err)) || attempt === LAST_ATTEMPT) {
87
+ throw err;
88
+ }
89
+ await wait(backoffMs(attempt));
90
+ return runWithBackoff(apiKey, run, wait, attempt + 1);
91
+ }
92
+ }
93
+
94
+ function canOverflow(
95
+ bucket: GeminiBucket,
96
+ vault: GeminiVault,
97
+ primary: string,
98
+ ): string | undefined {
99
+ if (bucket === 'paid') {
100
+ return undefined;
101
+ }
102
+ const { paid } = vault;
103
+ if (!paid || paid === primary) {
104
+ return undefined;
105
+ }
106
+ return paid;
107
+ }
108
+
109
+ async function withGeminiKey<T>(
110
+ bucket: GeminiBucket,
111
+ run: (apiKey: string) => Promise<T>,
112
+ transport: GeminiTransport,
113
+ ): Promise<T> {
114
+ const wait = transport.wait ?? waitDefault;
115
+ const primary = requireKey(transport.vault, bucket);
116
+ try {
117
+ return await runWithBackoff(primary, run, wait, 0);
118
+ } catch (err) {
119
+ const paid = canOverflow(bucket, transport.vault, primary);
120
+ if (!(isQuota(err) && paid)) {
121
+ throw err;
122
+ }
123
+ return await runWithBackoff(paid, run, wait, 0);
124
+ }
125
+ }
126
+
127
+ function withApiKey(init: RequestInit, apiKey: string): RequestInit {
128
+ const headers = new Headers(init.headers);
129
+ headers.set('x-goog-api-key', apiKey);
130
+ if (!headers.has('Content-Type') && (init.method || 'GET').toUpperCase() !== 'GET') {
131
+ headers.set('Content-Type', 'application/json');
132
+ }
133
+ return { ...init, headers };
134
+ }
135
+
136
+ interface FetchAttempt {
137
+ href: string;
138
+ init: RequestInit;
139
+ apiKey: string;
140
+ transport: GeminiTransport;
141
+ attempt: number;
142
+ }
143
+
144
+ async function fetchWithBackoff(args: FetchAttempt): Promise<Response> {
145
+ const wait = args.transport.wait ?? waitDefault;
146
+ const send = args.transport.fetch ?? fetch;
147
+ try {
148
+ const last = await send(args.href, withApiKey(args.init, args.apiKey));
149
+ if (!isTransientHttp(last.status) || args.attempt === LAST_ATTEMPT) {
150
+ return last;
151
+ }
152
+ await wait(backoffMs(args.attempt));
153
+ return fetchWithBackoff({ ...args, attempt: args.attempt + 1 });
154
+ } catch (err) {
155
+ if (!isTransientThrown(err) || args.attempt === LAST_ATTEMPT) {
156
+ throw err;
157
+ }
158
+ await wait(backoffMs(args.attempt));
159
+ return fetchWithBackoff({ ...args, attempt: args.attempt + 1 });
160
+ }
161
+ }
162
+
163
+ async function fetchGemini(
164
+ url: string,
165
+ init: RequestInit,
166
+ bucket: GeminiBucket,
167
+ transport: GeminiTransport,
168
+ ): Promise<Response> {
169
+ const parsed = new URL(url);
170
+ parsed.searchParams.delete('key');
171
+ const href = parsed.toString();
172
+ const primary = requireKey(transport.vault, bucket);
173
+ let last = await fetchWithBackoff({ href, init, apiKey: primary, transport, attempt: 0 });
174
+ const paid = canOverflow(bucket, transport.vault, primary);
175
+ if (last.status === HTTP_QUOTA && paid) {
176
+ last = await fetchWithBackoff({ href, init, apiKey: paid, transport, attempt: 0 });
177
+ }
178
+ return last;
179
+ }
180
+
181
+ export type { GeminiTransport, GeminiVault };
182
+ export { fetchGemini, withGeminiKey };
@@ -6,5 +6,7 @@
6
6
  *
7
7
  * @module
8
8
  */
9
- import "../../_dnt.polyfills.js";
10
- export { createProvider } from './create-provider.js';
9
+
10
+ export type { CreateProviderOptions } from './create-provider.ts';
11
+ export { createProvider } from './create-provider.ts';
12
+ export type { GeminiTransport, GeminiVault } from './keys.ts';
@@ -7,6 +7,10 @@
7
7
  *
8
8
  * @module
9
9
  */
10
- import "../../_dnt.polyfills.js";
11
- export { createOpenRouterProvider } from './openrouter.js';
12
- export { resolveOpenRouterModel, toOpenRouterPayload, } from './openrouter-payload.js';
10
+
11
+ export { createOpenRouterProvider } from './openrouter.ts';
12
+ export type { OpenRouterConfig } from './openrouter-payload.ts';
13
+ export {
14
+ resolveOpenRouterModel,
15
+ toOpenRouterPayload,
16
+ } from './openrouter-payload.ts';