theorum 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +338 -0
- package/docs/AGENT_PROFILE_CONTRACT.md +161 -0
- package/docs/CLI_SPEC.md +183 -0
- package/docs/SECRETS.md +55 -0
- package/esm/_dnt.polyfills.d.ts +11 -0
- package/esm/_dnt.polyfills.js +15 -0
- package/esm/_dnt.shims.d.ts +5 -0
- package/esm/_dnt.shims.js +61 -0
- package/esm/mod.d.ts +37 -0
- package/esm/mod.js +35 -0
- package/esm/package.json +3 -0
- package/esm/src/guardrails/error.d.ts +35 -0
- package/esm/src/guardrails/error.js +116 -0
- package/esm/src/guardrails/injection.d.ts +12 -0
- package/esm/src/guardrails/injection.js +220 -0
- package/esm/src/guardrails/keys.d.ts +12 -0
- package/esm/src/guardrails/keys.js +132 -0
- package/esm/src/guardrails/mod.d.ts +14 -0
- package/esm/src/guardrails/mod.js +14 -0
- package/esm/src/guardrails/sanitize.d.ts +22 -0
- package/esm/src/guardrails/sanitize.js +133 -0
- package/esm/src/guardrails/sensitive.d.ts +12 -0
- package/esm/src/guardrails/sensitive.js +88 -0
- package/esm/src/kernel/engine/boundary.d.ts +10 -0
- package/esm/src/kernel/engine/boundary.js +55 -0
- package/esm/src/kernel/engine/delta.d.ts +8 -0
- package/esm/src/kernel/engine/delta.js +362 -0
- package/esm/src/kernel/engine/hash.d.ts +1 -0
- package/esm/src/kernel/engine/hash.js +9 -0
- package/esm/src/kernel/engine/record.d.ts +2 -0
- package/esm/src/kernel/engine/record.js +7 -0
- package/esm/src/kernel/engine/repair.d.ts +9 -0
- package/esm/src/kernel/engine/repair.js +38 -0
- package/esm/src/kernel/engine/runner.d.ts +14 -0
- package/esm/src/kernel/engine/runner.js +731 -0
- package/esm/src/kernel/engine/tree.d.ts +2 -0
- package/esm/src/kernel/engine/tree.js +17 -0
- package/esm/src/kernel/mod.d.ts +16 -0
- package/esm/src/kernel/mod.js +15 -0
- package/esm/src/kernel/registry/catalog.d.ts +24 -0
- package/esm/src/kernel/registry/catalog.js +213 -0
- package/esm/src/kernel/registry/profiles.d.ts +36 -0
- package/esm/src/kernel/registry/profiles.js +111 -0
- package/esm/src/kernel/registry/resolve.d.ts +20 -0
- package/esm/src/kernel/registry/resolve.js +235 -0
- package/esm/src/kernel/registry/schemas.d.ts +14 -0
- package/esm/src/kernel/registry/schemas.js +23 -0
- package/esm/src/kernel/registry/tools.d.ts +12 -0
- package/esm/src/kernel/registry/tools.js +36 -0
- package/esm/src/kernel/types.d.ts +497 -0
- package/esm/src/kernel/types.js +10 -0
- package/esm/src/observability/mod.d.ts +12 -0
- package/esm/src/observability/mod.js +10 -0
- package/esm/src/observability/spans.d.ts +16 -0
- package/esm/src/observability/spans.js +56 -0
- package/esm/src/observability/trace-attach.d.ts +16 -0
- package/esm/src/observability/trace-attach.js +81 -0
- package/esm/src/observability/trace-record.d.ts +112 -0
- package/esm/src/observability/trace-record.js +140 -0
- package/esm/src/observability/trace-usage.d.ts +3 -0
- package/esm/src/observability/trace-usage.js +32 -0
- package/esm/src/observability/trace.d.ts +23 -0
- package/esm/src/observability/trace.js +121 -0
- package/esm/src/providers/attachments.d.ts +17 -0
- package/esm/src/providers/attachments.js +156 -0
- package/esm/src/providers/gemini-tape.d.ts +3 -0
- package/esm/src/providers/gemini-tape.js +46 -0
- package/esm/src/providers/google-tap.d.ts +3 -0
- package/esm/src/providers/google-tap.js +48 -0
- package/esm/src/providers/interactions.d.ts +5 -0
- package/esm/src/providers/interactions.js +127 -0
- package/esm/src/providers/media.d.ts +5 -0
- package/esm/src/providers/media.js +125 -0
- package/esm/src/providers/mod.d.ts +15 -0
- package/esm/src/providers/mod.js +13 -0
- package/esm/src/providers/openrouter-payload.d.ts +24 -0
- package/esm/src/providers/openrouter-payload.js +177 -0
- package/esm/src/providers/openrouter.d.ts +17 -0
- package/esm/src/providers/openrouter.js +332 -0
- package/esm/src/providers/provider.d.ts +13 -0
- package/esm/src/providers/provider.js +123 -0
- package/esm/src/providers/sse.d.ts +7 -0
- package/esm/src/providers/sse.js +53 -0
- package/esm/src/providers/tts.d.ts +24 -0
- package/esm/src/providers/tts.js +144 -0
- package/package.json +48 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { OMIT_CANARY } from '../kernel/engine/boundary.js';
|
|
2
|
+
import { sha256 } from '../kernel/engine/hash.js';
|
|
3
|
+
import { mapStrings } from '../kernel/engine/tree.js';
|
|
4
|
+
function isImageBlob(rec) {
|
|
5
|
+
if (rec.type === 'image' || rec.type === 'media') {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
if (typeof rec.mimeType === 'string' || typeof rec.mime_type === 'string') {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
async function scrubEntry(rec, key, nested) {
|
|
14
|
+
if (key === 'data' && typeof nested === 'string' && isImageBlob(rec)) {
|
|
15
|
+
return [key, await sha256(nested)];
|
|
16
|
+
}
|
|
17
|
+
return [key, await scrubGemini(nested)];
|
|
18
|
+
}
|
|
19
|
+
function scrubRecord(rec) {
|
|
20
|
+
return Promise.all(Object.entries(rec).map(([key, nested]) => scrubEntry(rec, key, nested))).then((pairs) => {
|
|
21
|
+
const out = Object.fromEntries(pairs);
|
|
22
|
+
if (typeof rec.data === 'string' && isImageBlob(rec)) {
|
|
23
|
+
out.dataKind = 'sha256';
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
function scrubGemini(value) {
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
return Promise.all(value.map((item) => scrubGemini(item)));
|
|
31
|
+
}
|
|
32
|
+
if (value && typeof value === 'object') {
|
|
33
|
+
return scrubRecord(value);
|
|
34
|
+
}
|
|
35
|
+
return Promise.resolve(value);
|
|
36
|
+
}
|
|
37
|
+
function redactCanaryInTree(value, canary) {
|
|
38
|
+
if (!canary) {
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
return mapStrings(value, (text) => text.replaceAll(canary, OMIT_CANARY));
|
|
42
|
+
}
|
|
43
|
+
async function tapeGemini(value, canary) {
|
|
44
|
+
return redactCanaryInTree(await scrubGemini(value), canary);
|
|
45
|
+
}
|
|
46
|
+
export { sha256, tapeGemini };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const SECRET_HEADER = /key|auth|cookie|secret|token/i;
|
|
2
|
+
function tapeHeaderValue(key, value) {
|
|
3
|
+
if (SECRET_HEADER.test(key)) {
|
|
4
|
+
return '[redacted]';
|
|
5
|
+
}
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
function tapeHeaders(headers) {
|
|
9
|
+
const out = {};
|
|
10
|
+
for (const [key, value] of new Headers(headers).entries()) {
|
|
11
|
+
out[key] = tapeHeaderValue(key, value);
|
|
12
|
+
}
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
function throwRow(err) {
|
|
16
|
+
if (err instanceof Error) {
|
|
17
|
+
return { eventType: 'http_throw', name: err.name, message: err.message };
|
|
18
|
+
}
|
|
19
|
+
return { eventType: 'http_throw', name: 'Error', message: String(err) };
|
|
20
|
+
}
|
|
21
|
+
function tapFetch(tap, send = fetch) {
|
|
22
|
+
return async (url, init) => {
|
|
23
|
+
const method = init?.method ?? 'GET';
|
|
24
|
+
tap?.({
|
|
25
|
+
eventType: 'http_request',
|
|
26
|
+
method,
|
|
27
|
+
url: String(url),
|
|
28
|
+
headers: tapeHeaders(init?.headers),
|
|
29
|
+
});
|
|
30
|
+
try {
|
|
31
|
+
const res = await send(url, init);
|
|
32
|
+
tap?.({
|
|
33
|
+
eventType: 'http_response',
|
|
34
|
+
status: res.status,
|
|
35
|
+
headers: tapeHeaders(res.headers),
|
|
36
|
+
});
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
tap?.({ eventType: 'http_error_body', body: await res.clone().text() });
|
|
39
|
+
}
|
|
40
|
+
return res;
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
tap?.(throwRow(err));
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export { tapFetch };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ProviderCompleteRequest } from '../kernel/types.js';
|
|
2
|
+
declare function camelToSnake(key: string): string;
|
|
3
|
+
/** Interactions REST body for one complete() call (Google snake_case keys). */
|
|
4
|
+
declare function toInteractionsBody(req: ProviderCompleteRequest): Record<string, unknown>;
|
|
5
|
+
export { camelToSnake, toInteractionsBody };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { TheorumError } from '../guardrails/error.js';
|
|
2
|
+
import { modelEntry } from '../kernel/registry/catalog.js';
|
|
3
|
+
import { getStructured } from '../kernel/registry/schemas.js';
|
|
4
|
+
const BUILTIN_API = {
|
|
5
|
+
googleSearch: 'google_search',
|
|
6
|
+
googleMaps: 'google_maps',
|
|
7
|
+
urlContext: 'url_context',
|
|
8
|
+
};
|
|
9
|
+
function camelToSnake(key) {
|
|
10
|
+
return key.replaceAll(/[A-Z]/g, (ch) => `_${ch.toLowerCase()}`);
|
|
11
|
+
}
|
|
12
|
+
function toGoogleValue(value) {
|
|
13
|
+
if (Array.isArray(value)) {
|
|
14
|
+
return value.map(toGoogleValue);
|
|
15
|
+
}
|
|
16
|
+
if (value && typeof value === 'object') {
|
|
17
|
+
const out = {};
|
|
18
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
19
|
+
// JSON Schema property names must stay as authored (e.g. correctAnswer in
|
|
20
|
+
// both properties and required). Snake-casing breaks Gemini validation.
|
|
21
|
+
if (key === 'schema') {
|
|
22
|
+
out[camelToSnake(key)] = nested;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
out[camelToSnake(key)] = toGoogleValue(nested);
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function wirePart(part) {
|
|
32
|
+
if (part.type === 'text') {
|
|
33
|
+
return { type: 'text', text: part.text };
|
|
34
|
+
}
|
|
35
|
+
return { type: part.type, mimeType: part.mimeType, data: part.data };
|
|
36
|
+
}
|
|
37
|
+
const USER_INPUT = 'user_input';
|
|
38
|
+
function userInputStep(parts) {
|
|
39
|
+
return { type: USER_INPUT, content: parts.map(wirePart) };
|
|
40
|
+
}
|
|
41
|
+
function historyStep(msg) {
|
|
42
|
+
const isAssistant = msg.role === 'assistant';
|
|
43
|
+
const type = isAssistant ? 'model_turn' : 'user_input';
|
|
44
|
+
if (msg.parts && msg.parts.length > 0) {
|
|
45
|
+
return { type, content: msg.parts.map(wirePart) };
|
|
46
|
+
}
|
|
47
|
+
return { type, content: [{ type: 'text', text: msg.content ?? '' }] };
|
|
48
|
+
}
|
|
49
|
+
function systemHoldsUserInput(system, parts) {
|
|
50
|
+
for (const part of parts) {
|
|
51
|
+
if (part.type === 'text' && part.text && system.includes(part.text)) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
function jsonResponseFormat(schema) {
|
|
58
|
+
return [{ type: 'text', mimeType: 'application/json', schema }];
|
|
59
|
+
}
|
|
60
|
+
function attachResponseFormat(req, camel) {
|
|
61
|
+
if (req.image) {
|
|
62
|
+
camel.responseFormat = {
|
|
63
|
+
type: 'image',
|
|
64
|
+
mimeType: req.image.mimeType,
|
|
65
|
+
aspectRatio: req.image.aspectRatio,
|
|
66
|
+
imageSize: req.image.imageSize,
|
|
67
|
+
};
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (!req.structured) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const spec = getStructured(req.structured);
|
|
74
|
+
if (spec.enforced !== 'responseFormat' || !spec.jsonSchema) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
camel.responseFormat = jsonResponseFormat(spec.jsonSchema);
|
|
78
|
+
}
|
|
79
|
+
function inputStepsFromRequest(req) {
|
|
80
|
+
const inputSteps = [];
|
|
81
|
+
for (const h of req.history ?? []) {
|
|
82
|
+
inputSteps.push(historyStep(h));
|
|
83
|
+
}
|
|
84
|
+
if (req.input.length > 0 || inputSteps.length === 0) {
|
|
85
|
+
inputSteps.push(userInputStep(req.input));
|
|
86
|
+
}
|
|
87
|
+
return inputSteps;
|
|
88
|
+
}
|
|
89
|
+
function applyOptionalRequestFields(req, camel) {
|
|
90
|
+
if (req.store !== undefined) {
|
|
91
|
+
camel.store = req.store;
|
|
92
|
+
}
|
|
93
|
+
if (req.previousInteractionId) {
|
|
94
|
+
camel.previousInteractionId = req.previousInteractionId;
|
|
95
|
+
}
|
|
96
|
+
if (req.system) {
|
|
97
|
+
camel.systemInstruction = req.system;
|
|
98
|
+
}
|
|
99
|
+
if (req.builtins.length > 0) {
|
|
100
|
+
camel.tools = req.builtins.map((id) => ({ type: BUILTIN_API[id] }));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function baseInteractionsBody(req) {
|
|
104
|
+
const catalog = modelEntry(req.model);
|
|
105
|
+
return {
|
|
106
|
+
model: catalog.apiId,
|
|
107
|
+
stream: true,
|
|
108
|
+
input: inputStepsFromRequest(req),
|
|
109
|
+
generationConfig: {
|
|
110
|
+
temperature: req.temperature,
|
|
111
|
+
maxOutputTokens: req.maxOutputTokens,
|
|
112
|
+
thinkingLevel: req.thinking,
|
|
113
|
+
thinkingSummaries: req.summaries,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** Interactions REST body for one complete() call (Google snake_case keys). */
|
|
118
|
+
function toInteractionsBody(req) {
|
|
119
|
+
if (systemHoldsUserInput(req.system, req.input)) {
|
|
120
|
+
throw new TheorumError('user input cannot be placed in the system block');
|
|
121
|
+
}
|
|
122
|
+
const camel = baseInteractionsBody(req);
|
|
123
|
+
applyOptionalRequestFields(req, camel);
|
|
124
|
+
attachResponseFormat(req, camel);
|
|
125
|
+
return toGoogleValue(camel);
|
|
126
|
+
}
|
|
127
|
+
export { camelToSnake, toInteractionsBody };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { BuiltinToolId, ImageResponseFormat, InteractionPart, ModelId, Profile, TurnRequest } from '../kernel/types.js';
|
|
2
|
+
declare function resolveImageFormat(profile: Profile, model: ModelId, slots?: Record<string, string>): ImageResponseFormat | null;
|
|
3
|
+
declare function resolveInputParts(profile: Profile, model: ModelId, req: TurnRequest): InteractionPart[];
|
|
4
|
+
declare function assertImageGrounding(model: ModelId, builtins: BuiltinToolId[]): void;
|
|
5
|
+
export { assertImageGrounding, resolveImageFormat, resolveInputParts };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { TheorumError } from '../guardrails/error.js';
|
|
2
|
+
import { wrapUserData } from '../kernel/engine/boundary.js';
|
|
3
|
+
import { synthesizeRepairPrompt } from '../kernel/engine/repair.js';
|
|
4
|
+
import { geminiKindForMime, mimeAllowed, mimeEssence, modelEntry, } from '../kernel/registry/catalog.js';
|
|
5
|
+
import { assertAttachmentLimits, requireMediaLimits } from './attachments.js';
|
|
6
|
+
function listedValue(allowed, requested) {
|
|
7
|
+
if (!requested) {
|
|
8
|
+
return allowed[0];
|
|
9
|
+
}
|
|
10
|
+
for (const item of allowed) {
|
|
11
|
+
if (item === requested) {
|
|
12
|
+
return item;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
function requireImageSpec(profile, model) {
|
|
18
|
+
const spec = modelEntry(model).image;
|
|
19
|
+
if (!spec) {
|
|
20
|
+
throw new TheorumError(`Profile ${profile.id} requests media but ${model} is not an image model`);
|
|
21
|
+
}
|
|
22
|
+
if (profile.outputs.structured !== null) {
|
|
23
|
+
throw new TheorumError(`Profile ${profile.id} cannot mix structured JSON with native image output`);
|
|
24
|
+
}
|
|
25
|
+
return spec;
|
|
26
|
+
}
|
|
27
|
+
function resolveImageFormat(profile, model, slots) {
|
|
28
|
+
if (!profile.outputs.media) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const spec = requireImageSpec(profile, model);
|
|
32
|
+
const aspectRatio = listedValue(spec.aspectRatios, slots?.aspectRatio);
|
|
33
|
+
const imageSize = listedValue(spec.sizes, slots?.imageSize);
|
|
34
|
+
if (!(aspectRatio && imageSize)) {
|
|
35
|
+
throw new TheorumError(`Unknown image aspect or size for ${profile.id}`);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
type: 'image',
|
|
39
|
+
mimeType: spec.outputMime,
|
|
40
|
+
aspectRatio,
|
|
41
|
+
imageSize,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function assertGeminiMime(mime) {
|
|
45
|
+
const kind = geminiKindForMime(mime);
|
|
46
|
+
if (!kind) {
|
|
47
|
+
throw new TheorumError(`MIME '${mime}' is not a Gemini input type`);
|
|
48
|
+
}
|
|
49
|
+
return kind;
|
|
50
|
+
}
|
|
51
|
+
function mediaParts(profile, model, blobs, channel) {
|
|
52
|
+
const accept = channel === 'voice' ? profile.inputs.voice?.accept : profile.inputs.attachments?.accept;
|
|
53
|
+
if (!accept) {
|
|
54
|
+
throw new TheorumError(`Profile ${profile.id} does not accept ${channel}`);
|
|
55
|
+
}
|
|
56
|
+
const spec = modelEntry(model).image;
|
|
57
|
+
const imageCount = blobs.filter((blob) => geminiKindForMime(blob.mimeType) === 'image').length;
|
|
58
|
+
if (spec && imageCount > spec.maxInputImages) {
|
|
59
|
+
throw new TheorumError(`At most ${spec.maxInputImages} reference images on ${model}`);
|
|
60
|
+
}
|
|
61
|
+
return blobs.map((blob) => {
|
|
62
|
+
const kind = assertGeminiMime(blob.mimeType);
|
|
63
|
+
if (!mimeAllowed(accept, blob.mimeType)) {
|
|
64
|
+
throw new TheorumError(`MIME '${blob.mimeType}' is not accepted on ${profile.id}`);
|
|
65
|
+
}
|
|
66
|
+
const essence = mimeEssence(blob.mimeType);
|
|
67
|
+
if (kind === 'image' && spec && !spec.inputMimes.includes(essence) && essence !== 'image/jpg') {
|
|
68
|
+
throw new TheorumError(`MIME '${blob.mimeType}' is not valid on ${model}`);
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
type: kind,
|
|
72
|
+
mimeType: essence === 'image/jpg' ? 'image/jpeg' : essence,
|
|
73
|
+
data: blob.data,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function extractTextPart(profile, req) {
|
|
78
|
+
const { text, repair, history } = req.input ?? {};
|
|
79
|
+
if (profile.inputs.text === false) {
|
|
80
|
+
if (text) {
|
|
81
|
+
throw new TheorumError(`Profile ${profile.id} does not accept text input`);
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
let promptText = text;
|
|
86
|
+
if (repair) {
|
|
87
|
+
promptText = synthesizeRepairPrompt({ profile, repair, history });
|
|
88
|
+
}
|
|
89
|
+
if (!promptText) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return { type: 'text', text: wrapUserData(promptText) };
|
|
93
|
+
}
|
|
94
|
+
function extractMediaParts(profile, model, req) {
|
|
95
|
+
const { attachments, voice } = req.input ?? {};
|
|
96
|
+
const files = attachments ?? [];
|
|
97
|
+
const clips = voice ?? [];
|
|
98
|
+
if (files.length + clips.length > 0) {
|
|
99
|
+
assertAttachmentLimits([...files, ...clips], requireMediaLimits(profile));
|
|
100
|
+
}
|
|
101
|
+
const parts = [];
|
|
102
|
+
if (files.length > 0) {
|
|
103
|
+
parts.push(...mediaParts(profile, model, files, 'attachments'));
|
|
104
|
+
}
|
|
105
|
+
if (clips.length > 0) {
|
|
106
|
+
parts.push(...mediaParts(profile, model, clips, 'voice'));
|
|
107
|
+
}
|
|
108
|
+
return parts;
|
|
109
|
+
}
|
|
110
|
+
function resolveInputParts(profile, model, req) {
|
|
111
|
+
const parts = [];
|
|
112
|
+
const textPart = extractTextPart(profile, req);
|
|
113
|
+
if (textPart) {
|
|
114
|
+
parts.push(textPart);
|
|
115
|
+
}
|
|
116
|
+
parts.push(...extractMediaParts(profile, model, req));
|
|
117
|
+
return parts;
|
|
118
|
+
}
|
|
119
|
+
function assertImageGrounding(model, builtins) {
|
|
120
|
+
const spec = modelEntry(model).image;
|
|
121
|
+
if (spec && !spec.allowsGrounding && builtins.length > 0) {
|
|
122
|
+
throw new TheorumError(`Grounding tools are not valid on ${model}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export { assertImageGrounding, resolveImageFormat, resolveInputParts };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider adapters for THEORUM's `ModelProvider` contract.
|
|
3
|
+
*
|
|
4
|
+
* Use this entrypoint when a host app wants to bind THEORUM to Google
|
|
5
|
+
* Interactions, OpenRouter-compatible chat completions, or OpenRouter TTS.
|
|
6
|
+
* Credentials are always supplied by the host application.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import "../../_dnt.polyfills.js";
|
|
11
|
+
export type { OpenRouterConfig } from './openrouter.js';
|
|
12
|
+
export { createOpenRouterProvider, resolveOpenRouterApiKey, resolveOpenRouterModel, toOpenRouterPayload, } from './openrouter.js';
|
|
13
|
+
export { createInteractionsProvider } from './provider.js';
|
|
14
|
+
export type { OpenRouterTtsConfig } from './tts.js';
|
|
15
|
+
export { createOpenRouterTtsProvider, streamOpenRouterTts, wrapPcmAsWav, } from './tts.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider adapters for THEORUM's `ModelProvider` contract.
|
|
3
|
+
*
|
|
4
|
+
* Use this entrypoint when a host app wants to bind THEORUM to Google
|
|
5
|
+
* Interactions, OpenRouter-compatible chat completions, or OpenRouter TTS.
|
|
6
|
+
* Credentials are always supplied by the host application.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import "../../_dnt.polyfills.js";
|
|
11
|
+
export { createOpenRouterProvider, resolveOpenRouterApiKey, resolveOpenRouterModel, toOpenRouterPayload, } from './openrouter.js';
|
|
12
|
+
export { createInteractionsProvider } from './provider.js';
|
|
13
|
+
export { createOpenRouterTtsProvider, streamOpenRouterTts, wrapPcmAsWav, } from './tts.js';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenRouter payload builder.
|
|
3
|
+
*
|
|
4
|
+
* Converts THEORUM provider requests into OpenAI-compatible chat completion
|
|
5
|
+
* payloads for OpenRouter and compatible gateways.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import type { ModelId, ProviderCompleteRequest } from '../kernel/types.js';
|
|
10
|
+
/** Configuration supplied by the host app when creating OpenRouter providers. */
|
|
11
|
+
interface OpenRouterConfig {
|
|
12
|
+
apiKey?: string;
|
|
13
|
+
baseUrl?: string;
|
|
14
|
+
siteUrl?: string;
|
|
15
|
+
siteName?: string;
|
|
16
|
+
fetch?: typeof fetch;
|
|
17
|
+
modelMap?: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
/** Resolve a THEORUM model id to an OpenRouter model string. */
|
|
20
|
+
declare function resolveOpenRouterModel(modelId: ModelId | string, customMap?: Record<string, string>): string;
|
|
21
|
+
/** Convert a provider-neutral request into an OpenRouter chat completion payload. */
|
|
22
|
+
declare function toOpenRouterPayload(req: ProviderCompleteRequest, config: OpenRouterConfig): Record<string, unknown>;
|
|
23
|
+
export type { OpenRouterConfig };
|
|
24
|
+
export { resolveOpenRouterModel, toOpenRouterPayload };
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenRouter payload builder.
|
|
3
|
+
*
|
|
4
|
+
* Converts THEORUM provider requests into OpenAI-compatible chat completion
|
|
5
|
+
* payloads for OpenRouter and compatible gateways.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { CATALOG } from '../kernel/registry/catalog.js';
|
|
10
|
+
import { getStructured } from '../kernel/registry/schemas.js';
|
|
11
|
+
/** Resolve a THEORUM model id to an OpenRouter model string. */
|
|
12
|
+
function resolveOpenRouterModel(modelId, customMap) {
|
|
13
|
+
if (customMap?.[modelId]) {
|
|
14
|
+
return customMap[modelId];
|
|
15
|
+
}
|
|
16
|
+
const catalogEntry = CATALOG.models[modelId];
|
|
17
|
+
if (catalogEntry?.openRouterId) {
|
|
18
|
+
return catalogEntry.openRouterId;
|
|
19
|
+
}
|
|
20
|
+
if (catalogEntry?.apiId.includes('/')) {
|
|
21
|
+
return catalogEntry.apiId;
|
|
22
|
+
}
|
|
23
|
+
if (catalogEntry?.apiId) {
|
|
24
|
+
return `google/${catalogEntry.apiId}`;
|
|
25
|
+
}
|
|
26
|
+
return String(modelId);
|
|
27
|
+
}
|
|
28
|
+
function mapThinkingEffort(thinking) {
|
|
29
|
+
if (thinking === 'low') {
|
|
30
|
+
return 'low';
|
|
31
|
+
}
|
|
32
|
+
if (thinking === 'medium') {
|
|
33
|
+
return 'medium';
|
|
34
|
+
}
|
|
35
|
+
if (thinking === 'high') {
|
|
36
|
+
return 'high';
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
function wireAudioPart(part) {
|
|
41
|
+
let format = 'mp3';
|
|
42
|
+
if (part.mimeType.includes('wav')) {
|
|
43
|
+
format = 'wav';
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
type: 'input_audio',
|
|
47
|
+
input_audio: {
|
|
48
|
+
data: part.data,
|
|
49
|
+
format,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function wireMessageContent(parts) {
|
|
54
|
+
const isAllText = parts.every((p) => p.type === 'text');
|
|
55
|
+
if (isAllText) {
|
|
56
|
+
return parts
|
|
57
|
+
.map((p) => {
|
|
58
|
+
if (p.type === 'text') {
|
|
59
|
+
return p.text;
|
|
60
|
+
}
|
|
61
|
+
return '';
|
|
62
|
+
})
|
|
63
|
+
.join('\n');
|
|
64
|
+
}
|
|
65
|
+
return parts.map((part) => {
|
|
66
|
+
if (part.type === 'text') {
|
|
67
|
+
return { type: 'text', text: part.text };
|
|
68
|
+
}
|
|
69
|
+
if (part.type === 'image') {
|
|
70
|
+
return {
|
|
71
|
+
type: 'image_url',
|
|
72
|
+
image_url: { url: `data:${part.mimeType};base64,${part.data}` },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (part.type === 'audio') {
|
|
76
|
+
return wireAudioPart(part);
|
|
77
|
+
}
|
|
78
|
+
return { type: 'text', text: '' };
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function wireHistoryMessage(msg) {
|
|
82
|
+
let content = msg.content ?? '';
|
|
83
|
+
if (msg.parts && msg.parts.length > 0) {
|
|
84
|
+
content = wireMessageContent(msg.parts);
|
|
85
|
+
}
|
|
86
|
+
const wired = {
|
|
87
|
+
role: msg.role,
|
|
88
|
+
content,
|
|
89
|
+
};
|
|
90
|
+
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
|
91
|
+
wired.tool_calls = msg.tool_calls;
|
|
92
|
+
}
|
|
93
|
+
if (msg.tool_call_id) {
|
|
94
|
+
wired.tool_call_id = msg.tool_call_id;
|
|
95
|
+
}
|
|
96
|
+
if (msg.name) {
|
|
97
|
+
wired.name = msg.name;
|
|
98
|
+
}
|
|
99
|
+
return wired;
|
|
100
|
+
}
|
|
101
|
+
function wireTools(dynamicTools) {
|
|
102
|
+
if (!dynamicTools || dynamicTools.length === 0) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
return dynamicTools.map((t) => ({
|
|
106
|
+
type: 'function',
|
|
107
|
+
function: {
|
|
108
|
+
name: t.name,
|
|
109
|
+
description: t.description ?? '',
|
|
110
|
+
parameters: t.parameters ?? { type: 'object', properties: {} },
|
|
111
|
+
},
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
function buildMessages(req) {
|
|
115
|
+
const messages = [];
|
|
116
|
+
if (req.system) {
|
|
117
|
+
messages.push({ role: 'system', content: req.system });
|
|
118
|
+
}
|
|
119
|
+
if (req.history && req.history.length > 0) {
|
|
120
|
+
for (const h of req.history) {
|
|
121
|
+
messages.push(wireHistoryMessage(h));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (req.input.length > 0) {
|
|
125
|
+
messages.push({
|
|
126
|
+
role: 'user',
|
|
127
|
+
content: wireMessageContent(req.input),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return messages;
|
|
131
|
+
}
|
|
132
|
+
function resolveResponseFormat(structured) {
|
|
133
|
+
if (!structured) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
const spec = getStructured(structured);
|
|
137
|
+
if (!spec.jsonSchema) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
type: 'json_schema',
|
|
142
|
+
json_schema: {
|
|
143
|
+
name: String(structured),
|
|
144
|
+
strict: true,
|
|
145
|
+
schema: spec.jsonSchema,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** Convert a provider-neutral request into an OpenRouter chat completion payload. */
|
|
150
|
+
function toOpenRouterPayload(req, config) {
|
|
151
|
+
const model = resolveOpenRouterModel(req.model, config.modelMap);
|
|
152
|
+
const messages = buildMessages(req);
|
|
153
|
+
const payload = {
|
|
154
|
+
model,
|
|
155
|
+
stream: true,
|
|
156
|
+
messages,
|
|
157
|
+
temperature: req.temperature,
|
|
158
|
+
max_tokens: req.maxOutputTokens,
|
|
159
|
+
};
|
|
160
|
+
const effort = mapThinkingEffort(req.thinking);
|
|
161
|
+
if (effort) {
|
|
162
|
+
payload.reasoning = { effort };
|
|
163
|
+
}
|
|
164
|
+
const responseFormat = resolveResponseFormat(req.structured);
|
|
165
|
+
if (responseFormat) {
|
|
166
|
+
payload.response_format = responseFormat;
|
|
167
|
+
}
|
|
168
|
+
const tools = wireTools(req.dynamicTools);
|
|
169
|
+
if (tools.length > 0) {
|
|
170
|
+
payload.tools = tools;
|
|
171
|
+
}
|
|
172
|
+
if (req.builtins.includes('googleSearch')) {
|
|
173
|
+
payload.plugins = [{ id: 'web' }];
|
|
174
|
+
}
|
|
175
|
+
return payload;
|
|
176
|
+
}
|
|
177
|
+
export { resolveOpenRouterModel, toOpenRouterPayload };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenRouter-compatible streaming provider adapter.
|
|
3
|
+
*
|
|
4
|
+
* This module maps THEORUM requests to OpenAI-style chat completions, including
|
|
5
|
+
* reasoning deltas, tool calls, citations, structured output, and token usage.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import "../../_dnt.polyfills.js";
|
|
10
|
+
import type { ModelProvider } from '../kernel/types.js';
|
|
11
|
+
import { type OpenRouterConfig, resolveOpenRouterModel, toOpenRouterPayload } from './openrouter-payload.js';
|
|
12
|
+
/** Resolve an OpenRouter API key supplied explicitly by the host application. */
|
|
13
|
+
export declare function resolveOpenRouterApiKey(explicitKey?: string): string | undefined;
|
|
14
|
+
/** Create a `ModelProvider` backed by OpenRouter-compatible chat completions. */
|
|
15
|
+
declare function createOpenRouterProvider(config?: OpenRouterConfig): ModelProvider;
|
|
16
|
+
export type { OpenRouterConfig };
|
|
17
|
+
export { createOpenRouterProvider, resolveOpenRouterModel, toOpenRouterPayload };
|