theorum 0.1.14 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +178 -89
- package/esm/mod.d.ts +21 -11
- package/esm/mod.js +15 -8
- package/esm/src/cli/commands/bench.js +6 -3
- package/esm/src/cli/commands/fuzz-canary.d.ts +13 -0
- package/esm/src/cli/commands/fuzz-canary.js +195 -0
- package/esm/src/cli/commands/fuzz-guardrails.d.ts +3 -5
- package/esm/src/cli/commands/fuzz-guardrails.js +4 -581
- package/esm/src/cli/commands/profile.js +25 -14
- package/esm/src/cli/commands/run.d.ts +3 -0
- package/esm/src/cli/commands/run.js +22 -30
- package/esm/src/cli/commands/test.d.ts +10 -1
- package/esm/src/cli/commands/test.js +31 -31
- package/esm/src/cli/event-log.d.ts +19 -0
- package/esm/src/cli/event-log.js +147 -0
- package/esm/src/cli/index.js +45 -11
- package/esm/src/cli/matrix/synthesizer.d.ts +7 -9
- package/esm/src/cli/matrix/synthesizer.js +32 -106
- package/esm/src/guardrails/canary-gate.d.ts +20 -0
- package/esm/src/guardrails/canary-gate.js +31 -0
- package/esm/src/guardrails/canary.d.ts +26 -0
- package/esm/src/guardrails/canary.js +139 -0
- package/esm/src/guardrails/corpus/canary-egress-attacks.d.ts +16 -0
- package/esm/src/guardrails/corpus/canary-egress-attacks.js +151 -0
- package/esm/src/guardrails/corpus/fuzz-inbound.d.ts +11 -0
- package/esm/src/guardrails/corpus/fuzz-inbound.js +216 -0
- package/esm/src/guardrails/corpus/inbound-payloads.d.ts +10 -0
- package/esm/src/guardrails/corpus/inbound-payloads.js +124 -0
- package/esm/src/guardrails/corpus/live-attacks.d.ts +20 -0
- package/esm/src/guardrails/corpus/live-attacks.js +232 -0
- package/esm/src/guardrails/corpus/mod.d.ts +13 -0
- package/esm/src/guardrails/corpus/mod.js +11 -0
- package/esm/src/guardrails/corpus/secrets.d.ts +16 -0
- package/esm/src/guardrails/corpus/secrets.js +16 -0
- package/esm/src/guardrails/corpus/strings.d.ts +27 -0
- package/esm/src/guardrails/corpus/strings.js +33 -0
- package/esm/src/guardrails/corpus/types.d.ts +37 -0
- package/esm/src/guardrails/corpus/types.js +6 -0
- package/esm/src/guardrails/egress.d.ts +9 -0
- package/esm/src/guardrails/egress.js +36 -0
- package/esm/src/guardrails/error.js +6 -4
- package/esm/src/guardrails/injection.js +28 -5
- package/esm/src/guardrails/live-outbound-gate.d.ts +39 -0
- package/esm/src/guardrails/live-outbound-gate.js +180 -0
- package/esm/src/guardrails/mod.d.ts +13 -4
- package/esm/src/guardrails/mod.js +10 -4
- package/esm/src/guardrails/quota.js +1 -1
- package/esm/src/guardrails/sanitize.d.ts +11 -15
- package/esm/src/guardrails/sanitize.js +40 -49
- package/esm/src/guardrails/sensitive.js +2 -1
- package/esm/src/guardrails/testing.d.ts +10 -0
- package/esm/src/guardrails/testing.js +9 -0
- package/esm/src/host/client-turn.d.ts +19 -0
- package/esm/src/host/client-turn.js +32 -0
- package/esm/src/host/mod.d.ts +5 -3
- package/esm/src/host/mod.js +4 -3
- package/esm/src/interface/blocks.d.ts +20 -0
- package/esm/src/interface/blocks.js +180 -0
- package/esm/src/interface/from-profile.d.ts +14 -0
- package/esm/src/interface/from-profile.js +104 -0
- package/esm/src/interface/inputs.d.ts +8 -0
- package/esm/src/interface/inputs.js +127 -0
- package/esm/src/interface/mod.d.ts +10 -0
- package/esm/src/interface/mod.js +9 -0
- package/esm/src/interface/types.d.ts +153 -0
- package/esm/src/interface/types.js +13 -0
- package/esm/src/kernel/engine/delta.d.ts +24 -2
- package/esm/src/kernel/engine/delta.js +519 -29
- package/esm/src/kernel/engine/live-inbound.d.ts +9 -0
- package/esm/src/kernel/engine/live-inbound.js +17 -0
- package/esm/src/kernel/engine/repair.js +1 -1
- package/esm/src/kernel/engine/runner/gates.d.ts +1 -1
- package/esm/src/kernel/engine/runner/gates.js +7 -7
- package/esm/src/kernel/engine/runner/mod.js +13 -13
- package/esm/src/kernel/engine/runner/state.d.ts +7 -0
- package/esm/src/kernel/engine/runner/steps.d.ts +1 -1
- package/esm/src/kernel/engine/runner/steps.js +171 -37
- package/esm/src/kernel/engine/runner/stream.d.ts +1 -2
- package/esm/src/kernel/engine/runner/stream.js +66 -29
- package/esm/src/kernel/engine/session/mod.d.ts +23 -0
- package/esm/src/kernel/engine/session/mod.js +205 -0
- package/esm/src/kernel/mod.d.ts +9 -10
- package/esm/src/kernel/mod.js +6 -8
- package/esm/src/kernel/registry/attachments.d.ts +4 -2
- package/esm/src/kernel/registry/attachments.js +6 -3
- package/esm/src/kernel/registry/catalog.d.ts +3 -16
- package/esm/src/kernel/registry/catalog.js +5 -81
- package/esm/src/kernel/registry/ingress.d.ts +9 -4
- package/esm/src/kernel/registry/ingress.js +73 -63
- package/esm/src/kernel/registry/profiles.d.ts +35 -12
- package/esm/src/kernel/registry/profiles.js +159 -66
- package/esm/src/kernel/registry/provider-request.js +11 -7
- package/esm/src/kernel/registry/resolve.d.ts +5 -6
- package/esm/src/kernel/registry/resolve.js +100 -70
- package/esm/src/kernel/registry/vault.d.ts +12 -7
- package/esm/src/kernel/registry/vault.js +30 -8
- package/esm/src/kernel/schema.d.ts +145 -0
- package/esm/src/kernel/schema.js +462 -0
- package/esm/src/kernel/stop.d.ts +15 -10
- package/esm/src/kernel/stop.js +5 -5
- package/esm/src/kernel/tools/execute.d.ts +45 -0
- package/esm/src/kernel/tools/execute.js +363 -0
- package/esm/src/kernel/tools/harness.d.ts +8 -0
- package/esm/src/kernel/tools/harness.js +44 -0
- package/esm/src/kernel/tools/invoke.d.ts +10 -0
- package/esm/src/kernel/tools/invoke.js +83 -0
- package/esm/src/kernel/tools/mod.d.ts +11 -0
- package/esm/src/kernel/tools/mod.js +10 -0
- package/esm/src/kernel/tools/project.d.ts +12 -0
- package/esm/src/kernel/tools/project.js +34 -0
- package/esm/src/kernel/tools/registry.d.ts +23 -0
- package/esm/src/kernel/tools/registry.js +71 -0
- package/esm/src/kernel/tools/resolve.d.ts +29 -0
- package/esm/src/kernel/tools/resolve.js +262 -0
- package/esm/src/kernel/tools/schema.d.ts +13 -0
- package/esm/src/kernel/tools/schema.js +159 -0
- package/esm/src/kernel/tools/types.d.ts +216 -0
- package/esm/src/kernel/tools/types.js +9 -0
- package/esm/src/kernel/types.d.ts +338 -193
- package/esm/src/observability/trace-attach.d.ts +6 -4
- package/esm/src/observability/trace-attach.js +39 -25
- package/esm/src/observability/trace-record.d.ts +11 -9
- package/esm/src/observability/trace-record.js +36 -23
- package/esm/src/observability/trace-usage.d.ts +10 -3
- package/esm/src/observability/trace-usage.js +70 -17
- package/esm/src/observability/trace.d.ts +9 -1
- package/esm/src/observability/trace.js +11 -3
- package/esm/src/presets/google/speech-voices.d.ts +11 -0
- package/esm/src/presets/google/speech-voices.js +41 -0
- package/esm/src/presets/google.d.ts +36 -24
- package/esm/src/presets/google.js +50 -63
- package/esm/src/presets/mod.d.ts +2 -2
- package/esm/src/presets/mod.js +1 -1
- package/esm/src/providers/create-provider.d.ts +14 -14
- package/esm/src/providers/create-provider.js +55 -23
- package/esm/src/providers/google/interactions/framing.d.ts +15 -0
- package/esm/src/providers/google/interactions/framing.js +208 -0
- package/esm/src/providers/google/interactions/mod.d.ts +7 -0
- package/esm/src/providers/google/interactions/mod.js +7 -0
- package/esm/src/providers/google/interactions/stream.d.ts +83 -0
- package/esm/src/providers/google/interactions/stream.js +588 -0
- package/esm/src/providers/google/keys.d.ts +26 -0
- package/esm/src/providers/{keys.js → google/keys.js} +19 -31
- package/esm/src/providers/google/live/framing.d.ts +46 -0
- package/esm/src/providers/google/live/framing.js +515 -0
- package/esm/src/providers/google/live/openapi-schema.d.ts +6 -0
- package/esm/src/providers/google/live/openapi-schema.js +45 -0
- package/esm/src/providers/google/live/session.d.ts +25 -0
- package/esm/src/providers/google/live/session.js +134 -0
- package/esm/src/providers/google/live/stream.d.ts +36 -0
- package/esm/src/providers/google/live/stream.js +201 -0
- package/esm/src/providers/google/urls.d.ts +6 -0
- package/esm/src/providers/google/urls.js +6 -0
- package/esm/src/providers/local/local.d.ts +30 -0
- package/esm/src/providers/{local.js → local/local.js} +66 -126
- package/esm/src/providers/local/mod.d.ts +9 -0
- package/esm/src/providers/local/mod.js +9 -0
- package/esm/src/providers/mod.d.ts +6 -3
- package/esm/src/providers/mod.js +3 -1
- package/esm/src/providers/openrouter/chat.d.ts +87 -0
- package/esm/src/providers/{openrouter.js → openrouter/chat.js} +70 -229
- package/esm/src/providers/openrouter/image.d.ts +34 -0
- package/esm/src/providers/openrouter/image.js +286 -0
- package/esm/src/providers/openrouter/openai/chat-payload.d.ts +24 -0
- package/esm/src/providers/openrouter/openai/chat-payload.js +60 -0
- package/esm/src/providers/openrouter/openai/compat.d.ts +47 -0
- package/esm/src/providers/openrouter/openai/compat.js +194 -0
- package/esm/src/providers/openrouter/openai/image-payload.d.ts +18 -0
- package/esm/src/providers/openrouter/openai/image-payload.js +84 -0
- package/esm/src/providers/openrouter/openai/sdk-messages.d.ts +22 -0
- package/esm/src/providers/openrouter/openai/sdk-messages.js +95 -0
- package/esm/src/providers/openrouter/resolve-api-key.d.ts +9 -0
- package/esm/src/providers/openrouter/resolve-api-key.js +24 -0
- package/esm/src/providers/openrouter/speech.d.ts +23 -0
- package/esm/src/providers/{speech.js → openrouter/speech.js} +32 -55
- package/esm/src/providers/probe.d.ts +1 -0
- package/esm/src/providers/probe.js +22 -0
- package/esm/src/providers/shared/pcm.d.ts +12 -0
- package/esm/src/providers/{pcm.js → shared/pcm.js} +16 -3
- package/esm/src/providers/shared/sse.d.ts +18 -0
- package/esm/src/providers/shared/sse.js +87 -0
- package/esm/src/providers/shared/tool-args.d.ts +17 -0
- package/esm/src/providers/shared/tool-args.js +45 -0
- package/esm/src/providers/shared/upstream-tap.d.ts +5 -0
- package/esm/src/providers/{google-tap.js → shared/upstream-tap.js} +4 -7
- package/esm/src/providers/shared/upstream-tape.d.ts +6 -0
- package/esm/src/providers/{gemini-tape.js → shared/upstream-tape.js} +12 -22
- package/esm/src/providers/types.d.ts +27 -0
- package/esm/src/providers/types.js +1 -0
- package/package.json +11 -7
- package/docs/cli.md +0 -97
- package/docs/guardrails.md +0 -178
- package/docs/host.md +0 -97
- package/docs/kernel.md +0 -404
- package/docs/observability.md +0 -105
- package/docs/openrouter.md +0 -125
- package/docs/presets-google.md +0 -91
- package/docs/presets.md +0 -88
- package/docs/providers.md +0 -201
- package/docs/streaming.md +0 -96
- package/esm/src/kernel/engine/boundary.d.ts +0 -10
- package/esm/src/kernel/engine/boundary.js +0 -55
- package/esm/src/kernel/engine/runner/tools.d.ts +0 -13
- package/esm/src/kernel/engine/runner/tools.js +0 -198
- package/esm/src/kernel/registry/tools.d.ts +0 -12
- package/esm/src/kernel/registry/tools.js +0 -36
- package/esm/src/providers/expose-for-tests.d.ts +0 -1
- package/esm/src/providers/expose-for-tests.js +0 -25
- package/esm/src/providers/gemini-tape.d.ts +0 -2
- package/esm/src/providers/google-tap.d.ts +0 -3
- package/esm/src/providers/interactions.d.ts +0 -5
- package/esm/src/providers/interactions.js +0 -169
- package/esm/src/providers/keys.d.ts +0 -19
- package/esm/src/providers/local.d.ts +0 -29
- package/esm/src/providers/openrouter-mod.d.ts +0 -13
- package/esm/src/providers/openrouter-mod.js +0 -12
- package/esm/src/providers/openrouter-payload.d.ts +0 -39
- package/esm/src/providers/openrouter-payload.js +0 -195
- package/esm/src/providers/openrouter.d.ts +0 -15
- package/esm/src/providers/pcm.d.ts +0 -7
- package/esm/src/providers/provider.d.ts +0 -15
- package/esm/src/providers/provider.js +0 -176
- package/esm/src/providers/speech.d.ts +0 -23
- package/esm/src/providers/sse.d.ts +0 -7
- package/esm/src/providers/sse.js +0 -55
- package/esm/src/streaming/mod.d.ts +0 -9
- package/esm/src/streaming/mod.js +0 -8
- /package/esm/src/{streaming → host}/readStreamingJsonStringField.d.ts +0 -0
- /package/esm/src/{streaming → host}/readStreamingJsonStringField.js +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini Live / OpenAPI Schema 3.0 style type uppercasing for function parameters.
|
|
3
|
+
* JSON Schema uses lowercase `object`/`string`; Gemini Live expects `OBJECT`/`STRING`.
|
|
4
|
+
*/
|
|
5
|
+
function convertGeminiSchemaType(value) {
|
|
6
|
+
if (typeof value === 'string')
|
|
7
|
+
return { type: value.toUpperCase() };
|
|
8
|
+
if (!Array.isArray(value))
|
|
9
|
+
return { type: toGeminiOpenApiSchema(value) };
|
|
10
|
+
const schemaTypes = value.filter((item) => typeof item === 'string');
|
|
11
|
+
const nonNullTypes = schemaTypes.filter((item) => item !== 'null');
|
|
12
|
+
const converted = {};
|
|
13
|
+
if (schemaTypes.includes('null'))
|
|
14
|
+
converted.nullable = true;
|
|
15
|
+
if (nonNullTypes.length === 1)
|
|
16
|
+
converted.type = nonNullTypes[0].toUpperCase();
|
|
17
|
+
if (nonNullTypes.length > 1) {
|
|
18
|
+
converted.anyOf = nonNullTypes.map((item) => ({ type: item.toUpperCase() }));
|
|
19
|
+
}
|
|
20
|
+
return converted;
|
|
21
|
+
}
|
|
22
|
+
function convertGeminiSchemaObject(obj) {
|
|
23
|
+
const result = {};
|
|
24
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
25
|
+
if (key === 'type') {
|
|
26
|
+
Object.assign(result, convertGeminiSchemaType(value));
|
|
27
|
+
}
|
|
28
|
+
else if (key === 'enum' && Array.isArray(value)) {
|
|
29
|
+
result.enum = value.filter((item) => item !== null);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
result[key] = toGeminiOpenApiSchema(value);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
/** Convert JSON-Schema-ish parameters to Gemini Live OpenAPI Schema shape. */
|
|
38
|
+
export function toGeminiOpenApiSchema(value) {
|
|
39
|
+
if (Array.isArray(value))
|
|
40
|
+
return value.map(toGeminiOpenApiSchema);
|
|
41
|
+
if (value !== null && typeof value === 'object') {
|
|
42
|
+
return convertGeminiSchemaObject(value);
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Gemini Live session transport — long-lived BidiGenerateContent WebSocket.
|
|
3
|
+
*
|
|
4
|
+
* `turnComplete` is a conversational turn boundary, not session teardown.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
import type { ProviderCompleteRequest } from '../../../kernel/types.js';
|
|
9
|
+
import { type GeminiTransport } from '../keys.js';
|
|
10
|
+
import { type SessionQueueItem } from './stream.js';
|
|
11
|
+
export interface GoogleLiveConnection {
|
|
12
|
+
/** Send a framed JSON payload upstream. */
|
|
13
|
+
send(payload: string): void;
|
|
14
|
+
/** Drain session batches until the socket closes or errors. */
|
|
15
|
+
batches(): AsyncGenerator<SessionQueueItem>;
|
|
16
|
+
close(code?: number, reason?: string): void;
|
|
17
|
+
}
|
|
18
|
+
export type OpenLiveWebSocket = (url: string) => Promise<WebSocket>;
|
|
19
|
+
/**
|
|
20
|
+
* Open a long-lived Gemini Live WebSocket after setup handshake.
|
|
21
|
+
* Callers own send / batch drain / close — typically via `runSession`.
|
|
22
|
+
*
|
|
23
|
+
* @param openWebSocket Host override for Cloudflare fetch-upgrade (etc.).
|
|
24
|
+
*/
|
|
25
|
+
export declare function openGoogleLiveSession(req: ProviderCompleteRequest, transport: GeminiTransport, openWebSocket?: OpenLiveWebSocket): Promise<GoogleLiveConnection>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Gemini Live session transport — long-lived BidiGenerateContent WebSocket.
|
|
3
|
+
*
|
|
4
|
+
* `turnComplete` is a conversational turn boundary, not session teardown.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
import { isAbortError, TheorumError } from '../../../guardrails/error.js';
|
|
9
|
+
import { requireKey } from '../keys.js';
|
|
10
|
+
import { buildGeminiLiveWebSocketUrl } from './framing.js';
|
|
11
|
+
import { attachLiveSessionHandlers, createLiveQueue, performLiveSetup, sendInitialPayloads, } from './stream.js';
|
|
12
|
+
function defaultOpenWebSocket(url) {
|
|
13
|
+
return Promise.resolve(new WebSocket(url));
|
|
14
|
+
}
|
|
15
|
+
function attachAbort(ws, liveQueue, signal) {
|
|
16
|
+
const onAbort = () => {
|
|
17
|
+
if (!liveQueue.isClosed()) {
|
|
18
|
+
liveQueue.close();
|
|
19
|
+
try {
|
|
20
|
+
ws.close(1000, 'aborted');
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Ignore
|
|
24
|
+
}
|
|
25
|
+
liveQueue.push({
|
|
26
|
+
type: 'error',
|
|
27
|
+
error: new DOMException('The operation was aborted.', 'AbortError'),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
if (!signal) {
|
|
32
|
+
return () => { };
|
|
33
|
+
}
|
|
34
|
+
if (signal.aborted) {
|
|
35
|
+
onAbort();
|
|
36
|
+
return () => { };
|
|
37
|
+
}
|
|
38
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
39
|
+
return () => signal.removeEventListener('abort', onAbort);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Open a long-lived Gemini Live WebSocket after setup handshake.
|
|
43
|
+
* Callers own send / batch drain / close — typically via `runSession`.
|
|
44
|
+
*
|
|
45
|
+
* @param openWebSocket Host override for Cloudflare fetch-upgrade (etc.).
|
|
46
|
+
*/
|
|
47
|
+
export async function openGoogleLiveSession(req, transport, openWebSocket = defaultOpenWebSocket) {
|
|
48
|
+
if (!req.keySlot) {
|
|
49
|
+
throw new TheorumError('Request requires keySlot');
|
|
50
|
+
}
|
|
51
|
+
const apiKey = requireKey(transport.vault, req.keySlot);
|
|
52
|
+
const wsUrl = buildGeminiLiveWebSocketUrl(apiKey);
|
|
53
|
+
let ws;
|
|
54
|
+
try {
|
|
55
|
+
ws = await openWebSocket(wsUrl);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
59
|
+
}
|
|
60
|
+
const liveQueue = createLiveQueue();
|
|
61
|
+
const detachAbort = attachAbort(ws, liveQueue, req.signal);
|
|
62
|
+
if (req.signal?.aborted) {
|
|
63
|
+
detachAbort();
|
|
64
|
+
try {
|
|
65
|
+
ws.close();
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Ignore
|
|
69
|
+
}
|
|
70
|
+
throw new DOMException('The operation was aborted.', 'AbortError');
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
await performLiveSetup(ws, req);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
detachAbort();
|
|
77
|
+
try {
|
|
78
|
+
ws.close();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Ignore
|
|
82
|
+
}
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
attachLiveSessionHandlers(ws, liveQueue);
|
|
86
|
+
sendInitialPayloads(ws, req);
|
|
87
|
+
return {
|
|
88
|
+
send(payload) {
|
|
89
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
90
|
+
ws.send(payload);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
async *batches() {
|
|
94
|
+
try {
|
|
95
|
+
while (true) {
|
|
96
|
+
const item = await liveQueue.next();
|
|
97
|
+
if (!item)
|
|
98
|
+
break;
|
|
99
|
+
if (item.type === 'error' && isAbortError(item.error)) {
|
|
100
|
+
throw item.error;
|
|
101
|
+
}
|
|
102
|
+
yield item;
|
|
103
|
+
if (item.type === 'closed' || item.type === 'error') {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
detachAbort();
|
|
110
|
+
liveQueue.close();
|
|
111
|
+
try {
|
|
112
|
+
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
113
|
+
ws.close(1000, 'session-closed');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Ignore
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
close(code = 1000, reason = 'session-closed') {
|
|
122
|
+
detachAbort();
|
|
123
|
+
liveQueue.close();
|
|
124
|
+
try {
|
|
125
|
+
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
126
|
+
ws.close(code, reason);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Ignore
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Gemini Live WebSocket transport helpers.
|
|
3
|
+
*
|
|
4
|
+
* Used by `openGoogleLiveSession` / `runSession`. No ModelProvider.complete() path —
|
|
5
|
+
* live is session-scoped, not turn-scoped.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import type { ProviderCompleteRequest, TurnEvent } from '../../../kernel/types.js';
|
|
10
|
+
export type LiveTurnPhase = 'streaming' | 'complete' | 'abort';
|
|
11
|
+
export type SessionQueueItem = {
|
|
12
|
+
type: 'batch';
|
|
13
|
+
events: TurnEvent[];
|
|
14
|
+
turnPhase: LiveTurnPhase;
|
|
15
|
+
} | {
|
|
16
|
+
type: 'error';
|
|
17
|
+
error: Error;
|
|
18
|
+
} | {
|
|
19
|
+
type: 'closed';
|
|
20
|
+
};
|
|
21
|
+
export declare function readMessageData(data: unknown): Promise<string>;
|
|
22
|
+
export declare function readGeminiLiveErrorMessage(message: Record<string, unknown>): string | null;
|
|
23
|
+
export declare function performLiveSetup(ws: WebSocket, req: ProviderCompleteRequest): Promise<void>;
|
|
24
|
+
export type LiveSocketSender = {
|
|
25
|
+
send(data: string): void;
|
|
26
|
+
};
|
|
27
|
+
export declare function sendInitialPayloads(ws: LiveSocketSender, req: ProviderCompleteRequest): void;
|
|
28
|
+
export interface LiveQueue {
|
|
29
|
+
push: (item: SessionQueueItem) => void;
|
|
30
|
+
next: () => Promise<SessionQueueItem | undefined>;
|
|
31
|
+
close: () => void;
|
|
32
|
+
isClosed: () => boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare function createLiveQueue(): LiveQueue;
|
|
35
|
+
/** Attach handlers that keep the socket open across conversational turns. */
|
|
36
|
+
export declare function attachLiveSessionHandlers(ws: WebSocket, liveQueue: LiveQueue): void;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Gemini Live WebSocket transport helpers.
|
|
3
|
+
*
|
|
4
|
+
* Used by `openGoogleLiveSession` / `runSession`. No ModelProvider.complete() path —
|
|
5
|
+
* live is session-scoped, not turn-scoped.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { TheorumError } from '../../../guardrails/error.js';
|
|
10
|
+
import { buildGeminiLiveClientContent, buildGeminiLiveRealtimeInput, buildGeminiLiveSetupMessage, foldGeminiLiveServerMessage, parseGeminiLiveMessage, } from './framing.js';
|
|
11
|
+
const SETUP_TIMEOUT_MS = 20_000;
|
|
12
|
+
export async function readMessageData(data) {
|
|
13
|
+
if (typeof data === 'string')
|
|
14
|
+
return data;
|
|
15
|
+
if (data instanceof ArrayBuffer) {
|
|
16
|
+
return new TextDecoder().decode(data);
|
|
17
|
+
}
|
|
18
|
+
if (typeof Blob !== 'undefined' && data instanceof Blob) {
|
|
19
|
+
return await data.text();
|
|
20
|
+
}
|
|
21
|
+
return String(data);
|
|
22
|
+
}
|
|
23
|
+
export function readGeminiLiveErrorMessage(message) {
|
|
24
|
+
const error = message.error;
|
|
25
|
+
if (!error || typeof error !== 'object')
|
|
26
|
+
return null;
|
|
27
|
+
const record = error;
|
|
28
|
+
if (typeof record.message === 'string' && record.message.length > 0) {
|
|
29
|
+
const status = typeof record.status === 'string' ? record.status : null;
|
|
30
|
+
return status ? `${status}: ${record.message}` : record.message;
|
|
31
|
+
}
|
|
32
|
+
return 'Gemini returned an error during live session.';
|
|
33
|
+
}
|
|
34
|
+
export function performLiveSetup(ws, req) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
let setupResolved = false;
|
|
37
|
+
const timeout = setTimeout(() => {
|
|
38
|
+
if (!setupResolved) {
|
|
39
|
+
setupResolved = true;
|
|
40
|
+
reject(new TheorumError(`Gemini Live setup timed out after ${SETUP_TIMEOUT_MS}ms`));
|
|
41
|
+
}
|
|
42
|
+
}, SETUP_TIMEOUT_MS);
|
|
43
|
+
ws.onopen = () => {
|
|
44
|
+
try {
|
|
45
|
+
const setupMsg = buildGeminiLiveSetupMessage(req);
|
|
46
|
+
ws.send(JSON.stringify(setupMsg));
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
clearTimeout(timeout);
|
|
50
|
+
setupResolved = true;
|
|
51
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
ws.onerror = () => {
|
|
55
|
+
clearTimeout(timeout);
|
|
56
|
+
if (!setupResolved) {
|
|
57
|
+
setupResolved = true;
|
|
58
|
+
reject(new TheorumError('Gemini Live WebSocket error during setup'));
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
ws.onclose = (evt) => {
|
|
62
|
+
clearTimeout(timeout);
|
|
63
|
+
if (!setupResolved) {
|
|
64
|
+
setupResolved = true;
|
|
65
|
+
reject(new TheorumError(`Gemini Live WebSocket closed during setup (${evt.code}: ${evt.reason})`));
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const initialMessageHandler = async (evt) => {
|
|
69
|
+
const rawText = await readMessageData(evt.data);
|
|
70
|
+
const parsed = parseGeminiLiveMessage(rawText);
|
|
71
|
+
if (!parsed.ok) {
|
|
72
|
+
if (parsed.reason === 'empty')
|
|
73
|
+
return;
|
|
74
|
+
clearTimeout(timeout);
|
|
75
|
+
setupResolved = true;
|
|
76
|
+
reject(new TheorumError('malformed Gemini Live message during setup'));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const errMsg = readGeminiLiveErrorMessage(parsed.value);
|
|
80
|
+
if (errMsg) {
|
|
81
|
+
clearTimeout(timeout);
|
|
82
|
+
setupResolved = true;
|
|
83
|
+
reject(new TheorumError(errMsg));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (parsed.value.setupComplete) {
|
|
87
|
+
clearTimeout(timeout);
|
|
88
|
+
setupResolved = true;
|
|
89
|
+
ws.removeEventListener('message', initialMessageHandler);
|
|
90
|
+
resolve();
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
ws.addEventListener('message', initialMessageHandler);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
export function sendInitialPayloads(ws, req) {
|
|
97
|
+
if (req.history && req.history.length > 0) {
|
|
98
|
+
const historyMsg = buildGeminiLiveClientContent(req.history);
|
|
99
|
+
if (historyMsg) {
|
|
100
|
+
ws.send(JSON.stringify(historyMsg));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (req.input && req.input.length > 0) {
|
|
104
|
+
for (const part of req.input) {
|
|
105
|
+
const inputMsg = buildGeminiLiveRealtimeInput(part);
|
|
106
|
+
ws.send(JSON.stringify(inputMsg));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export function createLiveQueue() {
|
|
111
|
+
const queue = [];
|
|
112
|
+
let notify = null;
|
|
113
|
+
let closed = false;
|
|
114
|
+
return {
|
|
115
|
+
push(item) {
|
|
116
|
+
queue.push(item);
|
|
117
|
+
if (notify) {
|
|
118
|
+
const fn = notify;
|
|
119
|
+
notify = null;
|
|
120
|
+
fn();
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
async next() {
|
|
124
|
+
while (queue.length === 0) {
|
|
125
|
+
if (closed)
|
|
126
|
+
return undefined;
|
|
127
|
+
await new Promise((resolve) => {
|
|
128
|
+
notify = resolve;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return queue.shift();
|
|
132
|
+
},
|
|
133
|
+
close() {
|
|
134
|
+
closed = true;
|
|
135
|
+
if (notify) {
|
|
136
|
+
const fn = notify;
|
|
137
|
+
notify = null;
|
|
138
|
+
fn();
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
isClosed() {
|
|
142
|
+
return closed;
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function turnPhaseFromMessage(message, events) {
|
|
147
|
+
const interrupted = events.some((ev) => ev.type === 'done' && ev.interrupted === true);
|
|
148
|
+
if (interrupted)
|
|
149
|
+
return 'abort';
|
|
150
|
+
const serverContent = message.serverContent;
|
|
151
|
+
if (serverContent?.turnComplete)
|
|
152
|
+
return 'complete';
|
|
153
|
+
return 'streaming';
|
|
154
|
+
}
|
|
155
|
+
/** Attach handlers that keep the socket open across conversational turns. */
|
|
156
|
+
export function attachLiveSessionHandlers(ws, liveQueue) {
|
|
157
|
+
ws.onmessage = async (evt) => {
|
|
158
|
+
try {
|
|
159
|
+
const rawText = await readMessageData(evt.data);
|
|
160
|
+
const parsed = parseGeminiLiveMessage(rawText);
|
|
161
|
+
if (!parsed.ok) {
|
|
162
|
+
if (parsed.reason === 'empty')
|
|
163
|
+
return;
|
|
164
|
+
liveQueue.push({
|
|
165
|
+
type: 'error',
|
|
166
|
+
error: new TheorumError('malformed Gemini Live message'),
|
|
167
|
+
});
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (parsed.value.setupComplete)
|
|
171
|
+
return;
|
|
172
|
+
const errMsg = readGeminiLiveErrorMessage(parsed.value);
|
|
173
|
+
if (errMsg) {
|
|
174
|
+
liveQueue.push({ type: 'error', error: new TheorumError(errMsg) });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const events = foldGeminiLiveServerMessage(parsed.value);
|
|
178
|
+
const turnPhase = turnPhaseFromMessage(parsed.value, events);
|
|
179
|
+
if (events.length > 0 || turnPhase !== 'streaming') {
|
|
180
|
+
liveQueue.push({ type: 'batch', events, turnPhase });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
liveQueue.push({
|
|
185
|
+
type: 'error',
|
|
186
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
ws.onerror = () => {
|
|
191
|
+
if (!liveQueue.isClosed()) {
|
|
192
|
+
liveQueue.push({ type: 'error', error: new TheorumError('Gemini Live WebSocket error') });
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
ws.onclose = () => {
|
|
196
|
+
if (!liveQueue.isClosed()) {
|
|
197
|
+
liveQueue.push({ type: 'closed' });
|
|
198
|
+
liveQueue.close();
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Google Interactions API endpoints. */
|
|
2
|
+
declare const INTERACTIONS_URL = "https://generativelanguage.googleapis.com/v1beta/interactions?alt=sse";
|
|
3
|
+
declare const INTERACTIONS_JSON_URL = "https://generativelanguage.googleapis.com/v1beta/interactions";
|
|
4
|
+
/** Google Gemini Live WebSocket endpoint. */
|
|
5
|
+
declare const GEMINI_LIVE_WS_URL = "wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent";
|
|
6
|
+
export { GEMINI_LIVE_WS_URL, INTERACTIONS_JSON_URL, INTERACTIONS_URL };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Google Interactions API endpoints. */
|
|
2
|
+
const INTERACTIONS_URL = 'https://generativelanguage.googleapis.com/v1beta/interactions?alt=sse';
|
|
3
|
+
const INTERACTIONS_JSON_URL = 'https://generativelanguage.googleapis.com/v1beta/interactions';
|
|
4
|
+
/** Google Gemini Live WebSocket endpoint. */
|
|
5
|
+
const GEMINI_LIVE_WS_URL = 'wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent';
|
|
6
|
+
export { GEMINI_LIVE_WS_URL, INTERACTIONS_JSON_URL, INTERACTIONS_URL };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local provider adapter for OpenAI-compatible endpoints (Ollama, llama.cpp,
|
|
3
|
+
* vLLM, LM Studio, etc.).
|
|
4
|
+
*
|
|
5
|
+
* Streams SSE from `/v1/chat/completions`, accumulates tool calls, and yields
|
|
6
|
+
* normalized `TurnEvent` objects. No external SDK dependency — raw fetch + SSE.
|
|
7
|
+
*
|
|
8
|
+
* Wire-format message building delegates to the shared `openai/compat` module.
|
|
9
|
+
* SSE parsing delegates to the shared `parseSseStream` from `sse.ts`.
|
|
10
|
+
*
|
|
11
|
+
* Hosts pass `baseUrl` explicitly. THEORUM does not read `OLLAMA_HOST` or other
|
|
12
|
+
* environment variables (see docs/contracts/providers.md).
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
import type { ModelProvider, TurnEvent } from '../../kernel/types.js';
|
|
17
|
+
import { buildChatMessages, wireTools } from '../openrouter/openai/compat.js';
|
|
18
|
+
import type { LocalProviderConfig } from '../types.js';
|
|
19
|
+
/** Default OpenAI-compat base when the host omits `baseUrl` (Ollama's default port). */
|
|
20
|
+
export declare const DEFAULT_LOCAL_BASE_URL = "http://127.0.0.1:11434";
|
|
21
|
+
export type PendingToolCall = {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
args: string;
|
|
25
|
+
};
|
|
26
|
+
export declare function resolveBaseUrl(config?: LocalProviderConfig): string;
|
|
27
|
+
export declare function flushPending(pending: Map<number, PendingToolCall>): TurnEvent[];
|
|
28
|
+
/** Create a `ModelProvider` for a local OpenAI-compatible server (Ollama, llama.cpp, vLLM, LM Studio). */
|
|
29
|
+
declare function createLocalProvider(config?: LocalProviderConfig): ModelProvider;
|
|
30
|
+
export { buildChatMessages as historyToWire, createLocalProvider, wireTools as toolsToWire };
|