theorum 0.1.15 → 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.
- package/README.md +241 -98
- package/esm/mod.d.ts +57 -28
- package/esm/mod.js +43 -23
- package/esm/src/cli/commands/bench.js +18 -18
- package/esm/src/cli/commands/fuzz-canary.d.ts +13 -0
- package/esm/src/cli/commands/fuzz-canary.js +191 -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/guardrails-eval.d.ts +14 -0
- package/esm/src/cli/commands/guardrails-eval.js +15 -0
- package/esm/src/cli/commands/profile.js +35 -15
- package/esm/src/cli/commands/run.d.ts +3 -0
- package/esm/src/cli/commands/run.js +23 -32
- package/esm/src/cli/commands/test.d.ts +10 -1
- package/esm/src/cli/commands/test.js +34 -34
- 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 +57 -11
- package/esm/src/cli/matrix/synthesizer.d.ts +10 -12
- package/esm/src/cli/matrix/synthesizer.js +45 -118
- package/esm/src/guardrails/canary-gate.d.ts +21 -0
- package/esm/src/guardrails/canary-gate.js +32 -0
- package/esm/src/guardrails/canary.d.ts +34 -0
- package/esm/src/guardrails/canary.js +150 -0
- package/esm/src/guardrails/corpus/canary-egress-attacks.d.ts +17 -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 +213 -0
- package/esm/src/guardrails/corpus/inbound-payloads.d.ts +10 -0
- package/esm/src/guardrails/corpus/inbound-payloads.js +125 -0
- package/esm/src/guardrails/corpus/live-attacks.d.ts +20 -0
- package/esm/src/guardrails/corpus/live-attacks.js +231 -0
- package/esm/src/guardrails/corpus/mod.d.ts +14 -0
- package/esm/src/guardrails/corpus/mod.js +11 -0
- package/esm/src/guardrails/corpus/secrets.d.ts +17 -0
- package/esm/src/guardrails/corpus/secrets.js +17 -0
- package/esm/src/guardrails/corpus/strings.d.ts +28 -0
- package/esm/src/guardrails/corpus/strings.js +34 -0
- package/esm/src/guardrails/corpus/types.d.ts +38 -0
- package/esm/src/guardrails/corpus/types.js +6 -0
- package/esm/src/guardrails/egress.d.ts +32 -0
- package/esm/src/guardrails/egress.js +87 -0
- package/esm/src/guardrails/error.d.ts +14 -23
- package/esm/src/guardrails/error.js +87 -76
- package/esm/src/guardrails/eval/corpus.d.ts +108 -0
- package/esm/src/guardrails/eval/corpus.js +978 -0
- package/esm/src/guardrails/eval/mod.d.ts +51 -0
- package/esm/src/guardrails/eval/mod.js +133 -0
- package/esm/src/guardrails/eval/score.d.ts +66 -0
- package/esm/src/guardrails/eval/score.js +114 -0
- package/esm/src/guardrails/events.d.ts +25 -0
- package/esm/src/guardrails/events.js +56 -0
- package/esm/src/guardrails/hits.d.ts +24 -0
- package/esm/src/guardrails/hits.js +45 -0
- package/esm/src/guardrails/injection.js +28 -5
- package/esm/src/guardrails/lexicon.d.ts +39 -0
- package/esm/src/guardrails/lexicon.js +200 -0
- package/esm/src/guardrails/live-outbound-gate.d.ts +41 -0
- package/esm/src/guardrails/live-outbound-gate.js +222 -0
- package/esm/src/guardrails/mod.d.ts +30 -6
- package/esm/src/guardrails/mod.js +20 -5
- package/esm/src/guardrails/network.d.ts +19 -0
- package/esm/src/guardrails/network.js +234 -0
- package/esm/src/guardrails/policy.d.ts +35 -0
- package/esm/src/guardrails/policy.js +50 -0
- package/esm/src/guardrails/progressive-yield.d.ts +51 -0
- package/esm/src/guardrails/progressive-yield.js +98 -0
- package/esm/src/guardrails/quota.d.ts +17 -3
- package/esm/src/guardrails/quota.js +18 -4
- package/esm/src/guardrails/sanitize.d.ts +45 -19
- package/esm/src/guardrails/sanitize.js +177 -94
- package/esm/src/guardrails/sensitive.js +2 -1
- package/esm/src/guardrails/serialize.d.ts +35 -0
- package/esm/src/guardrails/serialize.js +58 -0
- package/esm/src/guardrails/testing.d.ts +17 -0
- package/esm/src/guardrails/testing.js +13 -0
- package/esm/src/guardrails/theorum-error.d.ts +12 -0
- package/esm/src/guardrails/theorum-error.js +15 -0
- package/esm/src/guardrails/tool-directives.d.ts +48 -0
- package/esm/src/guardrails/tool-directives.js +124 -0
- package/esm/src/guardrails/tool-result.d.ts +93 -0
- package/esm/src/guardrails/tool-result.js +276 -0
- package/esm/src/guardrails/types.d.ts +291 -0
- package/esm/src/guardrails/types.js +72 -0
- package/esm/src/host/client-turn.d.ts +19 -0
- package/esm/src/host/client-turn.js +36 -0
- package/esm/src/host/mint-trace.d.ts +1 -1
- package/esm/src/host/mod.d.ts +5 -3
- package/esm/src/host/mod.js +4 -3
- package/esm/src/kernel/auth/crypto.d.ts +42 -0
- package/esm/src/kernel/auth/crypto.js +106 -0
- package/esm/src/kernel/auth/mod.d.ts +11 -0
- package/esm/src/kernel/auth/mod.js +11 -0
- package/esm/src/kernel/auth/oauth.d.ts +47 -0
- package/esm/src/kernel/auth/oauth.js +278 -0
- package/esm/src/kernel/auth/types.d.ts +133 -0
- package/esm/src/kernel/auth/types.js +13 -0
- package/esm/src/kernel/engine/delta.d.ts +24 -2
- package/esm/src/kernel/engine/delta.js +478 -39
- package/esm/src/kernel/engine/live-inbound.d.ts +21 -0
- package/esm/src/kernel/engine/live-inbound.js +31 -0
- package/esm/src/kernel/engine/live-ingress.d.ts +19 -0
- package/esm/src/kernel/engine/live-ingress.js +47 -0
- package/esm/src/kernel/engine/repair.js +13 -12
- package/esm/src/kernel/engine/runner/gates.d.ts +1 -1
- package/esm/src/kernel/engine/runner/gates.js +130 -43
- package/esm/src/kernel/engine/runner/mod.d.ts +6 -4
- package/esm/src/kernel/engine/runner/mod.js +192 -53
- package/esm/src/kernel/engine/runner/schema-validation.js +3 -3
- package/esm/src/kernel/engine/runner/stages.d.ts +39 -0
- package/esm/src/kernel/engine/runner/stages.js +89 -0
- package/esm/src/kernel/engine/runner/state.d.ts +31 -0
- package/esm/src/kernel/engine/runner/steps.d.ts +1 -1
- package/esm/src/kernel/engine/runner/steps.js +244 -43
- package/esm/src/kernel/engine/runner/stream.d.ts +9 -3
- package/esm/src/kernel/engine/runner/stream.js +140 -44
- package/esm/src/kernel/engine/session/mod.d.ts +25 -0
- package/esm/src/kernel/engine/session/mod.js +557 -0
- package/esm/src/kernel/interaction-parts.d.ts +14 -0
- package/esm/src/kernel/interaction-parts.js +23 -0
- package/esm/src/kernel/mod.d.ts +21 -10
- package/esm/src/kernel/mod.js +11 -8
- package/esm/src/kernel/profile-graph.d.ts +159 -0
- package/esm/src/kernel/profile-graph.js +156 -0
- package/esm/src/kernel/registry/attachments.d.ts +12 -10
- package/esm/src/kernel/registry/attachments.js +33 -27
- package/esm/src/kernel/registry/catalog.d.ts +25 -24
- package/esm/src/kernel/registry/catalog.js +60 -101
- package/esm/src/kernel/registry/ingress.d.ts +9 -4
- package/esm/src/kernel/registry/ingress.js +97 -75
- package/esm/src/kernel/registry/profile-outputs.d.ts +4 -0
- package/esm/src/kernel/registry/profile-outputs.js +8 -0
- package/esm/src/kernel/registry/profiles.d.ts +55 -12
- package/esm/src/kernel/registry/profiles.js +413 -73
- package/esm/src/kernel/registry/provider-request.js +13 -7
- package/esm/src/kernel/registry/resolve.d.ts +8 -8
- package/esm/src/kernel/registry/resolve.js +169 -154
- package/esm/src/kernel/registry/schemas.js +1 -1
- package/esm/src/kernel/registry/sole-model.d.ts +8 -0
- package/esm/src/kernel/registry/sole-model.js +10 -0
- package/esm/src/kernel/registry/system-prompt.d.ts +10 -0
- package/esm/src/kernel/registry/system-prompt.js +40 -0
- package/esm/src/kernel/registry/system-role.d.ts +8 -0
- package/esm/src/kernel/registry/system-role.js +14 -0
- package/esm/src/kernel/registry/vault.d.ts +12 -7
- package/esm/src/kernel/registry/vault.js +32 -10
- package/esm/src/kernel/schema.d.ts +231 -0
- package/esm/src/kernel/schema.js +607 -0
- package/esm/src/kernel/stages.d.ts +175 -0
- package/esm/src/kernel/stages.js +476 -0
- package/esm/src/kernel/stop.d.ts +78 -19
- package/esm/src/kernel/stop.js +51 -16
- package/esm/src/kernel/tools/events.d.ts +41 -0
- package/esm/src/kernel/tools/events.js +71 -0
- package/esm/src/kernel/tools/execute.d.ts +84 -0
- package/esm/src/kernel/tools/execute.js +614 -0
- package/esm/src/kernel/tools/harness.d.ts +8 -0
- package/esm/src/kernel/tools/harness.js +46 -0
- package/esm/src/kernel/tools/invoke.d.ts +10 -0
- package/esm/src/kernel/tools/invoke.js +101 -0
- package/esm/src/kernel/tools/mod.d.ts +13 -0
- package/esm/src/kernel/tools/mod.js +11 -0
- package/esm/src/kernel/tools/permission.d.ts +15 -0
- package/esm/src/kernel/tools/permission.js +47 -0
- package/esm/src/kernel/tools/project.d.ts +12 -0
- package/esm/src/kernel/tools/project.js +36 -0
- package/esm/src/kernel/tools/registry.d.ts +23 -0
- package/esm/src/kernel/tools/registry.js +81 -0
- package/esm/src/kernel/tools/remote.d.ts +94 -0
- package/esm/src/kernel/tools/remote.js +577 -0
- package/esm/src/kernel/tools/resolve.d.ts +39 -0
- package/esm/src/kernel/tools/resolve.js +283 -0
- package/esm/src/kernel/tools/schema.d.ts +15 -0
- package/esm/src/kernel/tools/schema.js +176 -0
- package/esm/src/kernel/tools/stage-run.d.ts +105 -0
- package/esm/src/kernel/tools/stage-run.js +155 -0
- package/esm/src/kernel/tools/types.d.ts +394 -0
- package/esm/src/kernel/tools/types.js +9 -0
- package/esm/src/kernel/types.d.ts +540 -256
- package/esm/src/kernel/util/find-last.d.ts +2 -0
- package/esm/src/kernel/util/find-last.js +10 -0
- package/esm/src/observability/destinations.d.ts +31 -0
- package/esm/src/observability/destinations.js +67 -0
- package/esm/src/observability/mod.d.ts +10 -3
- package/esm/src/observability/mod.js +6 -2
- package/esm/src/observability/policy.d.ts +27 -0
- package/esm/src/observability/policy.js +80 -0
- package/esm/src/observability/resolve-policy.d.ts +16 -0
- package/esm/src/observability/resolve-policy.js +64 -0
- package/esm/src/observability/trace-attach.d.ts +8 -4
- package/esm/src/observability/trace-attach.js +50 -29
- package/esm/src/observability/trace-record.d.ts +23 -13
- package/esm/src/observability/trace-record.js +96 -39
- package/esm/src/observability/trace-sink.d.ts +19 -0
- package/esm/src/observability/trace-sink.js +10 -0
- 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 +18 -7
- package/esm/src/observability/trace.js +34 -17
- package/esm/src/observability/types.d.ts +113 -0
- package/esm/src/observability/types.js +11 -0
- 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 +20 -17
- package/esm/src/providers/create-provider.js +72 -26
- package/esm/src/providers/google/interactions/framing.d.ts +23 -0
- package/esm/src/providers/google/interactions/framing.js +269 -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 +49 -0
- package/esm/src/providers/google/live/framing.js +552 -0
- package/esm/src/providers/google/live/openapi-schema.d.ts +6 -0
- package/esm/src/providers/google/live/openapi-schema.js +46 -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 +45 -0
- package/esm/src/providers/google/live/stream.js +214 -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/cache-control.d.ts +24 -0
- package/esm/src/providers/openrouter/cache-control.js +23 -0
- package/esm/src/providers/openrouter/chat.d.ts +107 -0
- package/esm/src/providers/{openrouter.js → openrouter/chat.js} +117 -231
- package/esm/src/providers/openrouter/image.d.ts +34 -0
- package/esm/src/providers/openrouter/image.js +275 -0
- package/esm/src/providers/openrouter/openai/chat-payload.d.ts +24 -0
- package/esm/src/providers/openrouter/openai/chat-payload.js +82 -0
- package/esm/src/providers/openrouter/openai/compat.d.ts +53 -0
- package/esm/src/providers/openrouter/openai/compat.js +213 -0
- package/esm/src/providers/openrouter/openai/image-payload.d.ts +18 -0
- package/esm/src/providers/openrouter/openai/image-payload.js +90 -0
- package/esm/src/providers/openrouter/openai/sdk-messages.d.ts +22 -0
- package/esm/src/providers/openrouter/openai/sdk-messages.js +122 -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 -202
- 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 -202
- 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,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,45 @@
|
|
|
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
|
+
/**
|
|
36
|
+
* Conversational cycle boundary.
|
|
37
|
+
*
|
|
38
|
+
* When the server reports `interactionStatus`, that is authoritative: `IDLE`
|
|
39
|
+
* closes the cycle and `IN_PROGRESS` keeps it open even across `turnComplete`
|
|
40
|
+
* (background reasoning / async tools may still produce output). Without the
|
|
41
|
+
* field, `turnComplete` is the boundary as before.
|
|
42
|
+
*/
|
|
43
|
+
export declare function turnPhaseFromMessage(message: Record<string, unknown>, events: TurnEvent[]): LiveTurnPhase;
|
|
44
|
+
/** Attach handlers that keep the socket open across conversational turns. */
|
|
45
|
+
export declare function attachLiveSessionHandlers(ws: WebSocket, liveQueue: LiveQueue): void;
|
|
@@ -0,0 +1,214 @@
|
|
|
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, readLiveInteractionStatus, } 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
|
+
/**
|
|
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) {
|
|
155
|
+
const interrupted = events.some((ev) => ev.type === 'done' && ev.interrupted === true);
|
|
156
|
+
if (interrupted)
|
|
157
|
+
return 'abort';
|
|
158
|
+
const status = readLiveInteractionStatus(message);
|
|
159
|
+
if (status === 'IDLE')
|
|
160
|
+
return 'complete';
|
|
161
|
+
if (status === 'IN_PROGRESS')
|
|
162
|
+
return 'streaming';
|
|
163
|
+
const serverContent = message.serverContent;
|
|
164
|
+
if (serverContent?.turnComplete || serverContent?.turn_complete)
|
|
165
|
+
return 'complete';
|
|
166
|
+
return 'streaming';
|
|
167
|
+
}
|
|
168
|
+
/** Attach handlers that keep the socket open across conversational turns. */
|
|
169
|
+
export function attachLiveSessionHandlers(ws, liveQueue) {
|
|
170
|
+
ws.onmessage = async (evt) => {
|
|
171
|
+
try {
|
|
172
|
+
const rawText = await readMessageData(evt.data);
|
|
173
|
+
const parsed = parseGeminiLiveMessage(rawText);
|
|
174
|
+
if (!parsed.ok) {
|
|
175
|
+
if (parsed.reason === 'empty')
|
|
176
|
+
return;
|
|
177
|
+
liveQueue.push({
|
|
178
|
+
type: 'error',
|
|
179
|
+
error: new TheorumError('malformed Gemini Live message'),
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (parsed.value.setupComplete)
|
|
184
|
+
return;
|
|
185
|
+
const errMsg = readGeminiLiveErrorMessage(parsed.value);
|
|
186
|
+
if (errMsg) {
|
|
187
|
+
liveQueue.push({ type: 'error', error: new TheorumError(errMsg) });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const events = foldGeminiLiveServerMessage(parsed.value);
|
|
191
|
+
const turnPhase = turnPhaseFromMessage(parsed.value, events);
|
|
192
|
+
if (events.length > 0 || turnPhase !== 'streaming') {
|
|
193
|
+
liveQueue.push({ type: 'batch', events, turnPhase });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
liveQueue.push({
|
|
198
|
+
type: 'error',
|
|
199
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
ws.onerror = () => {
|
|
204
|
+
if (!liveQueue.isClosed()) {
|
|
205
|
+
liveQueue.push({ type: 'error', error: new TheorumError('Gemini Live WebSocket error') });
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
ws.onclose = () => {
|
|
209
|
+
if (!liveQueue.isClosed()) {
|
|
210
|
+
liveQueue.push({ type: 'closed' });
|
|
211
|
+
liveQueue.close();
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
@@ -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 };
|
|
@@ -5,14 +5,19 @@
|
|
|
5
5
|
* Streams SSE from `/v1/chat/completions`, accumulates tool calls, and yields
|
|
6
6
|
* normalized `TurnEvent` objects. No external SDK dependency — raw fetch + SSE.
|
|
7
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
|
+
*
|
|
8
11
|
* Hosts pass `baseUrl` explicitly. THEORUM does not read `OLLAMA_HOST` or other
|
|
9
|
-
* environment variables (see
|
|
12
|
+
* environment variables (see docs/contracts/providers.md).
|
|
10
13
|
*
|
|
11
14
|
* @module
|
|
12
15
|
*/
|
|
13
|
-
import { isAbortError, toErrorEvent } from '
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
+
import { isAbortError, toErrorEvent } from '../../guardrails/error.js';
|
|
17
|
+
import { turnStopFromOpenAiFinishReason } from '../../kernel/stop.js';
|
|
18
|
+
import { buildChatMessages, wireTools } from '../openrouter/openai/compat.js';
|
|
19
|
+
import { parseSseStream } from '../shared/sse.js';
|
|
20
|
+
import { parseToolArgumentsObject } from '../shared/tool-args.js';
|
|
16
21
|
/** Default OpenAI-compat base when the host omits `baseUrl` (Ollama's default port). */
|
|
17
22
|
export const DEFAULT_LOCAL_BASE_URL = 'http://127.0.0.1:11434';
|
|
18
23
|
// ── request mapping ─────────────────────────────────
|
|
@@ -22,132 +27,48 @@ function normalizeBaseUrl(baseUrl) {
|
|
|
22
27
|
end -= 1;
|
|
23
28
|
return baseUrl.slice(0, end);
|
|
24
29
|
}
|
|
25
|
-
function resolveBaseUrl(config) {
|
|
30
|
+
export function resolveBaseUrl(config) {
|
|
26
31
|
return normalizeBaseUrl(config?.baseUrl?.trim() || DEFAULT_LOCAL_BASE_URL);
|
|
27
32
|
}
|
|
28
|
-
function inputToContent(parts) {
|
|
29
|
-
if (parts.every((p) => p.type === 'text')) {
|
|
30
|
-
return parts.map((p) => ('text' in p ? p.text : '')).join('\n');
|
|
31
|
-
}
|
|
32
|
-
return parts.map((p) => {
|
|
33
|
-
if (p.type === 'text')
|
|
34
|
-
return { type: 'text', text: p.text };
|
|
35
|
-
return {
|
|
36
|
-
type: 'image_url',
|
|
37
|
-
image_url: { url: `data:${p.mimeType};base64,${p.data}` },
|
|
38
|
-
};
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
function historyMessageContent(msg) {
|
|
42
|
-
if (msg.parts && msg.parts.length > 0) {
|
|
43
|
-
return inputToContent(msg.parts);
|
|
44
|
-
}
|
|
45
|
-
if (msg.content != null)
|
|
46
|
-
return msg.content;
|
|
47
|
-
return undefined;
|
|
48
|
-
}
|
|
49
|
-
function historyToWire(req) {
|
|
50
|
-
const msgs = [];
|
|
51
|
-
if (req.system)
|
|
52
|
-
msgs.push({ role: 'system', content: req.system });
|
|
53
|
-
for (const msg of req.history ?? []) {
|
|
54
|
-
msgs.push(historyMessageToWire(msg));
|
|
55
|
-
}
|
|
56
|
-
if (req.input.length > 0) {
|
|
57
|
-
msgs.push({ role: 'user', content: inputToContent(req.input) });
|
|
58
|
-
}
|
|
59
|
-
return msgs;
|
|
60
|
-
}
|
|
61
|
-
function historyMessageToWire(msg) {
|
|
62
|
-
if (msg.role === 'tool') {
|
|
63
|
-
return {
|
|
64
|
-
role: 'tool',
|
|
65
|
-
tool_call_id: msg.tool_call_id ?? `call_${msg.name ?? 'tool'}`,
|
|
66
|
-
name: msg.name,
|
|
67
|
-
content: msg.content ?? '',
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
if (msg.role === 'assistant' && msg.tool_calls && msg.tool_calls.length > 0) {
|
|
71
|
-
return {
|
|
72
|
-
role: 'assistant',
|
|
73
|
-
content: historyMessageContent(msg) ?? undefined,
|
|
74
|
-
tool_calls: msg.tool_calls.map((tc) => ({
|
|
75
|
-
id: tc.id,
|
|
76
|
-
type: 'function',
|
|
77
|
-
function: { name: tc.function.name, arguments: tc.function.arguments },
|
|
78
|
-
})),
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
return { role: msg.role, content: historyMessageContent(msg) ?? '' };
|
|
82
|
-
}
|
|
83
|
-
function toolsToWire(dynamicTools) {
|
|
84
|
-
if (!dynamicTools || dynamicTools.length === 0)
|
|
85
|
-
return undefined;
|
|
86
|
-
return dynamicTools.map((t) => ({
|
|
87
|
-
type: 'function',
|
|
88
|
-
function: {
|
|
89
|
-
name: t.name,
|
|
90
|
-
description: t.description ?? '',
|
|
91
|
-
parameters: t.parameters ?? { type: 'object', properties: {} },
|
|
92
|
-
},
|
|
93
|
-
}));
|
|
94
|
-
}
|
|
95
33
|
function buildBody(req) {
|
|
96
34
|
const body = {
|
|
97
35
|
model: req.apiId,
|
|
98
|
-
messages:
|
|
36
|
+
messages: buildChatMessages(req),
|
|
99
37
|
stream: true,
|
|
100
38
|
stream_options: { include_usage: true },
|
|
101
39
|
temperature: req.temperature,
|
|
102
40
|
max_tokens: req.maxOutputTokens,
|
|
103
41
|
};
|
|
104
|
-
const tools =
|
|
42
|
+
const tools = wireTools(req.wireTools);
|
|
105
43
|
if (tools)
|
|
106
44
|
body.tools = tools;
|
|
107
45
|
return body;
|
|
108
46
|
}
|
|
109
|
-
// ── SSE parsing ─────────────────────────────────────
|
|
110
|
-
async function* parseSse(body) {
|
|
111
|
-
const reader = body.getReader();
|
|
112
|
-
const decoder = new TextDecoder();
|
|
113
|
-
let buffer = '';
|
|
114
|
-
for (;;) {
|
|
115
|
-
const { done, value } = await reader.read();
|
|
116
|
-
if (done)
|
|
117
|
-
break;
|
|
118
|
-
buffer += decoder.decode(value, { stream: true });
|
|
119
|
-
const lines = buffer.split('\n');
|
|
120
|
-
buffer = lines.pop() ?? '';
|
|
121
|
-
for (const line of lines) {
|
|
122
|
-
const trimmed = line.trim();
|
|
123
|
-
if (!trimmed.startsWith('data: '))
|
|
124
|
-
continue;
|
|
125
|
-
const data = trimmed.slice(6);
|
|
126
|
-
if (data === '[DONE]')
|
|
127
|
-
return;
|
|
128
|
-
try {
|
|
129
|
-
yield JSON.parse(data);
|
|
130
|
-
}
|
|
131
|
-
catch {
|
|
132
|
-
// skip malformed
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
47
|
// ── stream → TurnEvent ──────────────────────────────
|
|
138
|
-
function flushPending(pending) {
|
|
48
|
+
export function flushPending(pending) {
|
|
139
49
|
const events = [];
|
|
140
50
|
for (const [, tc] of pending) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
51
|
+
const parsed = parseToolArgumentsObject(tc.args);
|
|
52
|
+
if (!parsed.ok) {
|
|
53
|
+
events.push({
|
|
54
|
+
type: 'tool',
|
|
55
|
+
tool: {
|
|
56
|
+
name: tc.name,
|
|
57
|
+
arguments: {},
|
|
58
|
+
id: tc.id,
|
|
59
|
+
phase: 'error',
|
|
60
|
+
failure: {
|
|
61
|
+
code: 'malformed_arguments',
|
|
62
|
+
message: parsed.error,
|
|
63
|
+
details: { raw: parsed.raw },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
continue;
|
|
147
68
|
}
|
|
148
69
|
events.push({
|
|
149
70
|
type: 'tool',
|
|
150
|
-
tool: { name: tc.name, arguments: parsed, id: tc.id },
|
|
71
|
+
tool: { name: tc.name, arguments: parsed.value, id: tc.id },
|
|
151
72
|
});
|
|
152
73
|
}
|
|
153
74
|
pending.clear();
|
|
@@ -174,11 +95,11 @@ async function* streamComplete(baseUrl, req, fetchFn) {
|
|
|
174
95
|
async function* streamOpenAiBody(body) {
|
|
175
96
|
const pending = new Map();
|
|
176
97
|
let finishReason;
|
|
177
|
-
for await (const
|
|
178
|
-
const usageEvent = tokensFromUsage(
|
|
98
|
+
for await (const raw of parseSseStream(body)) {
|
|
99
|
+
const usageEvent = tokensFromUsage(readOpenAiUsage(raw));
|
|
179
100
|
if (usageEvent)
|
|
180
101
|
yield usageEvent;
|
|
181
|
-
const choice =
|
|
102
|
+
const choice = firstOpenAiChoice(raw);
|
|
182
103
|
if (!choice)
|
|
183
104
|
continue;
|
|
184
105
|
yield* eventsFromChoiceDelta(choice.delta, pending);
|
|
@@ -190,7 +111,37 @@ async function* streamOpenAiBody(body) {
|
|
|
190
111
|
}
|
|
191
112
|
for (const event of flushPending(pending))
|
|
192
113
|
yield event;
|
|
193
|
-
yield { type: 'done', stop:
|
|
114
|
+
yield { type: 'done', stop: turnStopFromOpenAiFinishReason(finishReason) };
|
|
115
|
+
}
|
|
116
|
+
function readOpenAiUsage(raw) {
|
|
117
|
+
const usage = raw.usage;
|
|
118
|
+
if (!usage || typeof usage !== 'object' || Array.isArray(usage))
|
|
119
|
+
return undefined;
|
|
120
|
+
const row = usage;
|
|
121
|
+
return {
|
|
122
|
+
prompt_tokens: typeof row.prompt_tokens === 'number' ? row.prompt_tokens : undefined,
|
|
123
|
+
completion_tokens: typeof row.completion_tokens === 'number' ? row.completion_tokens : undefined,
|
|
124
|
+
total_tokens: typeof row.total_tokens === 'number' ? row.total_tokens : undefined,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function firstOpenAiChoice(raw) {
|
|
128
|
+
if (!Array.isArray(raw.choices) || raw.choices.length === 0)
|
|
129
|
+
return undefined;
|
|
130
|
+
const choice = raw.choices[0];
|
|
131
|
+
if (!choice || typeof choice !== 'object' || Array.isArray(choice))
|
|
132
|
+
return undefined;
|
|
133
|
+
const row = choice;
|
|
134
|
+
const deltaRaw = row.delta;
|
|
135
|
+
const delta = deltaRaw && typeof deltaRaw === 'object' && !Array.isArray(deltaRaw)
|
|
136
|
+
? deltaRaw
|
|
137
|
+
: undefined;
|
|
138
|
+
return {
|
|
139
|
+
index: typeof row.index === 'number' ? row.index : 0,
|
|
140
|
+
delta,
|
|
141
|
+
finish_reason: typeof row.finish_reason === 'string' || row.finish_reason === null
|
|
142
|
+
? row.finish_reason
|
|
143
|
+
: undefined,
|
|
144
|
+
};
|
|
194
145
|
}
|
|
195
146
|
function tokensFromUsage(usage) {
|
|
196
147
|
if (!usage)
|
|
@@ -245,15 +196,4 @@ function createLocalProvider(config) {
|
|
|
245
196
|
},
|
|
246
197
|
};
|
|
247
198
|
}
|
|
248
|
-
export { createLocalProvider };
|
|
249
|
-
exposeForTests('local', {
|
|
250
|
-
inputToContent,
|
|
251
|
-
historyMessageContent,
|
|
252
|
-
historyToWire,
|
|
253
|
-
toolsToWire,
|
|
254
|
-
buildBody,
|
|
255
|
-
parseSse,
|
|
256
|
-
flushPending,
|
|
257
|
-
resolveBaseUrl,
|
|
258
|
-
DEFAULT_LOCAL_BASE_URL,
|
|
259
|
-
});
|
|
199
|
+
export { buildChatMessages as historyToWire, createLocalProvider, wireTools as toolsToWire };
|