getpatter 0.6.7 → 0.6.8
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/dist/{aec-PJJMUM5E.mjs → aec-ZZ5HGKS3.mjs} +10 -2
- package/dist/{carrier-config-7YGNRBPO.mjs → carrier-config-6L5NND7B.mjs} +19 -5
- package/dist/chunk-3JNVSNLV.mjs +428 -0
- package/dist/{chunk-3VVATR6A.mjs → chunk-C2LWB42T.mjs} +9 -6
- package/dist/{chunk-BO227NTF.mjs → chunk-I56S5MDJ.mjs} +121 -43
- package/dist/chunk-OV252D2V.mjs +198 -0
- package/dist/{chunk-YJX2EKON.mjs → chunk-YJ4HKJL6.mjs} +7404 -4593
- package/dist/cli.js +32799 -705
- package/dist/dashboard/ui.html +9 -9
- package/dist/index.d.mts +2829 -47
- package/dist/index.d.ts +2829 -47
- package/dist/index.js +5947 -942
- package/dist/index.mjs +2279 -844
- package/dist/{openai-realtime-2-L5EKAAUH.mjs → openai-realtime-2-O4DP3LXN.mjs} +1 -1
- package/dist/session-N3CBCYYN.mjs +12 -0
- package/dist/silero-vad-SSGHVHLA.mjs +9 -0
- package/dist/{test-mode-XFOADUNE.mjs → test-mode-5CNXC447.mjs} +3 -2
- package/package.json +1 -1
- package/src/dashboard/ui.html +9 -9
- package/dist/silero-vad-RGF5HCIR.mjs +0 -7
package/dist/index.mjs
CHANGED
|
@@ -2,6 +2,17 @@ import {
|
|
|
2
2
|
startTunnel
|
|
3
3
|
} from "./chunk-XS45BAQL.mjs";
|
|
4
4
|
import {
|
|
5
|
+
TestSession
|
|
6
|
+
} from "./chunk-OV252D2V.mjs";
|
|
7
|
+
import {
|
|
8
|
+
EvalSession,
|
|
9
|
+
FakeAudioSender,
|
|
10
|
+
FakeSTT,
|
|
11
|
+
FakeTTS,
|
|
12
|
+
historyTranscript
|
|
13
|
+
} from "./chunk-3JNVSNLV.mjs";
|
|
14
|
+
import {
|
|
15
|
+
AGENT_BACKLOG_CAP_S,
|
|
5
16
|
AuthenticationError,
|
|
6
17
|
CallMetricsAccumulator,
|
|
7
18
|
Carrier,
|
|
@@ -10,11 +21,14 @@ import {
|
|
|
10
21
|
DeepgramModel,
|
|
11
22
|
DeepgramSTT,
|
|
12
23
|
DefaultToolExecutor,
|
|
24
|
+
ENV_FLAG,
|
|
13
25
|
ElevenLabsConvAIAdapter,
|
|
14
26
|
EmbeddedServer,
|
|
15
27
|
ErrorCode,
|
|
16
28
|
EventBus,
|
|
17
29
|
LLMLoop,
|
|
30
|
+
LLM_STREAM_IDLE_TIMEOUT_MS,
|
|
31
|
+
LocalCallRecorder,
|
|
18
32
|
MetricsStore,
|
|
19
33
|
OpenAILLMProvider,
|
|
20
34
|
PRICING_LAST_UPDATED,
|
|
@@ -26,6 +40,7 @@ import {
|
|
|
26
40
|
PlivoAdapter,
|
|
27
41
|
PricingUnit,
|
|
28
42
|
ProvisionError,
|
|
43
|
+
RECORDING_SAMPLE_RATE,
|
|
29
44
|
RateLimitError,
|
|
30
45
|
RemoteMessageHandler,
|
|
31
46
|
SPAN_BARGEIN,
|
|
@@ -36,7 +51,7 @@ import {
|
|
|
36
51
|
SPAN_TOOL,
|
|
37
52
|
SPAN_TTS,
|
|
38
53
|
SentenceChunker,
|
|
39
|
-
|
|
54
|
+
TwilioAdapter,
|
|
40
55
|
VERSION,
|
|
41
56
|
calculateRealtimeCost,
|
|
42
57
|
calculateSttCost,
|
|
@@ -44,6 +59,7 @@ import {
|
|
|
44
59
|
calculateTtsCost,
|
|
45
60
|
callsToCsv,
|
|
46
61
|
callsToJson,
|
|
62
|
+
createStreamIdleWatchdog,
|
|
47
63
|
initTracing,
|
|
48
64
|
isRemoteUrl,
|
|
49
65
|
isTracingEnabled,
|
|
@@ -56,8 +72,10 @@ import {
|
|
|
56
72
|
openclawConsult,
|
|
57
73
|
openclawPostCallNotifier,
|
|
58
74
|
resolveLogRoot,
|
|
59
|
-
|
|
60
|
-
|
|
75
|
+
shutdownTracing,
|
|
76
|
+
startSpan,
|
|
77
|
+
withSpan
|
|
78
|
+
} from "./chunk-YJ4HKJL6.mjs";
|
|
61
79
|
import {
|
|
62
80
|
OpenAIRealtime2Adapter,
|
|
63
81
|
OpenAIRealtimeAdapter,
|
|
@@ -78,7 +96,7 @@ import {
|
|
|
78
96
|
resample24kTo16k,
|
|
79
97
|
resample8kTo16k,
|
|
80
98
|
validateRealtimeTurnDetection
|
|
81
|
-
} from "./chunk-
|
|
99
|
+
} from "./chunk-I56S5MDJ.mjs";
|
|
82
100
|
import {
|
|
83
101
|
MinWordsStrategy,
|
|
84
102
|
evaluateStrategies,
|
|
@@ -92,8 +110,9 @@ import {
|
|
|
92
110
|
notifyDashboard
|
|
93
111
|
} from "./chunk-6GR5MHHQ.mjs";
|
|
94
112
|
import {
|
|
95
|
-
SileroVAD
|
|
96
|
-
|
|
113
|
+
SileroVAD,
|
|
114
|
+
loadOnnxRuntime
|
|
115
|
+
} from "./chunk-C2LWB42T.mjs";
|
|
97
116
|
import {
|
|
98
117
|
__dirname,
|
|
99
118
|
__require,
|
|
@@ -432,10 +451,20 @@ var cachedInstallId = null;
|
|
|
432
451
|
function runId() {
|
|
433
452
|
return RUN_ID;
|
|
434
453
|
}
|
|
454
|
+
function stateDir() {
|
|
455
|
+
const override = process.env.PATTER_TELEMETRY_STATE_DIR;
|
|
456
|
+
if (override && override.length > 0) return override;
|
|
457
|
+
const xdg = process.env.XDG_STATE_HOME;
|
|
458
|
+
if (xdg && xdg.length > 0) return path.join(xdg, "getpatter");
|
|
459
|
+
return path.join(os.homedir(), ".getpatter");
|
|
460
|
+
}
|
|
461
|
+
function legacyStateDir() {
|
|
462
|
+
if (process.env.PATTER_TELEMETRY_STATE_DIR) return null;
|
|
463
|
+
const xdg = process.env.XDG_STATE_HOME;
|
|
464
|
+
return xdg && xdg.length > 0 ? xdg : null;
|
|
465
|
+
}
|
|
435
466
|
function statePath() {
|
|
436
|
-
|
|
437
|
-
const root = base && base.length > 0 ? base : path.join(os.homedir(), ".getpatter");
|
|
438
|
-
return path.join(root, "install-id");
|
|
467
|
+
return path.join(stateDir(), "install-id");
|
|
439
468
|
}
|
|
440
469
|
function installId() {
|
|
441
470
|
if (cachedInstallId !== null) return cachedInstallId;
|
|
@@ -448,6 +477,27 @@ function installId() {
|
|
|
448
477
|
}
|
|
449
478
|
} catch {
|
|
450
479
|
}
|
|
480
|
+
const legacyDir = legacyStateDir();
|
|
481
|
+
if (legacyDir !== null) {
|
|
482
|
+
const legacy = path.join(legacyDir, "install-id");
|
|
483
|
+
let existing = "";
|
|
484
|
+
try {
|
|
485
|
+
existing = fs.readFileSync(legacy, "utf8").trim();
|
|
486
|
+
} catch {
|
|
487
|
+
existing = "";
|
|
488
|
+
}
|
|
489
|
+
if (HEX32.test(existing)) {
|
|
490
|
+
try {
|
|
491
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
492
|
+
fs.writeFileSync(p, existing, "utf8");
|
|
493
|
+
const stat = fs.statSync(legacy);
|
|
494
|
+
fs.utimesSync(p, stat.atime, stat.mtime);
|
|
495
|
+
} catch {
|
|
496
|
+
}
|
|
497
|
+
cachedInstallId = existing;
|
|
498
|
+
return cachedInstallId;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
451
501
|
const newId = randomUUID().replace(/-/g, "");
|
|
452
502
|
try {
|
|
453
503
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
@@ -469,6 +519,16 @@ function previousVersion(current) {
|
|
|
469
519
|
} catch {
|
|
470
520
|
prev = "";
|
|
471
521
|
}
|
|
522
|
+
if (prev === "") {
|
|
523
|
+
const legacyDir = legacyStateDir();
|
|
524
|
+
if (legacyDir !== null) {
|
|
525
|
+
try {
|
|
526
|
+
prev = fs.readFileSync(path.join(legacyDir, "version"), "utf8").trim();
|
|
527
|
+
} catch {
|
|
528
|
+
prev = "";
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
472
532
|
try {
|
|
473
533
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
474
534
|
fs.writeFileSync(p, current, "utf8");
|
|
@@ -499,6 +559,14 @@ function isFirstRun() {
|
|
|
499
559
|
} catch {
|
|
500
560
|
return false;
|
|
501
561
|
}
|
|
562
|
+
const legacyDir = legacyStateDir();
|
|
563
|
+
if (legacyDir !== null) {
|
|
564
|
+
try {
|
|
565
|
+
if (fs.existsSync(path.join(legacyDir, "first-run"))) return false;
|
|
566
|
+
} catch {
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
502
570
|
try {
|
|
503
571
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
504
572
|
fs.writeFileSync(p, "1", "utf8");
|
|
@@ -512,7 +580,13 @@ function optOutPath() {
|
|
|
512
580
|
}
|
|
513
581
|
function isOptedOut() {
|
|
514
582
|
try {
|
|
515
|
-
|
|
583
|
+
if (fs.existsSync(optOutPath())) return true;
|
|
584
|
+
} catch {
|
|
585
|
+
}
|
|
586
|
+
const legacyDir = legacyStateDir();
|
|
587
|
+
if (legacyDir === null) return false;
|
|
588
|
+
try {
|
|
589
|
+
return fs.existsSync(path.join(legacyDir, "telemetry-disabled"));
|
|
516
590
|
} catch {
|
|
517
591
|
return false;
|
|
518
592
|
}
|
|
@@ -603,7 +677,7 @@ function stackDimensions(stt, tts, llm) {
|
|
|
603
677
|
}
|
|
604
678
|
|
|
605
679
|
// src/telemetry/events.ts
|
|
606
|
-
var SCHEMA_VERSION =
|
|
680
|
+
var SCHEMA_VERSION = 7;
|
|
607
681
|
var EVENT_SDK_INITIALIZED = "sdk_initialized";
|
|
608
682
|
var EVENT_FIRST_RUN = "first_run";
|
|
609
683
|
var EVENT_CLI_COMMAND = "cli_command";
|
|
@@ -643,7 +717,7 @@ var DIMENSION_VALUES = {
|
|
|
643
717
|
// call_started / call_completed: inbound vs outbound — a core usage split.
|
|
644
718
|
direction: /* @__PURE__ */ new Set(["inbound", "outbound", "none"]),
|
|
645
719
|
// cli_command: which CLI subcommand was invoked (never args/flags values).
|
|
646
|
-
cli_command: /* @__PURE__ */ new Set(["dashboard", "eval", "telemetry", "none", "other"]),
|
|
720
|
+
cli_command: /* @__PURE__ */ new Set(["dashboard", "eval", "hermes", "openclaw", "telemetry", "none", "other"]),
|
|
647
721
|
// call_completed: the call's terminal outcome
|
|
648
722
|
outcome: /* @__PURE__ */ new Set(["completed", "error", "no_answer", "busy", "failed"]),
|
|
649
723
|
// call_completed: terminal error code (mirrors ErrorCode, plus "other"). Never
|
|
@@ -698,11 +772,14 @@ var BOOL_DIMENSIONS = /* @__PURE__ */ new Set([
|
|
|
698
772
|
"per_tool_timeouts_set",
|
|
699
773
|
"llm_fallback_configured"
|
|
700
774
|
]);
|
|
775
|
+
var ID_RE = /^[0-9a-f]{32}$/;
|
|
776
|
+
var ID_DIMENSIONS = /* @__PURE__ */ new Set(["call_uid"]);
|
|
701
777
|
var ALLOWED_DIMENSIONS = /* @__PURE__ */ new Set([
|
|
702
778
|
...Object.keys(DIMENSION_VALUES),
|
|
703
779
|
...NUMERIC_DIMENSIONS,
|
|
704
780
|
...STRING_DIMENSIONS,
|
|
705
|
-
...BOOL_DIMENSIONS
|
|
781
|
+
...BOOL_DIMENSIONS,
|
|
782
|
+
...ID_DIMENSIONS
|
|
706
783
|
]);
|
|
707
784
|
function osFamily() {
|
|
708
785
|
const p = os2.platform();
|
|
@@ -748,8 +825,14 @@ function buildEvent(name, opts) {
|
|
|
748
825
|
if (!(typeof value === "string" && MODEL_TOKEN_RE.test(value))) {
|
|
749
826
|
continue;
|
|
750
827
|
}
|
|
828
|
+
} else if (ID_DIMENSIONS.has(key)) {
|
|
829
|
+
if (!(typeof value === "string" && ID_RE.test(value))) {
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
751
832
|
} else if (BOOL_DIMENSIONS.has(key) && typeof value !== "boolean") {
|
|
752
833
|
continue;
|
|
834
|
+
} else if (NUMERIC_DIMENSIONS.has(key) && typeof value !== "number") {
|
|
835
|
+
continue;
|
|
753
836
|
}
|
|
754
837
|
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
755
838
|
event[key] = value;
|
|
@@ -762,6 +845,7 @@ function buildEvent(name, opts) {
|
|
|
762
845
|
var DEFAULT_ENDPOINT = "https://telemetry.getpatter.com/v1/ingest";
|
|
763
846
|
var TIMEOUT_MS = 3e3;
|
|
764
847
|
var BUFFER_MAX = 256;
|
|
848
|
+
var MAX_EVENTS_PER_POST = 64;
|
|
765
849
|
var noticeShown = false;
|
|
766
850
|
var liveClients = /* @__PURE__ */ new Set();
|
|
767
851
|
var exitHookRegistered = false;
|
|
@@ -770,7 +854,7 @@ function showNoticeOnce() {
|
|
|
770
854
|
if (noticeShown) return;
|
|
771
855
|
noticeShown = true;
|
|
772
856
|
getLogger().info(
|
|
773
|
-
"Anonymous usage telemetry is on (no PII, no call content). Collected: a random anonymous install id, SDK version, language, OS family, runtime version, coarse feature flags, the composed stack (provider + model per layer), tool counts, integration category, and per-call duration, latency, cost, and error codes (no call content, no message text). Disable with PATTER_TELEMETRY_DISABLED=1, DO_NOT_TRACK=1, or telemetry: false. Details: https://docs.getpatter.com/telemetry"
|
|
857
|
+
"Anonymous usage telemetry is on (no PII, no call content). Collected: a random anonymous install id, SDK version, language, OS family, runtime version, coarse feature flags, the composed stack (provider + model per layer), tool counts, integration category, a random per-call correlation id, and per-call duration, latency, cost, and error codes (no call content, no message text). Disable with PATTER_TELEMETRY_DISABLED=1, DO_NOT_TRACK=1, or telemetry: false. Details: https://docs.getpatter.com/telemetry"
|
|
774
858
|
);
|
|
775
859
|
}
|
|
776
860
|
function registerExitHook() {
|
|
@@ -847,6 +931,21 @@ var TelemetryClient = class {
|
|
|
847
931
|
getLogger().debug("telemetry flushPending failed", err);
|
|
848
932
|
}
|
|
849
933
|
}
|
|
934
|
+
/**
|
|
935
|
+
* Flush buffered events and wait for delivery. Unlike `close()` the client
|
|
936
|
+
* stays usable afterwards — for teardown paths that may serve again
|
|
937
|
+
* (`Patter.disconnect()`). Bounded by the flush's own per-POST abort timer.
|
|
938
|
+
* Mirrors Python's `drain()`.
|
|
939
|
+
*/
|
|
940
|
+
async drain() {
|
|
941
|
+
if (!this.enabledFlag || this.debug || this.closed) return;
|
|
942
|
+
try {
|
|
943
|
+
if (this.inflight) await this.inflight;
|
|
944
|
+
if (this.buffer.length > 0) await this.flush();
|
|
945
|
+
} catch (err) {
|
|
946
|
+
getLogger().debug("telemetry drain failed", err);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
850
949
|
/** Flush remaining events (graceful shutdown). Never throws. */
|
|
851
950
|
async close() {
|
|
852
951
|
if (this.closed) return;
|
|
@@ -868,7 +967,7 @@ var TelemetryClient = class {
|
|
|
868
967
|
if (this.inflight) return;
|
|
869
968
|
this.inflight = this.flush().finally(() => {
|
|
870
969
|
this.inflight = null;
|
|
871
|
-
if (this.buffer.length > 0) this.scheduleFlush();
|
|
970
|
+
if (this.buffer.length > 0 && !this.closed) this.scheduleFlush();
|
|
872
971
|
});
|
|
873
972
|
void this.inflight;
|
|
874
973
|
}
|
|
@@ -876,20 +975,24 @@ var TelemetryClient = class {
|
|
|
876
975
|
if (this.buffer.length === 0) return;
|
|
877
976
|
const events = this.buffer.splice(0, this.buffer.length);
|
|
878
977
|
pendingFlush.delete(this);
|
|
879
|
-
const controller = new AbortController();
|
|
880
|
-
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
881
|
-
timer.unref?.();
|
|
882
978
|
try {
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
979
|
+
for (let start = 0; start < events.length; start += MAX_EVENTS_PER_POST) {
|
|
980
|
+
const controller = new AbortController();
|
|
981
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
982
|
+
timer.unref?.();
|
|
983
|
+
try {
|
|
984
|
+
await fetch(this.endpoint, {
|
|
985
|
+
method: "POST",
|
|
986
|
+
headers: { "content-type": "application/json" },
|
|
987
|
+
body: JSON.stringify(events.slice(start, start + MAX_EVENTS_PER_POST)),
|
|
988
|
+
signal: controller.signal
|
|
989
|
+
});
|
|
990
|
+
} finally {
|
|
991
|
+
clearTimeout(timer);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
889
994
|
} catch (err) {
|
|
890
995
|
getLogger().debug("telemetry flush failed", err);
|
|
891
|
-
} finally {
|
|
892
|
-
clearTimeout(timer);
|
|
893
996
|
}
|
|
894
997
|
}
|
|
895
998
|
};
|
|
@@ -1221,7 +1324,7 @@ function resolvePersistRoot(persist) {
|
|
|
1221
1324
|
if (typeof persist === "string") return resolveLogRoot(persist);
|
|
1222
1325
|
const envRoot = resolveLogRoot();
|
|
1223
1326
|
if (envRoot !== null) return envRoot;
|
|
1224
|
-
return
|
|
1327
|
+
return resolveLogRoot("auto");
|
|
1225
1328
|
}
|
|
1226
1329
|
function closeParkedConnections(slot) {
|
|
1227
1330
|
if (slot.stt) {
|
|
@@ -1561,7 +1664,10 @@ var Patter = class {
|
|
|
1561
1664
|
const initDims = {
|
|
1562
1665
|
carrier: carrierFamily(carrier),
|
|
1563
1666
|
tunnel: tunnel instanceof Static ? "static" : options.tunnel ? "configured" : "none",
|
|
1564
|
-
|
|
1667
|
+
// Environment dims only when telemetry is ENABLED: the helper's
|
|
1668
|
+
// previousVersion probe writes ~/.getpatter/version, violating the
|
|
1669
|
+
// documented invariant that opting out never touches the filesystem.
|
|
1670
|
+
...this.telemetry.enabled ? telemetryEnvironmentDims() : {}
|
|
1565
1671
|
};
|
|
1566
1672
|
if (this.telemetry.enabled) {
|
|
1567
1673
|
try {
|
|
@@ -1570,8 +1676,8 @@ var Patter = class {
|
|
|
1570
1676
|
}
|
|
1571
1677
|
}
|
|
1572
1678
|
this.telemetry.record("sdk_initialized", initDims);
|
|
1573
|
-
this._tunnelReady = new Promise((
|
|
1574
|
-
this._tunnelReadyResolve =
|
|
1679
|
+
this._tunnelReady = new Promise((resolve2, reject) => {
|
|
1680
|
+
this._tunnelReadyResolve = resolve2;
|
|
1575
1681
|
this._tunnelReadyReject = reject;
|
|
1576
1682
|
});
|
|
1577
1683
|
this._tunnelReady.catch(() => {
|
|
@@ -1579,8 +1685,8 @@ var Patter = class {
|
|
|
1579
1685
|
if (normalizedWebhook) {
|
|
1580
1686
|
this._tunnelReadyResolve(normalizedWebhook);
|
|
1581
1687
|
}
|
|
1582
|
-
this._ready = new Promise((
|
|
1583
|
-
this._readyResolve =
|
|
1688
|
+
this._ready = new Promise((resolve2, reject) => {
|
|
1689
|
+
this._readyResolve = resolve2;
|
|
1584
1690
|
this._readyReject = reject;
|
|
1585
1691
|
});
|
|
1586
1692
|
this._ready.catch(() => {
|
|
@@ -1691,11 +1797,45 @@ var Patter = class {
|
|
|
1691
1797
|
throw new Error(`provider must be one of: ${valid.join(", ")}. Got: '${working.provider}'`);
|
|
1692
1798
|
}
|
|
1693
1799
|
}
|
|
1800
|
+
if (working.provider === "openai_realtime" && !working.engine && !this.localConfig.openaiKey) {
|
|
1801
|
+
const envKey = process.env.OPENAI_API_KEY;
|
|
1802
|
+
if (envKey) {
|
|
1803
|
+
this.localConfig = { ...this.localConfig, openaiKey: envKey };
|
|
1804
|
+
} else {
|
|
1805
|
+
throw new Error(
|
|
1806
|
+
"OpenAI Realtime mode requires an OpenAI API key. Pass engine: new OpenAIRealtime({ apiKey: 'sk-...' }) or set OPENAI_API_KEY in the environment."
|
|
1807
|
+
);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1694
1810
|
if (working.consult && working.provider === "elevenlabs_convai") {
|
|
1695
1811
|
getLogger().warn(
|
|
1696
1812
|
"consult is set but provider is ElevenLabs ConvAI; the consult tool is only injected in Realtime and Pipeline modes and will be ignored for this agent."
|
|
1697
1813
|
);
|
|
1698
1814
|
}
|
|
1815
|
+
if (working.handoffs !== void 0) {
|
|
1816
|
+
if (typeof working.handoffs !== "object" || working.handoffs === null || Array.isArray(working.handoffs)) {
|
|
1817
|
+
throw new TypeError(
|
|
1818
|
+
`handoffs must be an object of { name: agentOptions }, got ${Array.isArray(working.handoffs) ? "array" : typeof working.handoffs}.`
|
|
1819
|
+
);
|
|
1820
|
+
}
|
|
1821
|
+
for (const [hName, hAgent] of Object.entries(working.handoffs)) {
|
|
1822
|
+
if (!hName) {
|
|
1823
|
+
throw new Error(
|
|
1824
|
+
"handoffs keys must be non-empty strings (the names the LLM passes to handoff_to)."
|
|
1825
|
+
);
|
|
1826
|
+
}
|
|
1827
|
+
if (typeof hAgent !== "object" || hAgent === null || Array.isArray(hAgent)) {
|
|
1828
|
+
throw new TypeError(
|
|
1829
|
+
`handoffs['${hName}'] must be an agent options object (build with phone.agent({...})), got ${Array.isArray(hAgent) ? "array" : typeof hAgent}.`
|
|
1830
|
+
);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
if (working.provider === "elevenlabs_convai") {
|
|
1834
|
+
getLogger().warn(
|
|
1835
|
+
"handoffs is set but provider is ElevenLabs ConvAI; the handoff_to tool is only injected in Realtime and Pipeline modes and will be ignored for this agent."
|
|
1836
|
+
);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1699
1839
|
if (working.llm !== void 0) {
|
|
1700
1840
|
const llm = working.llm;
|
|
1701
1841
|
if (!llm || typeof llm.stream !== "function") {
|
|
@@ -1746,7 +1886,7 @@ var Patter = class {
|
|
|
1746
1886
|
}
|
|
1747
1887
|
if (opts.agent.echoCancellation) {
|
|
1748
1888
|
try {
|
|
1749
|
-
await import("./aec-
|
|
1889
|
+
await import("./aec-ZZ5HGKS3.mjs");
|
|
1750
1890
|
} catch (err) {
|
|
1751
1891
|
getLogger().debug(`AEC pre-import failed at serve(): ${String(err)}`);
|
|
1752
1892
|
}
|
|
@@ -1796,7 +1936,7 @@ var Patter = class {
|
|
|
1796
1936
|
const telephonyProvider = carrier.kind;
|
|
1797
1937
|
const wantsCarrierManagement = opts.manageWebhook !== false || wantsCloudflared;
|
|
1798
1938
|
if (wantsCarrierManagement) {
|
|
1799
|
-
const { autoConfigureCarrier } = await import("./carrier-config-
|
|
1939
|
+
const { autoConfigureCarrier } = await import("./carrier-config-6L5NND7B.mjs");
|
|
1800
1940
|
await autoConfigureCarrier({
|
|
1801
1941
|
telephonyProvider,
|
|
1802
1942
|
twilioSid: carrier.kind === "twilio" ? carrier.accountSid : void 0,
|
|
@@ -1835,11 +1975,14 @@ var Patter = class {
|
|
|
1835
1975
|
opts.pricing,
|
|
1836
1976
|
opts.dashboard ?? true,
|
|
1837
1977
|
opts.dashboardToken ?? "",
|
|
1838
|
-
opts.allowInsecureDashboard ?? false
|
|
1978
|
+
opts.allowInsecureDashboard ?? false,
|
|
1979
|
+
opts.localRecording ?? false
|
|
1839
1980
|
);
|
|
1840
1981
|
this.embeddedServer.telemetry = this.telemetry;
|
|
1841
1982
|
this.embeddedServer.popPrewarmAudio = this.popPrewarmAudio;
|
|
1842
1983
|
this.embeddedServer.popPrewarmedConnections = this.popPrewarmedConnections;
|
|
1984
|
+
this.embeddedServer.aliasPrewarm = this.aliasPrewarm;
|
|
1985
|
+
this.embeddedServer.speechEvents = this.speechEvents;
|
|
1843
1986
|
this.embeddedServer.recordPrewarmWaste = this.recordPrewarmWaste;
|
|
1844
1987
|
try {
|
|
1845
1988
|
await this.embeddedServer.start(port);
|
|
@@ -1856,7 +1999,7 @@ var Patter = class {
|
|
|
1856
1999
|
}
|
|
1857
2000
|
/** Run the agent in interactive terminal-test mode (no real telephony). */
|
|
1858
2001
|
async test(opts) {
|
|
1859
|
-
const { TestSession: TestSession2 } = await import("./test-mode-
|
|
2002
|
+
const { TestSession: TestSession2 } = await import("./test-mode-5CNXC447.mjs");
|
|
1860
2003
|
const session = new TestSession2();
|
|
1861
2004
|
await session.run({
|
|
1862
2005
|
agent: opts.agent,
|
|
@@ -1934,6 +2077,25 @@ var Patter = class {
|
|
|
1934
2077
|
* carrier ``start`` event instead of opening fresh ones — saving
|
|
1935
2078
|
* ~150-900 ms of cold-start handshake on the first turn.
|
|
1936
2079
|
*/
|
|
2080
|
+
/**
|
|
2081
|
+
* Re-key prewarm caches from a dial-time id to the live carrier id.
|
|
2082
|
+
* Plivo issues ``request_uuid`` at dial time but the media stream and
|
|
2083
|
+
* webhooks carry ``CallUUID`` — without re-keying, prewarmed first-message
|
|
2084
|
+
* audio and parked provider sockets never matched and always TTL-evicted
|
|
2085
|
+
* as "wasted". Mirrors Python ``_alias_prewarm``.
|
|
2086
|
+
*/
|
|
2087
|
+
aliasPrewarm = (oldId, newId) => {
|
|
2088
|
+
if (!oldId || !newId || oldId === newId) return;
|
|
2089
|
+
const rekey = (map) => {
|
|
2090
|
+
const v = map.get(oldId);
|
|
2091
|
+
if (v !== void 0 && !map.has(newId)) map.set(newId, v);
|
|
2092
|
+
map.delete(oldId);
|
|
2093
|
+
};
|
|
2094
|
+
rekey(this.prewarmAudio);
|
|
2095
|
+
rekey(this.prewarmTtlTimers);
|
|
2096
|
+
rekey(this.prewarmedConnections);
|
|
2097
|
+
rekey(this.prewarmedConnTimers);
|
|
2098
|
+
};
|
|
1937
2099
|
popPrewarmedConnections = (callId) => {
|
|
1938
2100
|
const slot = this.prewarmedConnections.get(callId);
|
|
1939
2101
|
if (slot === void 0) return void 0;
|
|
@@ -2035,7 +2197,7 @@ var Patter = class {
|
|
|
2035
2197
|
}
|
|
2036
2198
|
if (wantsRealtimePark) {
|
|
2037
2199
|
tasks.push((async () => {
|
|
2038
|
-
const { OpenAIRealtime2Adapter: OpenAIRealtime2Adapter2 } = await import("./openai-realtime-2-
|
|
2200
|
+
const { OpenAIRealtime2Adapter: OpenAIRealtime2Adapter2 } = await import("./openai-realtime-2-O4DP3LXN.mjs");
|
|
2039
2201
|
const apiKey = process.env.OPENAI_API_KEY ?? "";
|
|
2040
2202
|
if (!apiKey) {
|
|
2041
2203
|
getLogger().debug(`Park OpenAI Realtime skipped for ${callId}: no OPENAI_API_KEY`);
|
|
@@ -2249,6 +2411,12 @@ var Patter = class {
|
|
|
2249
2411
|
if (!options.to) {
|
|
2250
2412
|
throw new Error("'to' phone number is required");
|
|
2251
2413
|
}
|
|
2414
|
+
if (options.firstMessage) {
|
|
2415
|
+
options = {
|
|
2416
|
+
...options,
|
|
2417
|
+
agent: { ...options.agent, firstMessage: options.firstMessage }
|
|
2418
|
+
};
|
|
2419
|
+
}
|
|
2252
2420
|
if (!/^\+[1-9]\d{6,14}$/.test(options.to)) {
|
|
2253
2421
|
throw new Error("'to' must be E.164 format (+<country><digits>). Got value with invalid format.");
|
|
2254
2422
|
}
|
|
@@ -2393,6 +2561,9 @@ var Patter = class {
|
|
|
2393
2561
|
this.parkProviderConnections(options.agent, plivoCallId);
|
|
2394
2562
|
}
|
|
2395
2563
|
}
|
|
2564
|
+
if (plivoCallId) {
|
|
2565
|
+
return this.maybeAwaitCompletion(options, plivoCallId, effectiveRingTimeout);
|
|
2566
|
+
}
|
|
2396
2567
|
return;
|
|
2397
2568
|
}
|
|
2398
2569
|
const twilioSid = carrier.accountSid;
|
|
@@ -2530,7 +2701,7 @@ var Patter = class {
|
|
|
2530
2701
|
* entries leak across ``serve`` / ``disconnect`` cycles. See FIX #93.
|
|
2531
2702
|
*/
|
|
2532
2703
|
async disconnect() {
|
|
2533
|
-
this.telemetry.
|
|
2704
|
+
await this.telemetry.drain();
|
|
2534
2705
|
for (const handle of this.prewarmTtlTimers.values()) {
|
|
2535
2706
|
clearTimeout(handle);
|
|
2536
2707
|
}
|
|
@@ -2538,7 +2709,7 @@ var Patter = class {
|
|
|
2538
2709
|
if (this.prewarmTasks.size > 0) {
|
|
2539
2710
|
const drain = Promise.allSettled(Array.from(this.prewarmTasks));
|
|
2540
2711
|
const timer = new Promise(
|
|
2541
|
-
(
|
|
2712
|
+
(resolve2) => setTimeout(resolve2, 1e3).unref?.()
|
|
2542
2713
|
);
|
|
2543
2714
|
await Promise.race([drain, timer]);
|
|
2544
2715
|
}
|
|
@@ -2570,8 +2741,8 @@ var Patter = class {
|
|
|
2570
2741
|
this.localConfig = { ...this.localConfig, webhookUrl: void 0 };
|
|
2571
2742
|
this.tunnelOwnsWebhookUrl = false;
|
|
2572
2743
|
}
|
|
2573
|
-
this._tunnelReady = new Promise((
|
|
2574
|
-
this._tunnelReadyResolve =
|
|
2744
|
+
this._tunnelReady = new Promise((resolve2, reject) => {
|
|
2745
|
+
this._tunnelReadyResolve = resolve2;
|
|
2575
2746
|
this._tunnelReadyReject = reject;
|
|
2576
2747
|
});
|
|
2577
2748
|
this._tunnelReady.catch(() => {
|
|
@@ -2579,8 +2750,8 @@ var Patter = class {
|
|
|
2579
2750
|
if (this.localConfig.webhookUrl) {
|
|
2580
2751
|
this._tunnelReadyResolve(this.localConfig.webhookUrl);
|
|
2581
2752
|
}
|
|
2582
|
-
this._ready = new Promise((
|
|
2583
|
-
this._readyResolve =
|
|
2753
|
+
this._ready = new Promise((resolve2, reject) => {
|
|
2754
|
+
this._readyResolve = resolve2;
|
|
2584
2755
|
this._readyReject = reject;
|
|
2585
2756
|
});
|
|
2586
2757
|
this._ready.catch(() => {
|
|
@@ -3317,139 +3488,431 @@ function resultFromCallResult(result) {
|
|
|
3317
3488
|
|
|
3318
3489
|
// src/providers/gemini-live.ts
|
|
3319
3490
|
init_esm_shims();
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3491
|
+
|
|
3492
|
+
// src/providers/google-llm.ts
|
|
3493
|
+
init_esm_shims();
|
|
3494
|
+
var GoogleModel = {
|
|
3495
|
+
GEMINI_2_5_FLASH: "gemini-2.5-flash",
|
|
3496
|
+
GEMINI_2_5_PRO: "gemini-2.5-pro",
|
|
3497
|
+
GEMINI_2_0_FLASH: "gemini-2.0-flash",
|
|
3498
|
+
GEMINI_2_0_FLASH_LITE: "gemini-2.0-flash-lite",
|
|
3499
|
+
GEMINI_1_5_FLASH: "gemini-1.5-flash",
|
|
3500
|
+
GEMINI_1_5_PRO: "gemini-1.5-pro"
|
|
3501
|
+
};
|
|
3502
|
+
var DEFAULT_MODEL = GoogleModel.GEMINI_2_5_FLASH;
|
|
3503
|
+
var DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
|
3504
|
+
var GoogleLLMProvider = class {
|
|
3505
|
+
/** Stable pricing/dashboard key — read by stream-handler/metrics. */
|
|
3506
|
+
static providerKey = "google";
|
|
3334
3507
|
apiKey;
|
|
3335
3508
|
model;
|
|
3336
|
-
|
|
3337
|
-
instructions;
|
|
3338
|
-
language;
|
|
3339
|
-
tools;
|
|
3340
|
-
inputSampleRate;
|
|
3341
|
-
/** Output sample rate — exposed so callers can configure downstream transcoding. */
|
|
3342
|
-
outputSampleRate;
|
|
3509
|
+
baseUrl;
|
|
3343
3510
|
temperature;
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
handlers = [];
|
|
3348
|
-
running = false;
|
|
3349
|
-
/**
|
|
3350
|
-
* Tracks call_id -> function name so tool responses can be sent back with
|
|
3351
|
-
* the correct `name` field (Gemini expects the original function name,
|
|
3352
|
-
* not the call_id).
|
|
3353
|
-
*/
|
|
3354
|
-
pendingToolCalls = /* @__PURE__ */ new Map();
|
|
3355
|
-
/** Lazily import @google/genai, open a Live session, and start the receive loop. */
|
|
3356
|
-
async connect() {
|
|
3357
|
-
let genaiModule;
|
|
3358
|
-
try {
|
|
3359
|
-
const modName = "@google/genai";
|
|
3360
|
-
genaiModule = await import(modName);
|
|
3361
|
-
} catch {
|
|
3511
|
+
maxOutputTokens;
|
|
3512
|
+
constructor(options) {
|
|
3513
|
+
if (!options.apiKey) {
|
|
3362
3514
|
throw new Error(
|
|
3363
|
-
|
|
3515
|
+
"Google API key is required. Pass it via { apiKey } or read GOOGLE_API_KEY from the environment."
|
|
3364
3516
|
);
|
|
3365
3517
|
}
|
|
3366
|
-
|
|
3367
|
-
this.
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
description: t.description,
|
|
3388
|
-
parameters: t.parameters
|
|
3389
|
-
}))
|
|
3390
|
-
}
|
|
3391
|
-
];
|
|
3392
|
-
}
|
|
3393
|
-
const liveApi = this.client.live;
|
|
3394
|
-
if (!liveApi?.connect) {
|
|
3395
|
-
throw new Error("@google/genai: live.connect is not available in this version");
|
|
3518
|
+
this.apiKey = options.apiKey;
|
|
3519
|
+
this.model = options.model ?? DEFAULT_MODEL;
|
|
3520
|
+
this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
3521
|
+
this.temperature = options.temperature;
|
|
3522
|
+
this.maxOutputTokens = options.maxOutputTokens;
|
|
3523
|
+
}
|
|
3524
|
+
/**
|
|
3525
|
+
* Pre-call DNS / TLS warmup for the Gemini API.
|
|
3526
|
+
* Issues a lightweight ``GET ${baseUrl}/models?key=...`` so DNS, TLS
|
|
3527
|
+
* and HTTP/2 are already up by the time the first
|
|
3528
|
+
* ``streamGenerateContent`` call lands. Best-effort: 5 s timeout, all
|
|
3529
|
+
* exceptions swallowed at debug level.
|
|
3530
|
+
*/
|
|
3531
|
+
async warmup() {
|
|
3532
|
+
try {
|
|
3533
|
+
await fetch(`${this.baseUrl}/models?key=${encodeURIComponent(this.apiKey)}`, {
|
|
3534
|
+
method: "GET",
|
|
3535
|
+
signal: AbortSignal.timeout(5e3)
|
|
3536
|
+
});
|
|
3537
|
+
} catch (err) {
|
|
3538
|
+
getLogger().debug(`Google LLM warmup failed (best-effort): ${String(err)}`);
|
|
3396
3539
|
}
|
|
3397
|
-
this.session = await liveApi.connect({ model: this.model, config });
|
|
3398
|
-
this.running = true;
|
|
3399
|
-
this.receiveLoop = this.pumpReceive().catch((err) => {
|
|
3400
|
-
getLogger().error(`Gemini Live receive loop error: ${String(err)}`);
|
|
3401
|
-
});
|
|
3402
3540
|
}
|
|
3403
|
-
/**
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
const
|
|
3407
|
-
const
|
|
3408
|
-
|
|
3409
|
-
|
|
3541
|
+
/** Stream Patter-format LLM chunks from the Gemini SSE endpoint. */
|
|
3542
|
+
async *stream(messages, tools, opts) {
|
|
3543
|
+
const { systemInstruction, contents } = toGeminiContents(messages);
|
|
3544
|
+
const geminiTools = tools ? toGeminiTools(tools) : null;
|
|
3545
|
+
const body = { contents };
|
|
3546
|
+
if (systemInstruction) {
|
|
3547
|
+
body.systemInstruction = { role: "system", parts: [{ text: systemInstruction }] };
|
|
3548
|
+
}
|
|
3549
|
+
if (geminiTools) body.tools = geminiTools;
|
|
3550
|
+
const generationConfig = {};
|
|
3551
|
+
if (this.temperature !== void 0) generationConfig.temperature = this.temperature;
|
|
3552
|
+
if (this.maxOutputTokens !== void 0)
|
|
3553
|
+
generationConfig.maxOutputTokens = this.maxOutputTokens;
|
|
3554
|
+
if (Object.keys(generationConfig).length > 0) body.generationConfig = generationConfig;
|
|
3555
|
+
const url = `${this.baseUrl}/models/${encodeURIComponent(this.model)}:streamGenerateContent?alt=sse&key=${encodeURIComponent(this.apiKey)}`;
|
|
3556
|
+
const idle = createStreamIdleWatchdog();
|
|
3557
|
+
const response = await fetch(url, {
|
|
3558
|
+
method: "POST",
|
|
3559
|
+
headers: { "Content-Type": "application/json" },
|
|
3560
|
+
body: JSON.stringify(body),
|
|
3561
|
+
signal: mergeAbortSignals(opts?.signal, idle.signal)
|
|
3410
3562
|
});
|
|
3411
|
-
if (
|
|
3412
|
-
|
|
3413
|
-
|
|
3563
|
+
if (!response.ok) {
|
|
3564
|
+
const errText = await response.text();
|
|
3565
|
+
getLogger().error(`Gemini API error: ${response.status} ${errText.slice(0, 200)}`);
|
|
3566
|
+
throw new PatterConnectionError(
|
|
3567
|
+
`Gemini API returned ${response.status}: ${errText.slice(0, 200)}`
|
|
3414
3568
|
);
|
|
3415
3569
|
}
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3570
|
+
const reader = response.body?.getReader();
|
|
3571
|
+
if (!reader) return;
|
|
3572
|
+
const decoder = new TextDecoder();
|
|
3573
|
+
let buffer = "";
|
|
3574
|
+
let nextIndex = 0;
|
|
3575
|
+
let lastUsage;
|
|
3576
|
+
try {
|
|
3577
|
+
while (true) {
|
|
3578
|
+
const { done, value } = await reader.read();
|
|
3579
|
+
idle.touch();
|
|
3580
|
+
if (done) break;
|
|
3581
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3582
|
+
const lines = buffer.split("\n");
|
|
3583
|
+
buffer = lines.pop() || "";
|
|
3584
|
+
for (const line of lines) {
|
|
3585
|
+
const trimmed = line.trim();
|
|
3586
|
+
if (!trimmed.startsWith("data: ")) continue;
|
|
3587
|
+
const data = trimmed.slice(6);
|
|
3588
|
+
if (!data) continue;
|
|
3589
|
+
let payload;
|
|
3590
|
+
try {
|
|
3591
|
+
payload = JSON.parse(data);
|
|
3592
|
+
} catch {
|
|
3593
|
+
continue;
|
|
3594
|
+
}
|
|
3595
|
+
if (payload.usageMetadata) {
|
|
3596
|
+
lastUsage = payload.usageMetadata;
|
|
3597
|
+
}
|
|
3598
|
+
const candidate = payload.candidates?.[0];
|
|
3599
|
+
const parts = candidate?.content?.parts ?? [];
|
|
3600
|
+
for (const part of parts) {
|
|
3601
|
+
if (part.functionCall) {
|
|
3602
|
+
const args = part.functionCall.args ?? {};
|
|
3603
|
+
const callId = part.functionCall.id ?? `gemini_call_${nextIndex}`;
|
|
3604
|
+
yield {
|
|
3605
|
+
type: "tool_call",
|
|
3606
|
+
index: nextIndex,
|
|
3607
|
+
id: callId,
|
|
3608
|
+
name: part.functionCall.name ?? "",
|
|
3609
|
+
arguments: JSON.stringify(args)
|
|
3610
|
+
};
|
|
3611
|
+
nextIndex++;
|
|
3612
|
+
continue;
|
|
3613
|
+
}
|
|
3614
|
+
if (part.text) {
|
|
3615
|
+
yield { type: "text", content: part.text };
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
} catch (err) {
|
|
3621
|
+
if (idle.fired && !opts?.signal?.aborted) {
|
|
3622
|
+
throw new PatterConnectionError(
|
|
3623
|
+
`Gemini stream idle timeout \u2014 no data for ${LLM_STREAM_IDLE_TIMEOUT_MS / 1e3}s`
|
|
3624
|
+
);
|
|
3625
|
+
}
|
|
3626
|
+
throw err;
|
|
3627
|
+
} finally {
|
|
3628
|
+
idle.clear();
|
|
3629
|
+
reader.cancel().catch(() => {
|
|
3630
|
+
});
|
|
3631
|
+
}
|
|
3632
|
+
if (lastUsage) {
|
|
3633
|
+
const cached = lastUsage.cachedContentTokenCount ?? 0;
|
|
3634
|
+
yield {
|
|
3635
|
+
type: "usage",
|
|
3636
|
+
inputTokens: Math.max(0, (lastUsage.promptTokenCount ?? 0) - cached),
|
|
3637
|
+
outputTokens: lastUsage.candidatesTokenCount,
|
|
3638
|
+
cacheReadInputTokens: cached
|
|
3639
|
+
};
|
|
3640
|
+
}
|
|
3641
|
+
yield { type: "done" };
|
|
3642
|
+
}
|
|
3643
|
+
};
|
|
3644
|
+
var GEMINI_SCHEMA_KEYS = /* @__PURE__ */ new Set([
|
|
3645
|
+
"type",
|
|
3646
|
+
"description",
|
|
3647
|
+
"properties",
|
|
3648
|
+
"items",
|
|
3649
|
+
"enum",
|
|
3650
|
+
"required",
|
|
3651
|
+
"nullable",
|
|
3652
|
+
"format",
|
|
3653
|
+
"minimum",
|
|
3654
|
+
"maximum",
|
|
3655
|
+
"minLength",
|
|
3656
|
+
"maxLength",
|
|
3657
|
+
"minItems",
|
|
3658
|
+
"maxItems",
|
|
3659
|
+
"pattern",
|
|
3660
|
+
"anyOf",
|
|
3661
|
+
"default",
|
|
3662
|
+
"title"
|
|
3663
|
+
]);
|
|
3664
|
+
function sanitizeGeminiSchema(schema) {
|
|
3665
|
+
if (Array.isArray(schema)) return schema.map(sanitizeGeminiSchema);
|
|
3666
|
+
if (schema !== null && typeof schema === "object") {
|
|
3667
|
+
const out = {};
|
|
3668
|
+
for (const [k, v] of Object.entries(schema)) {
|
|
3669
|
+
if (GEMINI_SCHEMA_KEYS.has(k)) out[k] = sanitizeGeminiSchema(v);
|
|
3670
|
+
}
|
|
3671
|
+
return out;
|
|
3672
|
+
}
|
|
3673
|
+
return schema;
|
|
3674
|
+
}
|
|
3675
|
+
function toGeminiTools(tools) {
|
|
3676
|
+
const functionDeclarations = tools.map((t) => {
|
|
3677
|
+
const fn = t.function ?? t;
|
|
3678
|
+
return {
|
|
3679
|
+
name: String(fn.name ?? ""),
|
|
3680
|
+
description: String(fn.description ?? ""),
|
|
3681
|
+
parameters: sanitizeGeminiSchema(fn.parameters ?? { type: "object", properties: {} })
|
|
3682
|
+
};
|
|
3683
|
+
});
|
|
3684
|
+
if (functionDeclarations.length === 0) return [];
|
|
3685
|
+
return [{ functionDeclarations }];
|
|
3686
|
+
}
|
|
3687
|
+
function toGeminiContents(messages) {
|
|
3688
|
+
const systemParts = [];
|
|
3689
|
+
const contents = [];
|
|
3690
|
+
const fnNameByCallId = /* @__PURE__ */ new Map();
|
|
3691
|
+
for (const rawMsg of messages) {
|
|
3692
|
+
const role = rawMsg.role;
|
|
3693
|
+
if (role === "system") {
|
|
3694
|
+
if (typeof rawMsg.content === "string" && rawMsg.content) {
|
|
3695
|
+
systemParts.push(rawMsg.content);
|
|
3696
|
+
}
|
|
3697
|
+
continue;
|
|
3698
|
+
}
|
|
3699
|
+
if (role === "user") {
|
|
3700
|
+
if (typeof rawMsg.content === "string" && rawMsg.content) {
|
|
3701
|
+
contents.push({ role: "user", parts: [{ text: rawMsg.content }] });
|
|
3702
|
+
}
|
|
3703
|
+
continue;
|
|
3704
|
+
}
|
|
3705
|
+
if (role === "assistant") {
|
|
3706
|
+
const parts = [];
|
|
3707
|
+
if (typeof rawMsg.content === "string" && rawMsg.content) {
|
|
3708
|
+
parts.push({ text: rawMsg.content });
|
|
3709
|
+
}
|
|
3710
|
+
for (const tc of rawMsg.tool_calls ?? []) {
|
|
3711
|
+
let args = {};
|
|
3712
|
+
try {
|
|
3713
|
+
const parsed = JSON.parse(tc.function?.arguments ?? "{}");
|
|
3714
|
+
if (parsed && typeof parsed === "object") args = parsed;
|
|
3715
|
+
} catch {
|
|
3716
|
+
args = {};
|
|
3717
|
+
}
|
|
3718
|
+
if (tc.id && tc.function?.name) fnNameByCallId.set(tc.id, tc.function.name);
|
|
3719
|
+
parts.push({
|
|
3720
|
+
functionCall: {
|
|
3721
|
+
name: tc.function?.name ?? "",
|
|
3722
|
+
args,
|
|
3723
|
+
id: tc.id
|
|
3724
|
+
}
|
|
3725
|
+
});
|
|
3726
|
+
}
|
|
3727
|
+
if (parts.length > 0) contents.push({ role: "model", parts });
|
|
3728
|
+
continue;
|
|
3729
|
+
}
|
|
3730
|
+
if (role === "tool") {
|
|
3731
|
+
const raw = rawMsg.content;
|
|
3732
|
+
let response;
|
|
3733
|
+
if (typeof raw === "string") {
|
|
3734
|
+
try {
|
|
3735
|
+
const parsed = JSON.parse(raw);
|
|
3736
|
+
response = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { result: parsed };
|
|
3737
|
+
} catch {
|
|
3738
|
+
response = { result: raw };
|
|
3739
|
+
}
|
|
3740
|
+
} else {
|
|
3741
|
+
response = raw ?? {};
|
|
3742
|
+
}
|
|
3743
|
+
contents.push({
|
|
3744
|
+
role: "user",
|
|
3745
|
+
parts: [
|
|
3746
|
+
{
|
|
3747
|
+
functionResponse: {
|
|
3748
|
+
name: rawMsg.name ?? fnNameByCallId.get(rawMsg.tool_call_id ?? "") ?? rawMsg.tool_call_id ?? "",
|
|
3749
|
+
response,
|
|
3750
|
+
id: rawMsg.tool_call_id
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
]
|
|
3754
|
+
});
|
|
3755
|
+
continue;
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3758
|
+
const merged = [];
|
|
3759
|
+
for (const entry of contents) {
|
|
3760
|
+
const prev = merged[merged.length - 1];
|
|
3761
|
+
const isFunctionResponseOnly = (c) => c.role === "user" && c.parts.every((p) => p.functionResponse !== void 0);
|
|
3762
|
+
if (prev && isFunctionResponseOnly(prev) && isFunctionResponseOnly(entry)) {
|
|
3763
|
+
prev.parts.push(...entry.parts);
|
|
3764
|
+
} else {
|
|
3765
|
+
merged.push(entry);
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
if (merged.length > 0 && merged[0].role === "model") {
|
|
3769
|
+
merged.unshift({ role: "user", parts: [{ text: "(call connected)" }] });
|
|
3770
|
+
}
|
|
3771
|
+
return { systemInstruction: systemParts.join("\n\n"), contents: merged };
|
|
3772
|
+
}
|
|
3773
|
+
|
|
3774
|
+
// src/providers/gemini-live.ts
|
|
3775
|
+
var GEMINI_DEFAULT_INPUT_SR = 16e3;
|
|
3776
|
+
var GEMINI_DEFAULT_OUTPUT_SR = 24e3;
|
|
3777
|
+
var GeminiLiveAdapter = class {
|
|
3778
|
+
constructor(apiKey, options = {}) {
|
|
3779
|
+
this.apiKey = apiKey;
|
|
3780
|
+
this.model = options.model ?? "gemini-2.5-flash-native-audio-preview-09-2025";
|
|
3781
|
+
this.voice = options.voice ?? "Puck";
|
|
3782
|
+
this.instructions = options.instructions ?? "";
|
|
3783
|
+
this.language = options.language ?? "en-US";
|
|
3784
|
+
this.tools = options.tools;
|
|
3785
|
+
this.inputSampleRate = options.inputSampleRate ?? GEMINI_DEFAULT_INPUT_SR;
|
|
3786
|
+
this.outputSampleRate = options.outputSampleRate ?? GEMINI_DEFAULT_OUTPUT_SR;
|
|
3787
|
+
this.temperature = options.temperature ?? 0.8;
|
|
3788
|
+
}
|
|
3789
|
+
apiKey;
|
|
3790
|
+
model;
|
|
3791
|
+
voice;
|
|
3792
|
+
instructions;
|
|
3793
|
+
language;
|
|
3794
|
+
tools;
|
|
3795
|
+
inputSampleRate;
|
|
3796
|
+
/** Output sample rate — exposed so callers can configure downstream transcoding. */
|
|
3797
|
+
outputSampleRate;
|
|
3798
|
+
temperature;
|
|
3799
|
+
client = null;
|
|
3800
|
+
session = null;
|
|
3801
|
+
receiveLoop = null;
|
|
3802
|
+
handlers = [];
|
|
3803
|
+
running = false;
|
|
3804
|
+
/**
|
|
3805
|
+
* Tracks call_id -> function name so tool responses can be sent back with
|
|
3806
|
+
* the correct `name` field (Gemini expects the original function name,
|
|
3807
|
+
* not the call_id).
|
|
3808
|
+
*/
|
|
3809
|
+
pendingToolCalls = /* @__PURE__ */ new Map();
|
|
3810
|
+
/** Lazily import @google/genai, open a Live session, and start the receive loop. */
|
|
3811
|
+
async connect() {
|
|
3812
|
+
let genaiModule;
|
|
3813
|
+
try {
|
|
3814
|
+
const modName = "@google/genai";
|
|
3815
|
+
genaiModule = await import(modName);
|
|
3816
|
+
} catch {
|
|
3817
|
+
throw new Error(
|
|
3818
|
+
'\nGemini Live requires the "@google/genai" package, which is not installed.\n\n Install: npm install @google/genai\n\nThis is an optional peer dependency of getpatter \u2014 it is only needed when\nyou use GeminiLive as an agent engine. Other LLM/engine providers do not\nrequire it.\n'
|
|
3819
|
+
);
|
|
3820
|
+
}
|
|
3821
|
+
const { GoogleGenAI } = genaiModule;
|
|
3822
|
+
this.client = new GoogleGenAI({
|
|
3823
|
+
apiKey: this.apiKey,
|
|
3824
|
+
httpOptions: { apiVersion: "v1alpha" }
|
|
3825
|
+
});
|
|
3826
|
+
const config = {
|
|
3827
|
+
responseModalities: ["AUDIO"],
|
|
3828
|
+
speechConfig: {
|
|
3829
|
+
voiceConfig: { prebuiltVoiceConfig: { voiceName: this.voice } },
|
|
3830
|
+
languageCode: this.language
|
|
3831
|
+
},
|
|
3832
|
+
temperature: this.temperature,
|
|
3833
|
+
// Without these, native-audio sessions produced NO user transcript
|
|
3834
|
+
// ever and no assistant transcript in AUDIO modality — logs/history/
|
|
3835
|
+
// metrics got nothing for Gemini Live calls. Mirrors Python.
|
|
3836
|
+
inputAudioTranscription: {},
|
|
3837
|
+
outputAudioTranscription: {}
|
|
3838
|
+
};
|
|
3839
|
+
if (this.instructions) {
|
|
3840
|
+
config.systemInstruction = { parts: [{ text: this.instructions }] };
|
|
3841
|
+
}
|
|
3842
|
+
if (this.tools?.length) {
|
|
3843
|
+
config.tools = [
|
|
3844
|
+
{
|
|
3845
|
+
functionDeclarations: this.tools.map((t) => ({
|
|
3846
|
+
name: t.name,
|
|
3847
|
+
description: t.description,
|
|
3848
|
+
// Strip JSON-Schema keys the Live API's proto Schema rejects
|
|
3849
|
+
// ($schema, additionalProperties — strict-mode and zod-derived
|
|
3850
|
+
// MCP tools): one such tool 400'd the whole session.
|
|
3851
|
+
parameters: sanitizeGeminiSchema(t.parameters)
|
|
3852
|
+
}))
|
|
3853
|
+
}
|
|
3854
|
+
];
|
|
3855
|
+
}
|
|
3856
|
+
const liveApi = this.client.live;
|
|
3857
|
+
if (!liveApi?.connect) {
|
|
3858
|
+
throw new Error("@google/genai: live.connect is not available in this version");
|
|
3859
|
+
}
|
|
3860
|
+
this.session = await liveApi.connect({ model: this.model, config });
|
|
3861
|
+
this.running = true;
|
|
3862
|
+
this.receiveLoop = this.pumpReceive().catch((err) => {
|
|
3863
|
+
getLogger().error(`Gemini Live receive loop error: ${String(err)}`);
|
|
3864
|
+
});
|
|
3865
|
+
}
|
|
3866
|
+
/** Send a PCM audio chunk to Gemini as base64 inline data. */
|
|
3867
|
+
sendAudio(pcm) {
|
|
3868
|
+
if (!this.session || !this.running) return;
|
|
3869
|
+
const mime = `audio/pcm;rate=${this.inputSampleRate}`;
|
|
3870
|
+
const sess = this.session;
|
|
3871
|
+
const result = sess.sendRealtimeInput?.({
|
|
3872
|
+
media: { data: pcm.toString("base64"), mimeType: mime }
|
|
3873
|
+
});
|
|
3874
|
+
if (result instanceof Promise) {
|
|
3875
|
+
void result.catch(
|
|
3876
|
+
(err) => getLogger().warn(`Gemini Live sendAudio error: ${String(err)}`)
|
|
3877
|
+
);
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
/** Send a text turn to Gemini and mark the turn complete. */
|
|
3881
|
+
async sendText(text) {
|
|
3882
|
+
if (!this.session) return;
|
|
3883
|
+
const sess = this.session;
|
|
3884
|
+
await sess.sendClientContent?.({
|
|
3885
|
+
turns: { role: "user", parts: [{ text }] },
|
|
3886
|
+
turnComplete: true
|
|
3887
|
+
});
|
|
3888
|
+
}
|
|
3889
|
+
/** Send a tool/function-call result back to Gemini. */
|
|
3890
|
+
async sendFunctionResult(callId, result) {
|
|
3891
|
+
if (!this.session) return;
|
|
3892
|
+
const sess = this.session;
|
|
3893
|
+
const name = this.pendingToolCalls.get(callId) ?? callId;
|
|
3894
|
+
this.pendingToolCalls.delete(callId);
|
|
3895
|
+
await sess.sendToolResponse?.({
|
|
3896
|
+
functionResponses: [
|
|
3897
|
+
{ id: callId, name, response: { result } }
|
|
3898
|
+
]
|
|
3899
|
+
});
|
|
3900
|
+
}
|
|
3901
|
+
/** No-op — Gemini Live barge-in is VAD-driven, not client-cancelled. */
|
|
3902
|
+
cancelResponse() {
|
|
3903
|
+
getLogger().debug("Gemini Live: cancelResponse is implicit via VAD");
|
|
3904
|
+
}
|
|
3905
|
+
/** Register an event handler that receives every Gemini Live event. */
|
|
3906
|
+
onEvent(handler) {
|
|
3907
|
+
this.handlers.push(handler);
|
|
3908
|
+
}
|
|
3909
|
+
async emit(type, data) {
|
|
3910
|
+
for (const h of this.handlers) {
|
|
3911
|
+
try {
|
|
3912
|
+
await h(type, data);
|
|
3913
|
+
} catch (err) {
|
|
3914
|
+
getLogger().error(`Gemini Live handler threw: ${String(err)}`);
|
|
3915
|
+
}
|
|
3453
3916
|
}
|
|
3454
3917
|
}
|
|
3455
3918
|
async pumpReceive() {
|
|
@@ -3471,9 +3934,20 @@ var GeminiLiveAdapter = class {
|
|
|
3471
3934
|
}
|
|
3472
3935
|
if (part.text) await this.emit("transcript_output", part.text);
|
|
3473
3936
|
}
|
|
3937
|
+
if (sc.inputTranscription?.text) {
|
|
3938
|
+
await this.emit("transcript_input", sc.inputTranscription.text);
|
|
3939
|
+
}
|
|
3940
|
+
if (sc.outputTranscription?.text) {
|
|
3941
|
+
await this.emit("transcript_output", sc.outputTranscription.text);
|
|
3942
|
+
}
|
|
3474
3943
|
if (sc.turnComplete) await this.emit("response_done", null);
|
|
3475
3944
|
if (sc.interrupted) await this.emit("speech_started", null);
|
|
3476
3945
|
}
|
|
3946
|
+
if (r.goAway) {
|
|
3947
|
+
getLogger().warn(
|
|
3948
|
+
`Gemini Live goAway received \u2014 session ends in ${r.goAway.timeLeft ?? "unknown"}`
|
|
3949
|
+
);
|
|
3950
|
+
}
|
|
3477
3951
|
if (r.toolCall) {
|
|
3478
3952
|
for (const fn of r.toolCall.functionCalls ?? []) {
|
|
3479
3953
|
const args = fn.args ?? {};
|
|
@@ -3543,6 +4017,10 @@ var UltravoxRealtimeAdapter = class {
|
|
|
3543
4017
|
sampleRate;
|
|
3544
4018
|
firstMessage;
|
|
3545
4019
|
ws = null;
|
|
4020
|
+
/** Last Ultravox state string (turn-end transition detection). */
|
|
4021
|
+
lastUltravoxState = "";
|
|
4022
|
+
/** Whether the current agent turn streamed delta frames (dedupe finals). */
|
|
4023
|
+
agentStreamedDeltas = false;
|
|
3546
4024
|
handlers = [];
|
|
3547
4025
|
/** Exposed for diagnostics — true while the underlying socket is open. */
|
|
3548
4026
|
running = false;
|
|
@@ -3594,7 +4072,7 @@ var UltravoxRealtimeAdapter = class {
|
|
|
3594
4072
|
const call = await resp.json();
|
|
3595
4073
|
if (!call.joinUrl) throw new Error("Ultravox response missing joinUrl");
|
|
3596
4074
|
this.ws = new WebSocket(call.joinUrl);
|
|
3597
|
-
await new Promise((
|
|
4075
|
+
await new Promise((resolve2, reject) => {
|
|
3598
4076
|
const ws = this.ws;
|
|
3599
4077
|
let settled = false;
|
|
3600
4078
|
const timer = setTimeout(() => {
|
|
@@ -3614,7 +4092,7 @@ var UltravoxRealtimeAdapter = class {
|
|
|
3614
4092
|
settled = true;
|
|
3615
4093
|
clearTimeout(timer);
|
|
3616
4094
|
ws.off("error", onError);
|
|
3617
|
-
|
|
4095
|
+
resolve2();
|
|
3618
4096
|
};
|
|
3619
4097
|
const onError = (err) => {
|
|
3620
4098
|
if (settled) return;
|
|
@@ -3697,10 +4175,20 @@ var UltravoxRealtimeAdapter = class {
|
|
|
3697
4175
|
const etype = event.type ?? "";
|
|
3698
4176
|
if (etype === "transcript") {
|
|
3699
4177
|
const role = event.role;
|
|
3700
|
-
const
|
|
4178
|
+
const delta = event.delta ?? "";
|
|
4179
|
+
const fullText = event.text ?? "";
|
|
3701
4180
|
const isFinal = Boolean(event.final);
|
|
3702
|
-
if (role === "user" && isFinal &&
|
|
3703
|
-
|
|
4181
|
+
if (role === "user" && isFinal && (fullText || delta)) {
|
|
4182
|
+
await this.emit("transcript_input", fullText || delta);
|
|
4183
|
+
} else if (role === "agent") {
|
|
4184
|
+
if (delta) {
|
|
4185
|
+
await this.emit("transcript_output", delta);
|
|
4186
|
+
this.agentStreamedDeltas = true;
|
|
4187
|
+
} else if (isFinal && fullText && !this.agentStreamedDeltas) {
|
|
4188
|
+
await this.emit("transcript_output", fullText);
|
|
4189
|
+
}
|
|
4190
|
+
if (isFinal) this.agentStreamedDeltas = false;
|
|
4191
|
+
}
|
|
3704
4192
|
} else if (etype === "client_tool_invocation") {
|
|
3705
4193
|
await this.emit("function_call", {
|
|
3706
4194
|
call_id: event.invocationId ?? "",
|
|
@@ -3709,8 +4197,13 @@ var UltravoxRealtimeAdapter = class {
|
|
|
3709
4197
|
});
|
|
3710
4198
|
} else if (etype === "state") {
|
|
3711
4199
|
const state = event.state;
|
|
3712
|
-
|
|
3713
|
-
|
|
4200
|
+
const prev = this.lastUltravoxState;
|
|
4201
|
+
this.lastUltravoxState = state ?? "";
|
|
4202
|
+
if (state === "listening" && prev === "speaking") {
|
|
4203
|
+
await this.emit("response_done", null);
|
|
4204
|
+
} else if (state === "idle") {
|
|
4205
|
+
await this.emit("response_done", null);
|
|
4206
|
+
}
|
|
3714
4207
|
} else if (etype === "playback_clear_buffer") {
|
|
3715
4208
|
await this.emit("speech_started", null);
|
|
3716
4209
|
}
|
|
@@ -3808,14 +4301,23 @@ function scheduleCron(cron, callback) {
|
|
|
3808
4301
|
};
|
|
3809
4302
|
}
|
|
3810
4303
|
function scheduleOnce(at, callback) {
|
|
3811
|
-
const delayMs = at.getTime() - Date.now();
|
|
3812
4304
|
let cancelled = false;
|
|
3813
4305
|
let done = false;
|
|
3814
|
-
const
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
4306
|
+
const MAX_TIMEOUT_MS = 2147483647;
|
|
4307
|
+
let timer;
|
|
4308
|
+
const arm = () => {
|
|
4309
|
+
const remaining = at.getTime() - Date.now();
|
|
4310
|
+
if (remaining > MAX_TIMEOUT_MS) {
|
|
4311
|
+
timer = setTimeout(arm, MAX_TIMEOUT_MS);
|
|
4312
|
+
return;
|
|
4313
|
+
}
|
|
4314
|
+
timer = setTimeout(() => {
|
|
4315
|
+
if (cancelled) return;
|
|
4316
|
+
done = true;
|
|
4317
|
+
wrapCallback(callback)();
|
|
4318
|
+
}, Math.max(0, remaining));
|
|
4319
|
+
};
|
|
4320
|
+
arm();
|
|
3819
4321
|
return {
|
|
3820
4322
|
jobId: `once-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
3821
4323
|
cancel() {
|
|
@@ -3911,7 +4413,9 @@ var ELEVENLABS_VOICE_ID_BY_NAME = {
|
|
|
3911
4413
|
var VOICE_ID_PATTERN = /^[A-Za-z0-9]{20}$/;
|
|
3912
4414
|
var CARRIER_NATIVE_FORMAT = {
|
|
3913
4415
|
twilio: "ulaw_8000",
|
|
3914
|
-
|
|
4416
|
+
// The SDK's streaming_start pins the Telnyx wire to PCMU/μ-law @ 8 kHz —
|
|
4417
|
+
// 'pcm_16000' here shipped raw PCM16 onto a μ-law wire (static).
|
|
4418
|
+
telnyx: "ulaw_8000",
|
|
3915
4419
|
// Plivo streams mulaw 8 kHz (we pin contentType in the answer XML).
|
|
3916
4420
|
plivo: "ulaw_8000"
|
|
3917
4421
|
};
|
|
@@ -4037,18 +4541,14 @@ var ElevenLabsTTS = class _ElevenLabsTTS {
|
|
|
4037
4541
|
/**
|
|
4038
4542
|
* Construct an instance pre-configured for Telnyx bidirectional media.
|
|
4039
4543
|
*
|
|
4040
|
-
*
|
|
4041
|
-
*
|
|
4042
|
-
*
|
|
4043
|
-
*
|
|
4044
|
-
* Trade-off: if your Telnyx profile is pinned to PCMU/8000 (μ-law),
|
|
4045
|
-
* construct `ElevenLabsTTS` directly with `outputFormat: 'ulaw_8000'`
|
|
4046
|
-
* — Telnyx supports that natively too.
|
|
4544
|
+
* The SDK's ``streaming_start`` pins the Telnyx wire to PCMU/μ-law @
|
|
4545
|
+
* 8 kHz (stream_bidirectional_codec=PCMU), so μ-law output flows
|
|
4546
|
+
* end-to-end with zero resampling or transcoding.
|
|
4047
4547
|
*/
|
|
4048
4548
|
static forTelnyx(apiKey, options = {}) {
|
|
4049
4549
|
return new _ElevenLabsTTS(apiKey, {
|
|
4050
4550
|
...options,
|
|
4051
|
-
outputFormat: ElevenLabsOutputFormat.
|
|
4551
|
+
outputFormat: ElevenLabsOutputFormat.ULAW_8000
|
|
4052
4552
|
});
|
|
4053
4553
|
}
|
|
4054
4554
|
/**
|
|
@@ -4182,7 +4682,7 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
4182
4682
|
static forTwilio(apiKey, options = {}) {
|
|
4183
4683
|
return new _CartesiaTTS(apiKey, {
|
|
4184
4684
|
...options,
|
|
4185
|
-
sampleRate: CartesiaTTSSampleRate.
|
|
4685
|
+
sampleRate: CartesiaTTSSampleRate.HZ_16000
|
|
4186
4686
|
});
|
|
4187
4687
|
}
|
|
4188
4688
|
/**
|
|
@@ -4526,7 +5026,10 @@ var WhisperSTT = class _WhisperSTT {
|
|
|
4526
5026
|
* ``(apiKey, model, language, bufferSize, responseFormat)`` — callers using
|
|
4527
5027
|
* the old order will need to swap ``language`` and ``model``.
|
|
4528
5028
|
*/
|
|
5029
|
+
/** Construction args replayed by clone(). */
|
|
5030
|
+
patterCtorArgs;
|
|
4529
5031
|
constructor(apiKey, language, model = "whisper-1", bufferSize = DEFAULT_BUFFER_SIZE, responseFormat = "json") {
|
|
5032
|
+
this.patterCtorArgs = [apiKey, language, model, bufferSize, responseFormat];
|
|
4530
5033
|
if (!ALLOWED_MODELS.has(model)) {
|
|
4531
5034
|
throw new Error(
|
|
4532
5035
|
`WhisperSTT: unsupported model "${model}". Expected one of ${[...ALLOWED_MODELS].join(", ")}.`
|
|
@@ -4543,6 +5046,15 @@ var WhisperSTT = class _WhisperSTT {
|
|
|
4543
5046
|
return new _WhisperSTT(apiKey, language, model);
|
|
4544
5047
|
}
|
|
4545
5048
|
/** Reset the audio buffer and arm the adapter for incoming chunks. */
|
|
5049
|
+
/**
|
|
5050
|
+
* Fresh adapter built with this instance's construction arguments —
|
|
5051
|
+
* called per call by the stream handler so concurrent calls never share
|
|
5052
|
+
* connection state (sockets/queues; cross-call transcript bleed).
|
|
5053
|
+
*/
|
|
5054
|
+
clone() {
|
|
5055
|
+
const ctor = this.constructor;
|
|
5056
|
+
return new ctor(...this.patterCtorArgs);
|
|
5057
|
+
}
|
|
4546
5058
|
async connect() {
|
|
4547
5059
|
this.running = true;
|
|
4548
5060
|
this.chunks = [];
|
|
@@ -4685,6 +5197,11 @@ var OpenAITranscribeSTT = class extends WhisperSTT {
|
|
|
4685
5197
|
`OpenAITranscribeSTT: unsupported model "${model}". Expected one of ${[...ALLOWED_MODELS2].join(", ")}. For "whisper-1", use WhisperSTT instead.`
|
|
4686
5198
|
);
|
|
4687
5199
|
}
|
|
5200
|
+
if (responseFormat === "verbose_json") {
|
|
5201
|
+
throw new Error(
|
|
5202
|
+
`OpenAITranscribeSTT: responseFormat "verbose_json" is only supported by whisper-1 (use WhisperSTT). "${model}" accepts "json".`
|
|
5203
|
+
);
|
|
5204
|
+
}
|
|
4688
5205
|
super(apiKey, language, model, bufferSize, responseFormat);
|
|
4689
5206
|
}
|
|
4690
5207
|
};
|
|
@@ -4731,7 +5248,7 @@ var CartesiaSTTServerEvent = {
|
|
|
4731
5248
|
var CartesiaSTTClientFrame = {
|
|
4732
5249
|
FINALIZE: "finalize"
|
|
4733
5250
|
};
|
|
4734
|
-
var
|
|
5251
|
+
var DEFAULT_BASE_URL2 = "https://api.cartesia.ai";
|
|
4735
5252
|
var API_VERSION = "2025-04-16";
|
|
4736
5253
|
var USER_AGENT = "Patter/1.0";
|
|
4737
5254
|
var KEEPALIVE_INTERVAL_MS = 3e4;
|
|
@@ -4740,6 +5257,7 @@ var CartesiaSTT = class {
|
|
|
4740
5257
|
constructor(apiKey, options = {}) {
|
|
4741
5258
|
this.apiKey = apiKey;
|
|
4742
5259
|
this.options = options;
|
|
5260
|
+
this.patterCtorArgs = [apiKey, options];
|
|
4743
5261
|
if (!apiKey) {
|
|
4744
5262
|
throw new Error("CartesiaSTT requires a non-empty apiKey");
|
|
4745
5263
|
}
|
|
@@ -4756,6 +5274,8 @@ var CartesiaSTT = class {
|
|
|
4756
5274
|
* `null` until the first transcript event arrives (matches Python's `None`).
|
|
4757
5275
|
*/
|
|
4758
5276
|
requestId = null;
|
|
5277
|
+
/** Construction args replayed by clone(). */
|
|
5278
|
+
patterCtorArgs;
|
|
4759
5279
|
/**
|
|
4760
5280
|
* Open a fresh WebSocket without arming any message / keepalive handlers
|
|
4761
5281
|
* and without taking ownership on `this.ws`. Returns the OPEN socket so
|
|
@@ -4771,14 +5291,14 @@ var CartesiaSTT = class {
|
|
|
4771
5291
|
const ws = new WebSocket2(url, {
|
|
4772
5292
|
headers: { "User-Agent": USER_AGENT }
|
|
4773
5293
|
});
|
|
4774
|
-
await new Promise((
|
|
5294
|
+
await new Promise((resolve2, reject) => {
|
|
4775
5295
|
const timer = setTimeout(
|
|
4776
5296
|
() => reject(new Error("Cartesia STT park connect timeout")),
|
|
4777
5297
|
CONNECT_TIMEOUT_MS
|
|
4778
5298
|
);
|
|
4779
5299
|
ws.once("open", () => {
|
|
4780
5300
|
clearTimeout(timer);
|
|
4781
|
-
|
|
5301
|
+
resolve2();
|
|
4782
5302
|
});
|
|
4783
5303
|
ws.once("error", (err) => {
|
|
4784
5304
|
clearTimeout(timer);
|
|
@@ -4789,7 +5309,7 @@ var CartesiaSTT = class {
|
|
|
4789
5309
|
}
|
|
4790
5310
|
buildWsUrl() {
|
|
4791
5311
|
const opts = this.options;
|
|
4792
|
-
const rawBase = opts.baseUrl ??
|
|
5312
|
+
const rawBase = opts.baseUrl ?? DEFAULT_BASE_URL2;
|
|
4793
5313
|
let base;
|
|
4794
5314
|
if (rawBase.startsWith("http://")) {
|
|
4795
5315
|
base = `ws://${rawBase.slice("http://".length)}`;
|
|
@@ -4828,7 +5348,7 @@ var CartesiaSTT = class {
|
|
|
4828
5348
|
const url = this.buildWsUrl();
|
|
4829
5349
|
let ws = null;
|
|
4830
5350
|
try {
|
|
4831
|
-
ws = await new Promise((
|
|
5351
|
+
ws = await new Promise((resolve2, reject) => {
|
|
4832
5352
|
const sock = new WebSocket2(url, {
|
|
4833
5353
|
headers: { "User-Agent": USER_AGENT }
|
|
4834
5354
|
});
|
|
@@ -4841,7 +5361,7 @@ var CartesiaSTT = class {
|
|
|
4841
5361
|
}, 5e3);
|
|
4842
5362
|
sock.once("open", () => {
|
|
4843
5363
|
clearTimeout(timer);
|
|
4844
|
-
|
|
5364
|
+
resolve2(sock);
|
|
4845
5365
|
});
|
|
4846
5366
|
sock.once("error", (err) => {
|
|
4847
5367
|
clearTimeout(timer);
|
|
@@ -4863,19 +5383,28 @@ var CartesiaSTT = class {
|
|
|
4863
5383
|
}
|
|
4864
5384
|
}
|
|
4865
5385
|
/** Open the streaming WebSocket and arm message + keepalive handlers. */
|
|
5386
|
+
/**
|
|
5387
|
+
* Fresh adapter built with this instance's construction arguments —
|
|
5388
|
+
* called per call by the stream handler so concurrent calls never share
|
|
5389
|
+
* connection state (sockets/queues; cross-call transcript bleed).
|
|
5390
|
+
*/
|
|
5391
|
+
clone() {
|
|
5392
|
+
const ctor = this.constructor;
|
|
5393
|
+
return new ctor(...this.patterCtorArgs);
|
|
5394
|
+
}
|
|
4866
5395
|
async connect() {
|
|
4867
5396
|
const url = this.buildWsUrl();
|
|
4868
5397
|
this.ws = new WebSocket2(url, {
|
|
4869
5398
|
headers: { "User-Agent": USER_AGENT }
|
|
4870
5399
|
});
|
|
4871
|
-
await new Promise((
|
|
5400
|
+
await new Promise((resolve2, reject) => {
|
|
4872
5401
|
const timer = setTimeout(
|
|
4873
5402
|
() => reject(new Error("Cartesia STT connect timeout")),
|
|
4874
5403
|
CONNECT_TIMEOUT_MS
|
|
4875
5404
|
);
|
|
4876
5405
|
this.ws.once("open", () => {
|
|
4877
5406
|
clearTimeout(timer);
|
|
4878
|
-
|
|
5407
|
+
resolve2();
|
|
4879
5408
|
});
|
|
4880
5409
|
this.ws.once("error", (err) => {
|
|
4881
5410
|
clearTimeout(timer);
|
|
@@ -4939,7 +5468,13 @@ var CartesiaSTT = class {
|
|
|
4939
5468
|
}
|
|
4940
5469
|
emit(transcript) {
|
|
4941
5470
|
for (const cb of this.callbacks) {
|
|
4942
|
-
|
|
5471
|
+
try {
|
|
5472
|
+
Promise.resolve(cb(transcript)).catch(
|
|
5473
|
+
(err) => getLogger().error(`STT transcript callback failed: ${String(err)}`)
|
|
5474
|
+
);
|
|
5475
|
+
} catch (err) {
|
|
5476
|
+
getLogger().error(`STT transcript callback threw: ${String(err)}`);
|
|
5477
|
+
}
|
|
4943
5478
|
}
|
|
4944
5479
|
}
|
|
4945
5480
|
/** Send a binary PCM16-LE audio chunk to Cartesia for transcription. */
|
|
@@ -4963,12 +5498,12 @@ var CartesiaSTT = class {
|
|
|
4963
5498
|
*/
|
|
4964
5499
|
async finalize() {
|
|
4965
5500
|
if (!this.ws || this.ws.readyState !== WebSocket2.OPEN) return;
|
|
4966
|
-
await new Promise((
|
|
5501
|
+
await new Promise((resolve2) => {
|
|
4967
5502
|
this.ws.send(CartesiaSTTClientFrame.FINALIZE, (err) => {
|
|
4968
5503
|
if (err) {
|
|
4969
5504
|
getLogger().debug(`Cartesia finalize send failed: ${String(err)}`);
|
|
4970
5505
|
}
|
|
4971
|
-
|
|
5506
|
+
resolve2();
|
|
4972
5507
|
});
|
|
4973
5508
|
});
|
|
4974
5509
|
}
|
|
@@ -5017,10 +5552,10 @@ var CartesiaSTT = class {
|
|
|
5017
5552
|
if (!ws) return;
|
|
5018
5553
|
if (ws.readyState === WebSocket2.OPEN) {
|
|
5019
5554
|
try {
|
|
5020
|
-
await new Promise((
|
|
5555
|
+
await new Promise((resolve2) => {
|
|
5021
5556
|
ws.send(CartesiaSTTClientFrame.FINALIZE, (err) => {
|
|
5022
5557
|
if (err) getLogger().warn(`CartesiaSTT finalize send failed: ${String(err)}`);
|
|
5023
|
-
|
|
5558
|
+
resolve2();
|
|
5024
5559
|
});
|
|
5025
5560
|
});
|
|
5026
5561
|
} catch (err) {
|
|
@@ -5028,18 +5563,18 @@ var CartesiaSTT = class {
|
|
|
5028
5563
|
}
|
|
5029
5564
|
}
|
|
5030
5565
|
if (ws.readyState === WebSocket2.OPEN || ws.readyState === WebSocket2.CONNECTING) {
|
|
5031
|
-
await new Promise((
|
|
5566
|
+
await new Promise((resolve2) => {
|
|
5032
5567
|
const done = () => {
|
|
5033
5568
|
ws.off("close", done);
|
|
5034
5569
|
ws.off("error", done);
|
|
5035
|
-
|
|
5570
|
+
resolve2();
|
|
5036
5571
|
};
|
|
5037
5572
|
ws.once("close", done);
|
|
5038
5573
|
ws.once("error", done);
|
|
5039
5574
|
try {
|
|
5040
5575
|
ws.close();
|
|
5041
5576
|
} catch {
|
|
5042
|
-
|
|
5577
|
+
resolve2();
|
|
5043
5578
|
}
|
|
5044
5579
|
});
|
|
5045
5580
|
}
|
|
@@ -5139,6 +5674,7 @@ var SonioxSTT = class _SonioxSTT {
|
|
|
5139
5674
|
ws = null;
|
|
5140
5675
|
callbacks = /* @__PURE__ */ new Set();
|
|
5141
5676
|
final = new TokenAccumulator();
|
|
5677
|
+
lastInterimText = "";
|
|
5142
5678
|
keepaliveTimer = null;
|
|
5143
5679
|
apiKey;
|
|
5144
5680
|
model;
|
|
@@ -5151,7 +5687,10 @@ var SonioxSTT = class _SonioxSTT {
|
|
|
5151
5687
|
maxEndpointDelayMs;
|
|
5152
5688
|
clientReferenceId;
|
|
5153
5689
|
baseUrl;
|
|
5690
|
+
/** Construction args replayed by clone(). */
|
|
5691
|
+
patterCtorArgs;
|
|
5154
5692
|
constructor(apiKey, options = {}) {
|
|
5693
|
+
this.patterCtorArgs = [apiKey, options];
|
|
5155
5694
|
if (!apiKey) {
|
|
5156
5695
|
throw new Error("Soniox apiKey is required");
|
|
5157
5696
|
}
|
|
@@ -5200,14 +5739,23 @@ var SonioxSTT = class _SonioxSTT {
|
|
|
5200
5739
|
return config;
|
|
5201
5740
|
}
|
|
5202
5741
|
/** Open the streaming WebSocket and send the initial config payload. */
|
|
5742
|
+
/**
|
|
5743
|
+
* Fresh adapter built with this instance's construction arguments —
|
|
5744
|
+
* called per call by the stream handler so concurrent calls never share
|
|
5745
|
+
* connection state (sockets/queues; cross-call transcript bleed).
|
|
5746
|
+
*/
|
|
5747
|
+
clone() {
|
|
5748
|
+
const ctor = this.constructor;
|
|
5749
|
+
return new ctor(...this.patterCtorArgs);
|
|
5750
|
+
}
|
|
5203
5751
|
async connect() {
|
|
5204
5752
|
this.final.reset();
|
|
5205
5753
|
this.ws = new WebSocket3(this.baseUrl);
|
|
5206
|
-
await new Promise((
|
|
5754
|
+
await new Promise((resolve2, reject) => {
|
|
5207
5755
|
const timer = setTimeout(() => reject(new Error("Soniox connect timeout")), 1e4);
|
|
5208
5756
|
this.ws.once("open", () => {
|
|
5209
5757
|
clearTimeout(timer);
|
|
5210
|
-
|
|
5758
|
+
resolve2();
|
|
5211
5759
|
});
|
|
5212
5760
|
this.ws.once("error", (err) => {
|
|
5213
5761
|
clearTimeout(timer);
|
|
@@ -5271,7 +5819,8 @@ var SonioxSTT = class _SonioxSTT {
|
|
|
5271
5819
|
}
|
|
5272
5820
|
if (!emittedFinalThisMsg) {
|
|
5273
5821
|
const text = (this.final.text + nonFinal.text).trim();
|
|
5274
|
-
if (text) {
|
|
5822
|
+
if (text && text !== this.lastInterimText) {
|
|
5823
|
+
this.lastInterimText = text;
|
|
5275
5824
|
const { sum: fSum, count: fCount } = this.final.raw;
|
|
5276
5825
|
const { sum: nSum, count: nCount } = nonFinal.raw;
|
|
5277
5826
|
const total = fCount + nCount;
|
|
@@ -5290,7 +5839,13 @@ var SonioxSTT = class _SonioxSTT {
|
|
|
5290
5839
|
}
|
|
5291
5840
|
emit(transcript) {
|
|
5292
5841
|
for (const cb of this.callbacks) {
|
|
5293
|
-
|
|
5842
|
+
try {
|
|
5843
|
+
Promise.resolve(cb(transcript)).catch(
|
|
5844
|
+
(err) => getLogger().error(`STT transcript callback failed: ${String(err)}`)
|
|
5845
|
+
);
|
|
5846
|
+
} catch (err) {
|
|
5847
|
+
getLogger().error(`STT transcript callback threw: ${String(err)}`);
|
|
5848
|
+
}
|
|
5294
5849
|
}
|
|
5295
5850
|
}
|
|
5296
5851
|
/** Send a binary PCM16-LE audio chunk to Soniox for transcription. */
|
|
@@ -5299,6 +5854,19 @@ var SonioxSTT = class _SonioxSTT {
|
|
|
5299
5854
|
if (audio.length === 0) return;
|
|
5300
5855
|
this.ws.send(audio);
|
|
5301
5856
|
}
|
|
5857
|
+
/**
|
|
5858
|
+
* Ask Soniox to finalize buffered audio immediately. The pipeline's VAD
|
|
5859
|
+
* ``speech_end`` fast-path duck-types ``stt.finalize`` — without this
|
|
5860
|
+
* every Soniox turn waited out the full endpointing delay. Mirrors Python.
|
|
5861
|
+
*/
|
|
5862
|
+
finalize() {
|
|
5863
|
+
if (!this.ws || this.ws.readyState !== WebSocket3.OPEN) return;
|
|
5864
|
+
try {
|
|
5865
|
+
this.ws.send(JSON.stringify({ type: "finalize" }));
|
|
5866
|
+
} catch (err) {
|
|
5867
|
+
getLogger().debug(`Soniox finalize failed: ${String(err)}`);
|
|
5868
|
+
}
|
|
5869
|
+
}
|
|
5302
5870
|
/** Register a transcript listener. */
|
|
5303
5871
|
onTranscript(callback) {
|
|
5304
5872
|
this.callbacks.add(callback);
|
|
@@ -5375,7 +5943,7 @@ var AssemblyAIClientFrame = {
|
|
|
5375
5943
|
FORCE_ENDPOINT: "ForceEndpoint",
|
|
5376
5944
|
TERMINATE: "Terminate"
|
|
5377
5945
|
};
|
|
5378
|
-
var
|
|
5946
|
+
var DEFAULT_BASE_URL3 = "wss://streaming.assemblyai.com";
|
|
5379
5947
|
var DEFAULT_MIN_TURN_SILENCE_MS = 400;
|
|
5380
5948
|
var CONNECT_TIMEOUT_MS2 = 1e4;
|
|
5381
5949
|
var TERMINATION_WAIT_TIMEOUT_MS = 500;
|
|
@@ -5390,6 +5958,7 @@ var AssemblyAISTT = class _AssemblyAISTT {
|
|
|
5390
5958
|
constructor(apiKey, options = {}) {
|
|
5391
5959
|
this.apiKey = apiKey;
|
|
5392
5960
|
this.options = options;
|
|
5961
|
+
this.patterCtorArgs = [apiKey, options];
|
|
5393
5962
|
if (!apiKey) {
|
|
5394
5963
|
throw new Error("AssemblyAISTT requires a non-empty apiKey");
|
|
5395
5964
|
}
|
|
@@ -5431,6 +6000,8 @@ var AssemblyAISTT = class _AssemblyAISTT {
|
|
|
5431
6000
|
sessionId = null;
|
|
5432
6001
|
/** Unix timestamp when the AssemblyAI session expires. */
|
|
5433
6002
|
expiresAt = null;
|
|
6003
|
+
/** Construction args replayed by clone(). */
|
|
6004
|
+
patterCtorArgs;
|
|
5434
6005
|
/** Factory for Twilio calls — mulaw 8 kHz. */
|
|
5435
6006
|
static forTwilio(apiKey, model = AssemblyAIModel.UNIVERSAL_STREAMING_ENGLISH) {
|
|
5436
6007
|
return new _AssemblyAISTT(apiKey, {
|
|
@@ -5482,7 +6053,7 @@ var AssemblyAISTT = class _AssemblyAISTT {
|
|
|
5482
6053
|
params.set(key, String(value));
|
|
5483
6054
|
}
|
|
5484
6055
|
}
|
|
5485
|
-
const base = opts.baseUrl ??
|
|
6056
|
+
const base = opts.baseUrl ?? DEFAULT_BASE_URL3;
|
|
5486
6057
|
return `${base}/v3/ws?${params.toString()}`;
|
|
5487
6058
|
}
|
|
5488
6059
|
buildHeaders() {
|
|
@@ -5515,7 +6086,7 @@ var AssemblyAISTT = class _AssemblyAISTT {
|
|
|
5515
6086
|
const headers = this.buildHeaders();
|
|
5516
6087
|
let ws = null;
|
|
5517
6088
|
try {
|
|
5518
|
-
ws = await new Promise((
|
|
6089
|
+
ws = await new Promise((resolve2, reject) => {
|
|
5519
6090
|
const sock = new WebSocket4(url, { headers });
|
|
5520
6091
|
const timer = setTimeout(() => {
|
|
5521
6092
|
try {
|
|
@@ -5526,7 +6097,7 @@ var AssemblyAISTT = class _AssemblyAISTT {
|
|
|
5526
6097
|
}, 5e3);
|
|
5527
6098
|
sock.once("open", () => {
|
|
5528
6099
|
clearTimeout(timer);
|
|
5529
|
-
|
|
6100
|
+
resolve2(sock);
|
|
5530
6101
|
});
|
|
5531
6102
|
sock.once("error", (err) => {
|
|
5532
6103
|
clearTimeout(timer);
|
|
@@ -5552,6 +6123,15 @@ var AssemblyAISTT = class _AssemblyAISTT {
|
|
|
5552
6123
|
}
|
|
5553
6124
|
}
|
|
5554
6125
|
/** Open the streaming WebSocket and arm message handlers. */
|
|
6126
|
+
/**
|
|
6127
|
+
* Fresh adapter built with this instance's construction arguments —
|
|
6128
|
+
* called per call by the stream handler so concurrent calls never share
|
|
6129
|
+
* connection state (sockets/queues; cross-call transcript bleed).
|
|
6130
|
+
*/
|
|
6131
|
+
clone() {
|
|
6132
|
+
const ctor = this.constructor;
|
|
6133
|
+
return new ctor(...this.patterCtorArgs);
|
|
6134
|
+
}
|
|
5555
6135
|
async connect() {
|
|
5556
6136
|
this.closing = false;
|
|
5557
6137
|
const url = this.buildUrl();
|
|
@@ -5560,14 +6140,14 @@ var AssemblyAISTT = class _AssemblyAISTT {
|
|
|
5560
6140
|
this.attachHandlers(this.ws);
|
|
5561
6141
|
}
|
|
5562
6142
|
async awaitOpen(ws) {
|
|
5563
|
-
await new Promise((
|
|
6143
|
+
await new Promise((resolve2, reject) => {
|
|
5564
6144
|
const timer = setTimeout(
|
|
5565
6145
|
() => reject(new Error("AssemblyAI connect timeout")),
|
|
5566
6146
|
CONNECT_TIMEOUT_MS2
|
|
5567
6147
|
);
|
|
5568
6148
|
ws.once("open", () => {
|
|
5569
6149
|
clearTimeout(timer);
|
|
5570
|
-
|
|
6150
|
+
resolve2();
|
|
5571
6151
|
});
|
|
5572
6152
|
ws.once("error", (err) => {
|
|
5573
6153
|
clearTimeout(timer);
|
|
@@ -5654,7 +6234,13 @@ var AssemblyAISTT = class _AssemblyAISTT {
|
|
|
5654
6234
|
}
|
|
5655
6235
|
emit(transcript) {
|
|
5656
6236
|
for (const cb of this.callbacks) {
|
|
5657
|
-
|
|
6237
|
+
try {
|
|
6238
|
+
Promise.resolve(cb(transcript)).catch(
|
|
6239
|
+
(err) => getLogger().error(`STT transcript callback failed: ${String(err)}`)
|
|
6240
|
+
);
|
|
6241
|
+
} catch (err) {
|
|
6242
|
+
getLogger().error(`STT transcript callback threw: ${String(err)}`);
|
|
6243
|
+
}
|
|
5658
6244
|
}
|
|
5659
6245
|
}
|
|
5660
6246
|
/** Send a binary PCM/mu-law audio chunk to AssemblyAI for transcription. */
|
|
@@ -5764,14 +6350,14 @@ var AssemblyAISTT = class _AssemblyAISTT {
|
|
|
5764
6350
|
this.ws.send(JSON.stringify({ type: AssemblyAIClientFrame.TERMINATE }));
|
|
5765
6351
|
} catch {
|
|
5766
6352
|
}
|
|
5767
|
-
await new Promise((
|
|
6353
|
+
await new Promise((resolve2) => {
|
|
5768
6354
|
const timer = setTimeout(() => {
|
|
5769
6355
|
this.terminationResolve = null;
|
|
5770
|
-
|
|
6356
|
+
resolve2();
|
|
5771
6357
|
}, TERMINATION_WAIT_TIMEOUT_MS);
|
|
5772
6358
|
this.terminationResolve = () => {
|
|
5773
6359
|
clearTimeout(timer);
|
|
5774
|
-
|
|
6360
|
+
resolve2();
|
|
5775
6361
|
};
|
|
5776
6362
|
});
|
|
5777
6363
|
try {
|
|
@@ -5878,7 +6464,10 @@ var SpeechmaticsSTT = class {
|
|
|
5878
6464
|
operatingPoint;
|
|
5879
6465
|
domain;
|
|
5880
6466
|
outputLocale;
|
|
6467
|
+
/** Construction args replayed by clone(). */
|
|
6468
|
+
patterCtorArgs;
|
|
5881
6469
|
constructor(apiKey, options = {}) {
|
|
6470
|
+
this.patterCtorArgs = [apiKey, options];
|
|
5882
6471
|
if (!apiKey) {
|
|
5883
6472
|
throw new Error("Speechmatics apiKey is required");
|
|
5884
6473
|
}
|
|
@@ -5950,13 +6539,22 @@ var SpeechmaticsSTT = class {
|
|
|
5950
6539
|
};
|
|
5951
6540
|
}
|
|
5952
6541
|
/** Open the streaming WebSocket and send the `StartRecognition` frame. */
|
|
6542
|
+
/**
|
|
6543
|
+
* Fresh adapter built with this instance's construction arguments —
|
|
6544
|
+
* called per call by the stream handler so concurrent calls never share
|
|
6545
|
+
* connection state (sockets/queues; cross-call transcript bleed).
|
|
6546
|
+
*/
|
|
6547
|
+
clone() {
|
|
6548
|
+
const ctor = this.constructor;
|
|
6549
|
+
return new ctor(...this.patterCtorArgs);
|
|
6550
|
+
}
|
|
5953
6551
|
async connect() {
|
|
5954
6552
|
if (this.ws !== null) return;
|
|
5955
6553
|
const ws = new WebSocket5(this.baseUrl, {
|
|
5956
6554
|
headers: { Authorization: `Bearer ${this.apiKey}` }
|
|
5957
6555
|
});
|
|
5958
6556
|
this.ws = ws;
|
|
5959
|
-
await new Promise((
|
|
6557
|
+
await new Promise((resolve2, reject) => {
|
|
5960
6558
|
let settled = false;
|
|
5961
6559
|
const settle = (fn) => {
|
|
5962
6560
|
if (settled) return;
|
|
@@ -5970,7 +6568,7 @@ var SpeechmaticsSTT = class {
|
|
|
5970
6568
|
),
|
|
5971
6569
|
CONNECT_TIMEOUT_MS3
|
|
5972
6570
|
);
|
|
5973
|
-
ws.once("open", () => settle(
|
|
6571
|
+
ws.once("open", () => settle(resolve2));
|
|
5974
6572
|
ws.once("error", (err) => settle(() => reject(err)));
|
|
5975
6573
|
ws.once("unexpected-response", (_req, res) => {
|
|
5976
6574
|
const status = res?.statusCode ?? 0;
|
|
@@ -6102,7 +6700,9 @@ var SpeechmaticsSTT = class {
|
|
|
6102
6700
|
emitTranscript(transcript) {
|
|
6103
6701
|
for (const cb of this.transcriptCallbacks) {
|
|
6104
6702
|
try {
|
|
6105
|
-
cb(transcript)
|
|
6703
|
+
Promise.resolve(cb(transcript)).catch(
|
|
6704
|
+
(err) => getLogger().error(`SpeechmaticsSTT transcript callback failed: ${String(err)}`)
|
|
6705
|
+
);
|
|
6106
6706
|
} catch (err) {
|
|
6107
6707
|
getLogger().error(`SpeechmaticsSTT transcript callback threw: ${String(err)}`);
|
|
6108
6708
|
}
|
|
@@ -6225,7 +6825,9 @@ function sanitiseLogStr(value, limit = 200) {
|
|
|
6225
6825
|
}
|
|
6226
6826
|
var CARRIER_NATIVE_FORMAT2 = {
|
|
6227
6827
|
twilio: "ulaw_8000",
|
|
6228
|
-
|
|
6828
|
+
// The SDK's streaming_start pins the Telnyx wire to PCMU/μ-law @ 8 kHz —
|
|
6829
|
+
// 'pcm_16000' here shipped raw PCM16 onto a μ-law wire (static).
|
|
6830
|
+
telnyx: "ulaw_8000",
|
|
6229
6831
|
// Plivo streams mulaw 8 kHz (we pin contentType in the answer XML).
|
|
6230
6832
|
plivo: "ulaw_8000"
|
|
6231
6833
|
};
|
|
@@ -6353,11 +6955,11 @@ var ElevenLabsWebSocketTTS = class _ElevenLabsWebSocketTTS {
|
|
|
6353
6955
|
}
|
|
6354
6956
|
});
|
|
6355
6957
|
}
|
|
6356
|
-
/** Pre-configured for Telnyx (
|
|
6958
|
+
/** Pre-configured for Telnyx (μ-law 8 kHz wire). */
|
|
6357
6959
|
static forTelnyx(opts) {
|
|
6358
6960
|
return new _ElevenLabsWebSocketTTS({
|
|
6359
6961
|
...opts,
|
|
6360
|
-
outputFormat: "
|
|
6962
|
+
outputFormat: "ulaw_8000"
|
|
6361
6963
|
});
|
|
6362
6964
|
}
|
|
6363
6965
|
buildUrl() {
|
|
@@ -6497,7 +7099,7 @@ var ElevenLabsWebSocketTTS = class _ElevenLabsWebSocketTTS {
|
|
|
6497
7099
|
ws.on("error", onError);
|
|
6498
7100
|
try {
|
|
6499
7101
|
if (!adopted) {
|
|
6500
|
-
await new Promise((
|
|
7102
|
+
await new Promise((resolve2, reject) => {
|
|
6501
7103
|
connectTimer = setTimeout(
|
|
6502
7104
|
() => reject(new Error("ElevenLabs WS connect timeout")),
|
|
6503
7105
|
CONNECT_TIMEOUT_MS4
|
|
@@ -6505,7 +7107,7 @@ var ElevenLabsWebSocketTTS = class _ElevenLabsWebSocketTTS {
|
|
|
6505
7107
|
ws.once("open", () => {
|
|
6506
7108
|
if (connectTimer) clearTimeout(connectTimer);
|
|
6507
7109
|
connectTimer = void 0;
|
|
6508
|
-
|
|
7110
|
+
resolve2();
|
|
6509
7111
|
});
|
|
6510
7112
|
ws.once("error", (err) => {
|
|
6511
7113
|
if (connectTimer) clearTimeout(connectTimer);
|
|
@@ -6590,14 +7192,14 @@ var ElevenLabsWebSocketTTS = class _ElevenLabsWebSocketTTS {
|
|
|
6590
7192
|
headers: { "xi-api-key": this.apiKey }
|
|
6591
7193
|
});
|
|
6592
7194
|
try {
|
|
6593
|
-
await new Promise((
|
|
7195
|
+
await new Promise((resolve2, reject) => {
|
|
6594
7196
|
const timer = setTimeout(
|
|
6595
7197
|
() => reject(new Error("ElevenLabs WS TTS warmup connect timeout")),
|
|
6596
7198
|
CONNECT_TIMEOUT_MS4
|
|
6597
7199
|
);
|
|
6598
7200
|
ws.once("open", () => {
|
|
6599
7201
|
clearTimeout(timer);
|
|
6600
|
-
|
|
7202
|
+
resolve2();
|
|
6601
7203
|
});
|
|
6602
7204
|
ws.once("error", (err) => {
|
|
6603
7205
|
clearTimeout(timer);
|
|
@@ -6641,14 +7243,14 @@ var ElevenLabsWebSocketTTS = class _ElevenLabsWebSocketTTS {
|
|
|
6641
7243
|
const ws = new WebSocket6(this.buildUrl(), {
|
|
6642
7244
|
headers: { "xi-api-key": this.apiKey }
|
|
6643
7245
|
});
|
|
6644
|
-
await new Promise((
|
|
7246
|
+
await new Promise((resolve2, reject) => {
|
|
6645
7247
|
const timer = setTimeout(
|
|
6646
7248
|
() => reject(new Error("ElevenLabs WS park connect timeout")),
|
|
6647
7249
|
CONNECT_TIMEOUT_MS4
|
|
6648
7250
|
);
|
|
6649
7251
|
ws.once("open", () => {
|
|
6650
7252
|
clearTimeout(timer);
|
|
6651
|
-
|
|
7253
|
+
resolve2();
|
|
6652
7254
|
});
|
|
6653
7255
|
ws.once("error", (err) => {
|
|
6654
7256
|
clearTimeout(timer);
|
|
@@ -6989,7 +7591,7 @@ var TTS4 = class _TTS extends CartesiaTTS {
|
|
|
6989
7591
|
}
|
|
6990
7592
|
static forTwilio(arg1, arg2) {
|
|
6991
7593
|
const opts = typeof arg1 === "string" ? { apiKey: arg1, ...arg2 ?? {} } : arg1 ?? {};
|
|
6992
|
-
return new _TTS({ ...opts, sampleRate:
|
|
7594
|
+
return new _TTS({ ...opts, sampleRate: 16e3 });
|
|
6993
7595
|
}
|
|
6994
7596
|
static forTelnyx(arg1, arg2) {
|
|
6995
7597
|
const opts = typeof arg1 === "string" ? { apiKey: arg1, ...arg2 ?? {} } : arg1 ?? {};
|
|
@@ -7372,7 +7974,7 @@ var AnthropicModel = {
|
|
|
7372
7974
|
CLAUDE_3_5_SONNET_20241022: "claude-3-5-sonnet-20241022",
|
|
7373
7975
|
CLAUDE_3_5_HAIKU_20241022: "claude-3-5-haiku-20241022"
|
|
7374
7976
|
};
|
|
7375
|
-
var
|
|
7977
|
+
var DEFAULT_MODEL2 = AnthropicModel.CLAUDE_HAIKU_4_5_20251001;
|
|
7376
7978
|
var DEFAULT_MAX_TOKENS = 1024;
|
|
7377
7979
|
var PROMPT_CACHING_BETA = "prompt-caching-2024-07-31";
|
|
7378
7980
|
var AnthropicLLMProvider = class {
|
|
@@ -7392,7 +7994,7 @@ var AnthropicLLMProvider = class {
|
|
|
7392
7994
|
);
|
|
7393
7995
|
}
|
|
7394
7996
|
this.apiKey = options.apiKey;
|
|
7395
|
-
this.model = options.model ??
|
|
7997
|
+
this.model = options.model ?? DEFAULT_MODEL2;
|
|
7396
7998
|
this.maxTokens = options.maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
7397
7999
|
this.temperature = options.temperature;
|
|
7398
8000
|
this.url = options.baseUrl ?? DEFAULT_ANTHROPIC_URL;
|
|
@@ -7465,16 +8067,19 @@ var AnthropicLLMProvider = class {
|
|
|
7465
8067
|
if (this.promptCaching) {
|
|
7466
8068
|
headers["anthropic-beta"] = PROMPT_CACHING_BETA;
|
|
7467
8069
|
}
|
|
8070
|
+
const idle = createStreamIdleWatchdog();
|
|
7468
8071
|
const response = await fetch(this.url, {
|
|
7469
8072
|
method: "POST",
|
|
7470
8073
|
headers,
|
|
7471
8074
|
body: JSON.stringify(body),
|
|
7472
|
-
signal: mergeAbortSignals(opts?.signal,
|
|
8075
|
+
signal: mergeAbortSignals(opts?.signal, idle.signal)
|
|
7473
8076
|
});
|
|
7474
8077
|
if (!response.ok) {
|
|
7475
8078
|
const errText = await response.text();
|
|
7476
|
-
getLogger().error(`Anthropic API error: ${response.status} ${errText}`);
|
|
7477
|
-
|
|
8079
|
+
getLogger().error(`Anthropic API error: ${response.status} ${errText.slice(0, 200)}`);
|
|
8080
|
+
throw new PatterConnectionError(
|
|
8081
|
+
`Anthropic API returned ${response.status}: ${errText.slice(0, 200)}`
|
|
8082
|
+
);
|
|
7478
8083
|
}
|
|
7479
8084
|
const reader = response.body?.getReader();
|
|
7480
8085
|
if (!reader) return;
|
|
@@ -7490,6 +8095,7 @@ var AnthropicLLMProvider = class {
|
|
|
7490
8095
|
try {
|
|
7491
8096
|
while (true) {
|
|
7492
8097
|
const { done, value } = await reader.read();
|
|
8098
|
+
idle.touch();
|
|
7493
8099
|
if (done) break;
|
|
7494
8100
|
buffer += decoder.decode(value, { stream: true });
|
|
7495
8101
|
const lines = buffer.split("\n");
|
|
@@ -7505,6 +8111,15 @@ var AnthropicLLMProvider = class {
|
|
|
7505
8111
|
} catch {
|
|
7506
8112
|
continue;
|
|
7507
8113
|
}
|
|
8114
|
+
if (event.type === "error") {
|
|
8115
|
+
const errPayload = event.error;
|
|
8116
|
+
const detail = `${errPayload?.type ?? "unknown"}: ${errPayload?.message ?? ""}`.slice(
|
|
8117
|
+
0,
|
|
8118
|
+
200
|
|
8119
|
+
);
|
|
8120
|
+
getLogger().error(`Anthropic in-stream error event: ${detail}`);
|
|
8121
|
+
throw new PatterConnectionError(`Anthropic stream error \u2014 ${detail}`);
|
|
8122
|
+
}
|
|
7508
8123
|
if (event.type === "message_start" && event.message?.usage) {
|
|
7509
8124
|
const u = event.message.usage;
|
|
7510
8125
|
if (u.input_tokens) inputTokens = u.input_tokens;
|
|
@@ -7552,7 +8167,15 @@ var AnthropicLLMProvider = class {
|
|
|
7552
8167
|
}
|
|
7553
8168
|
}
|
|
7554
8169
|
}
|
|
8170
|
+
} catch (err) {
|
|
8171
|
+
if (idle.fired && !opts?.signal?.aborted) {
|
|
8172
|
+
throw new PatterConnectionError(
|
|
8173
|
+
`Anthropic stream idle timeout \u2014 no data for ${LLM_STREAM_IDLE_TIMEOUT_MS / 1e3}s`
|
|
8174
|
+
);
|
|
8175
|
+
}
|
|
8176
|
+
throw err;
|
|
7555
8177
|
} finally {
|
|
8178
|
+
idle.clear();
|
|
7556
8179
|
reader.cancel().catch(() => {
|
|
7557
8180
|
});
|
|
7558
8181
|
}
|
|
@@ -7637,6 +8260,9 @@ function toAnthropicMessages(messages) {
|
|
|
7637
8260
|
continue;
|
|
7638
8261
|
}
|
|
7639
8262
|
}
|
|
8263
|
+
if (out.length > 0 && out[0].role === "assistant") {
|
|
8264
|
+
out.unshift({ role: "user", content: "(call connected)" });
|
|
8265
|
+
}
|
|
7640
8266
|
return { system: systemParts.join("\n\n"), messages: out };
|
|
7641
8267
|
}
|
|
7642
8268
|
|
|
@@ -7677,7 +8303,7 @@ var GroqModel = {
|
|
|
7677
8303
|
MIXTRAL_8X7B: "mixtral-8x7b-32768",
|
|
7678
8304
|
GEMMA2_9B: "gemma2-9b-it"
|
|
7679
8305
|
};
|
|
7680
|
-
var
|
|
8306
|
+
var DEFAULT_MODEL3 = GroqModel.LLAMA_3_3_70B_VERSATILE;
|
|
7681
8307
|
var GroqLLMProvider = class {
|
|
7682
8308
|
/** Stable pricing/dashboard key — read by stream-handler/metrics. */
|
|
7683
8309
|
static providerKey = "groq";
|
|
@@ -7701,7 +8327,7 @@ var GroqLLMProvider = class {
|
|
|
7701
8327
|
);
|
|
7702
8328
|
}
|
|
7703
8329
|
this.apiKey = options.apiKey;
|
|
7704
|
-
this.model = options.model ??
|
|
8330
|
+
this.model = options.model ?? DEFAULT_MODEL3;
|
|
7705
8331
|
this.baseUrl = options.baseUrl ?? GROQ_BASE_URL;
|
|
7706
8332
|
this.temperature = options.temperature;
|
|
7707
8333
|
this.maxTokens = options.maxTokens;
|
|
@@ -7750,6 +8376,7 @@ var GroqLLMProvider = class {
|
|
|
7750
8376
|
if (this.presencePenalty !== void 0) body.presence_penalty = this.presencePenalty;
|
|
7751
8377
|
if (this.stop !== void 0) body.stop = this.stop;
|
|
7752
8378
|
if (tools) body.tools = tools;
|
|
8379
|
+
const idle = createStreamIdleWatchdog();
|
|
7753
8380
|
const response = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
7754
8381
|
method: "POST",
|
|
7755
8382
|
headers: {
|
|
@@ -7758,17 +8385,30 @@ var GroqLLMProvider = class {
|
|
|
7758
8385
|
"User-Agent": `getpatter/${VERSION}`
|
|
7759
8386
|
},
|
|
7760
8387
|
body: JSON.stringify(body),
|
|
7761
|
-
signal: mergeAbortSignals(opts?.signal,
|
|
8388
|
+
signal: mergeAbortSignals(opts?.signal, idle.signal)
|
|
7762
8389
|
});
|
|
7763
8390
|
if (!response.ok) {
|
|
7764
8391
|
const errText = await response.text();
|
|
7765
|
-
getLogger().error(`Groq API error: ${response.status} ${errText}`);
|
|
7766
|
-
|
|
8392
|
+
getLogger().error(`Groq API error: ${response.status} ${errText.slice(0, 200)}`);
|
|
8393
|
+
throw new PatterConnectionError(
|
|
8394
|
+
`Groq API returned ${response.status}: ${errText.slice(0, 200)}`
|
|
8395
|
+
);
|
|
8396
|
+
}
|
|
8397
|
+
try {
|
|
8398
|
+
yield* parseOpenAISseStream(response, idle.touch);
|
|
8399
|
+
} catch (err) {
|
|
8400
|
+
if (idle.fired && !opts?.signal?.aborted) {
|
|
8401
|
+
throw new PatterConnectionError(
|
|
8402
|
+
`Groq stream idle timeout \u2014 no data for ${LLM_STREAM_IDLE_TIMEOUT_MS / 1e3}s`
|
|
8403
|
+
);
|
|
8404
|
+
}
|
|
8405
|
+
throw err;
|
|
8406
|
+
} finally {
|
|
8407
|
+
idle.clear();
|
|
7767
8408
|
}
|
|
7768
|
-
yield* parseOpenAISseStream(response);
|
|
7769
8409
|
}
|
|
7770
8410
|
};
|
|
7771
|
-
async function* parseOpenAISseStream(response) {
|
|
8411
|
+
async function* parseOpenAISseStream(response, onRead) {
|
|
7772
8412
|
const reader = response.body?.getReader();
|
|
7773
8413
|
if (!reader) return;
|
|
7774
8414
|
const decoder = new TextDecoder();
|
|
@@ -7776,6 +8416,7 @@ async function* parseOpenAISseStream(response) {
|
|
|
7776
8416
|
try {
|
|
7777
8417
|
while (true) {
|
|
7778
8418
|
const { done, value } = await reader.read();
|
|
8419
|
+
onRead?.();
|
|
7779
8420
|
if (done) break;
|
|
7780
8421
|
buffer += decoder.decode(value, { stream: true });
|
|
7781
8422
|
const lines = buffer.split("\n");
|
|
@@ -7796,7 +8437,7 @@ async function* parseOpenAISseStream(response) {
|
|
|
7796
8437
|
const cached = chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0;
|
|
7797
8438
|
yield {
|
|
7798
8439
|
type: "usage",
|
|
7799
|
-
inputTokens: usage.prompt_tokens,
|
|
8440
|
+
inputTokens: Math.max(0, (usage.prompt_tokens ?? 0) - cached),
|
|
7800
8441
|
outputTokens: usage.completion_tokens,
|
|
7801
8442
|
cacheReadInputTokens: cached
|
|
7802
8443
|
};
|
|
@@ -7852,468 +8493,248 @@ var LLM3 = class extends GroqLLMProvider {
|
|
|
7852
8493
|
});
|
|
7853
8494
|
}
|
|
7854
8495
|
};
|
|
7855
|
-
|
|
7856
|
-
// src/llm/cerebras.ts
|
|
7857
|
-
init_esm_shims();
|
|
7858
|
-
|
|
7859
|
-
// src/providers/cerebras-llm.ts
|
|
7860
|
-
init_esm_shims();
|
|
7861
|
-
var CEREBRAS_BASE_URL = "https://api.cerebras.ai/v1";
|
|
7862
|
-
var CerebrasModel = {
|
|
7863
|
-
GPT_OSS_120B: "gpt-oss-120b",
|
|
7864
|
-
LLAMA_3_1_8B: "llama3.1-8b",
|
|
7865
|
-
LLAMA_3_3_70B: "llama-3.3-70b",
|
|
7866
|
-
QWEN_3_235B_INSTRUCT: "qwen-3-235b-a22b-instruct-2507",
|
|
7867
|
-
ZAI_GLM_4_7: "zai-glm-4.7"
|
|
7868
|
-
};
|
|
7869
|
-
var DEFAULT_MODEL3 = CerebrasModel.GPT_OSS_120B;
|
|
7870
|
-
var RETRY_BACKOFF_BASE_MS = 500;
|
|
7871
|
-
var CerebrasLLMProvider = class {
|
|
7872
|
-
/** Stable pricing/dashboard key — read by stream-handler/metrics. */
|
|
7873
|
-
static providerKey = "cerebras";
|
|
7874
|
-
apiKey;
|
|
7875
|
-
model;
|
|
7876
|
-
baseUrl;
|
|
7877
|
-
gzipCompression;
|
|
7878
|
-
temperature;
|
|
7879
|
-
maxTokens;
|
|
7880
|
-
responseFormat;
|
|
7881
|
-
parallelToolCalls;
|
|
7882
|
-
toolChoice;
|
|
7883
|
-
seed;
|
|
7884
|
-
topP;
|
|
7885
|
-
frequencyPenalty;
|
|
7886
|
-
presencePenalty;
|
|
7887
|
-
stop;
|
|
7888
|
-
constructor(options) {
|
|
7889
|
-
if (!options.apiKey) {
|
|
7890
|
-
throw new Error(
|
|
7891
|
-
"Cerebras API key is required. Pass it via { apiKey } or read CEREBRAS_API_KEY from the environment."
|
|
7892
|
-
);
|
|
7893
|
-
}
|
|
7894
|
-
this.apiKey = options.apiKey;
|
|
7895
|
-
this.model = options.model ?? DEFAULT_MODEL3;
|
|
7896
|
-
this.baseUrl = options.baseUrl ?? CEREBRAS_BASE_URL;
|
|
7897
|
-
this.gzipCompression = options.gzipCompression ?? true;
|
|
7898
|
-
this.temperature = options.temperature;
|
|
7899
|
-
this.maxTokens = options.maxTokens;
|
|
7900
|
-
this.responseFormat = options.responseFormat;
|
|
7901
|
-
this.parallelToolCalls = options.parallelToolCalls;
|
|
7902
|
-
this.toolChoice = options.toolChoice;
|
|
7903
|
-
this.seed = options.seed;
|
|
7904
|
-
this.topP = options.topP;
|
|
7905
|
-
this.frequencyPenalty = options.frequencyPenalty;
|
|
7906
|
-
this.presencePenalty = options.presencePenalty;
|
|
7907
|
-
this.stop = options.stop;
|
|
7908
|
-
}
|
|
7909
|
-
/**
|
|
7910
|
-
* Pre-call DNS / TLS warmup for the Cerebras inference endpoint.
|
|
7911
|
-
* Best-effort: 5 s timeout, all exceptions swallowed at debug level.
|
|
7912
|
-
*/
|
|
7913
|
-
async warmup() {
|
|
7914
|
-
try {
|
|
7915
|
-
await fetch(`${this.baseUrl}/models`, {
|
|
7916
|
-
method: "GET",
|
|
7917
|
-
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
7918
|
-
signal: AbortSignal.timeout(5e3)
|
|
7919
|
-
});
|
|
7920
|
-
} catch (err) {
|
|
7921
|
-
getLogger().debug(`Cerebras LLM warmup failed (best-effort): ${String(err)}`);
|
|
7922
|
-
}
|
|
7923
|
-
}
|
|
7924
|
-
/** Stream Patter-format LLM chunks from the Cerebras chat completions API. */
|
|
7925
|
-
async *stream(messages, tools, opts) {
|
|
7926
|
-
const body = {
|
|
7927
|
-
model: this.model,
|
|
7928
|
-
messages,
|
|
7929
|
-
stream: true,
|
|
7930
|
-
stream_options: { include_usage: true }
|
|
7931
|
-
};
|
|
7932
|
-
if (this.temperature !== void 0) body.temperature = this.temperature;
|
|
7933
|
-
if (this.maxTokens !== void 0) {
|
|
7934
|
-
body.max_completion_tokens = this.maxTokens;
|
|
7935
|
-
}
|
|
7936
|
-
if (this.responseFormat !== void 0) body.response_format = this.responseFormat;
|
|
7937
|
-
if (this.parallelToolCalls !== void 0) body.parallel_tool_calls = this.parallelToolCalls;
|
|
7938
|
-
if (this.toolChoice !== void 0) body.tool_choice = this.toolChoice;
|
|
7939
|
-
if (this.seed !== void 0) body.seed = this.seed;
|
|
7940
|
-
if (this.topP !== void 0) body.top_p = this.topP;
|
|
7941
|
-
if (this.frequencyPenalty !== void 0) body.frequency_penalty = this.frequencyPenalty;
|
|
7942
|
-
if (this.presencePenalty !== void 0) body.presence_penalty = this.presencePenalty;
|
|
7943
|
-
if (this.stop !== void 0) body.stop = this.stop;
|
|
7944
|
-
if (tools) body.tools = tools;
|
|
7945
|
-
const headers = {
|
|
7946
|
-
"Content-Type": "application/json",
|
|
7947
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
7948
|
-
// Identify the SDK in upstream logs/rate-limit attribution.
|
|
7949
|
-
"User-Agent": `getpatter/${VERSION}`
|
|
7950
|
-
};
|
|
7951
|
-
let payload = JSON.stringify(body);
|
|
7952
|
-
if (this.gzipCompression) {
|
|
7953
|
-
const compressed = await gzipEncode(payload);
|
|
7954
|
-
if (compressed) {
|
|
7955
|
-
payload = compressed;
|
|
7956
|
-
headers["Content-Encoding"] = "gzip";
|
|
7957
|
-
}
|
|
7958
|
-
}
|
|
7959
|
-
const maxAttempts = 2;
|
|
7960
|
-
let lastErrText = "";
|
|
7961
|
-
let lastStatus = 0;
|
|
7962
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
7963
|
-
const response = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
7964
|
-
method: "POST",
|
|
7965
|
-
headers,
|
|
7966
|
-
body: payload,
|
|
7967
|
-
signal: mergeAbortSignals(opts?.signal, AbortSignal.timeout(3e4))
|
|
7968
|
-
});
|
|
7969
|
-
if (response.ok) {
|
|
7970
|
-
yield* parseOpenAISseStream(response);
|
|
7971
|
-
return;
|
|
7972
|
-
}
|
|
7973
|
-
lastStatus = response.status;
|
|
7974
|
-
lastErrText = await response.text().catch(() => "");
|
|
7975
|
-
const isRetriable = response.status === 429 || response.status >= 500;
|
|
7976
|
-
const isLastAttempt = attempt >= maxAttempts - 1;
|
|
7977
|
-
if (!isRetriable || isLastAttempt) {
|
|
7978
|
-
if (response.status === 404 && lastErrText.includes("model_not_found")) {
|
|
7979
|
-
getLogger().error(
|
|
7980
|
-
`Cerebras: model "${this.model}" not available on your tier. Override via \`new CerebrasLLM({ model: '<id>' })\` and list tier-available ids with \`GET ${this.baseUrl}/models\` (common: llama3.1-8b, qwen-3-235b-a22b-instruct-2507, llama-3.3-70b on paid). Raw response: ${lastErrText}`
|
|
7981
|
-
);
|
|
7982
|
-
} else {
|
|
7983
|
-
getLogger().error(`Cerebras API error: ${response.status} ${lastErrText}`);
|
|
7984
|
-
}
|
|
7985
|
-
return;
|
|
7986
|
-
}
|
|
7987
|
-
const advisoryMs = parseRateLimitResetMs(response.headers);
|
|
7988
|
-
const exponentialMs = RETRY_BACKOFF_BASE_MS * Math.pow(2, attempt);
|
|
7989
|
-
const delayMs = Math.min(5e3, Math.max(advisoryMs, exponentialMs));
|
|
7990
|
-
getLogger().warn(
|
|
7991
|
-
`Cerebras API ${response.status} (attempt ${attempt + 1}/${maxAttempts}); retrying after ${delayMs}ms`
|
|
7992
|
-
);
|
|
7993
|
-
await new Promise((resolve, reject) => {
|
|
7994
|
-
const t = setTimeout(resolve, delayMs);
|
|
7995
|
-
opts?.signal?.addEventListener(
|
|
7996
|
-
"abort",
|
|
7997
|
-
() => {
|
|
7998
|
-
clearTimeout(t);
|
|
7999
|
-
reject(opts.signal.reason);
|
|
8000
|
-
},
|
|
8001
|
-
{ once: true }
|
|
8002
|
-
);
|
|
8003
|
-
});
|
|
8004
|
-
}
|
|
8005
|
-
throw new PatterError(`Cerebras API error ${lastStatus}: ${lastErrText || "request failed"}`);
|
|
8006
|
-
}
|
|
8007
|
-
};
|
|
8008
|
-
async function gzipEncode(data) {
|
|
8009
|
-
const CompressionCtor = globalThis.CompressionStream;
|
|
8010
|
-
if (!CompressionCtor) return null;
|
|
8011
|
-
const stream = new CompressionCtor("gzip");
|
|
8012
|
-
const writer = stream.writable.getWriter();
|
|
8013
|
-
const encoder = new TextEncoder();
|
|
8014
|
-
await writer.write(encoder.encode(data));
|
|
8015
|
-
await writer.close();
|
|
8016
|
-
const chunks = [];
|
|
8017
|
-
const reader = stream.readable.getReader();
|
|
8018
|
-
while (true) {
|
|
8019
|
-
const { done, value } = await reader.read();
|
|
8020
|
-
if (done) break;
|
|
8021
|
-
if (value) chunks.push(value);
|
|
8022
|
-
}
|
|
8023
|
-
const total = chunks.reduce((n, c) => n + c.length, 0);
|
|
8024
|
-
const out = new Uint8Array(total);
|
|
8025
|
-
let offset = 0;
|
|
8026
|
-
for (const c of chunks) {
|
|
8027
|
-
out.set(c, offset);
|
|
8028
|
-
offset += c.length;
|
|
8029
|
-
}
|
|
8030
|
-
return out;
|
|
8031
|
-
}
|
|
8032
|
-
function parseRateLimitResetMs(headers) {
|
|
8033
|
-
const candidates = [
|
|
8034
|
-
headers.get("x-ratelimit-reset-tokens-minute"),
|
|
8035
|
-
headers.get("x-ratelimit-reset-requests-minute"),
|
|
8036
|
-
// Some upstreams send the standard ``retry-after`` (seconds).
|
|
8037
|
-
headers.get("retry-after")
|
|
8038
|
-
];
|
|
8039
|
-
let bestMs = 0;
|
|
8040
|
-
for (const raw of candidates) {
|
|
8041
|
-
if (!raw) continue;
|
|
8042
|
-
const parsed = Number.parseFloat(raw);
|
|
8043
|
-
if (Number.isFinite(parsed) && parsed > 0) {
|
|
8044
|
-
const ms = parsed * 1e3;
|
|
8045
|
-
if (ms > bestMs) bestMs = ms;
|
|
8046
|
-
}
|
|
8047
|
-
}
|
|
8048
|
-
return bestMs;
|
|
8049
|
-
}
|
|
8050
|
-
|
|
8051
|
-
// src/llm/cerebras.ts
|
|
8052
|
-
var LLM4 = class extends CerebrasLLMProvider {
|
|
8053
|
-
static providerKey = "cerebras";
|
|
8054
|
-
constructor(opts = {}) {
|
|
8055
|
-
const key = opts.apiKey ?? process.env.CEREBRAS_API_KEY;
|
|
8056
|
-
if (!key) {
|
|
8057
|
-
throw new Error(
|
|
8058
|
-
"Cerebras LLM requires an apiKey. Pass { apiKey: 'csk-...' } or set CEREBRAS_API_KEY."
|
|
8059
|
-
);
|
|
8060
|
-
}
|
|
8061
|
-
super({
|
|
8062
|
-
apiKey: key,
|
|
8063
|
-
model: opts.model,
|
|
8064
|
-
baseUrl: opts.baseUrl,
|
|
8065
|
-
gzipCompression: opts.gzipCompression,
|
|
8066
|
-
temperature: opts.temperature,
|
|
8067
|
-
maxTokens: opts.maxTokens,
|
|
8068
|
-
responseFormat: opts.responseFormat,
|
|
8069
|
-
parallelToolCalls: opts.parallelToolCalls,
|
|
8070
|
-
toolChoice: opts.toolChoice,
|
|
8071
|
-
seed: opts.seed,
|
|
8072
|
-
topP: opts.topP,
|
|
8073
|
-
frequencyPenalty: opts.frequencyPenalty,
|
|
8074
|
-
presencePenalty: opts.presencePenalty,
|
|
8075
|
-
stop: opts.stop
|
|
8076
|
-
});
|
|
8077
|
-
}
|
|
8078
|
-
};
|
|
8079
|
-
|
|
8080
|
-
// src/llm/google.ts
|
|
8081
|
-
init_esm_shims();
|
|
8082
|
-
|
|
8083
|
-
// src/providers/google-llm.ts
|
|
8084
|
-
init_esm_shims();
|
|
8085
|
-
var GoogleModel = {
|
|
8086
|
-
GEMINI_2_5_FLASH: "gemini-2.5-flash",
|
|
8087
|
-
GEMINI_2_5_PRO: "gemini-2.5-pro",
|
|
8088
|
-
GEMINI_2_0_FLASH: "gemini-2.0-flash",
|
|
8089
|
-
GEMINI_2_0_FLASH_LITE: "gemini-2.0-flash-lite",
|
|
8090
|
-
GEMINI_1_5_FLASH: "gemini-1.5-flash",
|
|
8091
|
-
GEMINI_1_5_PRO: "gemini-1.5-pro"
|
|
8496
|
+
|
|
8497
|
+
// src/llm/cerebras.ts
|
|
8498
|
+
init_esm_shims();
|
|
8499
|
+
|
|
8500
|
+
// src/providers/cerebras-llm.ts
|
|
8501
|
+
init_esm_shims();
|
|
8502
|
+
var CEREBRAS_BASE_URL = "https://api.cerebras.ai/v1";
|
|
8503
|
+
var CerebrasModel = {
|
|
8504
|
+
GPT_OSS_120B: "gpt-oss-120b",
|
|
8505
|
+
LLAMA_3_1_8B: "llama3.1-8b",
|
|
8506
|
+
LLAMA_3_3_70B: "llama-3.3-70b",
|
|
8507
|
+
QWEN_3_235B_INSTRUCT: "qwen-3-235b-a22b-instruct-2507",
|
|
8508
|
+
ZAI_GLM_4_7: "zai-glm-4.7"
|
|
8092
8509
|
};
|
|
8093
|
-
var DEFAULT_MODEL4 =
|
|
8094
|
-
var
|
|
8095
|
-
var
|
|
8510
|
+
var DEFAULT_MODEL4 = CerebrasModel.GPT_OSS_120B;
|
|
8511
|
+
var RETRY_BACKOFF_BASE_MS = 500;
|
|
8512
|
+
var CerebrasLLMProvider = class {
|
|
8096
8513
|
/** Stable pricing/dashboard key — read by stream-handler/metrics. */
|
|
8097
|
-
static providerKey = "
|
|
8514
|
+
static providerKey = "cerebras";
|
|
8098
8515
|
apiKey;
|
|
8099
8516
|
model;
|
|
8100
8517
|
baseUrl;
|
|
8518
|
+
gzipCompression;
|
|
8101
8519
|
temperature;
|
|
8102
|
-
|
|
8520
|
+
maxTokens;
|
|
8521
|
+
responseFormat;
|
|
8522
|
+
parallelToolCalls;
|
|
8523
|
+
toolChoice;
|
|
8524
|
+
seed;
|
|
8525
|
+
topP;
|
|
8526
|
+
frequencyPenalty;
|
|
8527
|
+
presencePenalty;
|
|
8528
|
+
stop;
|
|
8103
8529
|
constructor(options) {
|
|
8104
8530
|
if (!options.apiKey) {
|
|
8105
8531
|
throw new Error(
|
|
8106
|
-
"
|
|
8532
|
+
"Cerebras API key is required. Pass it via { apiKey } or read CEREBRAS_API_KEY from the environment."
|
|
8107
8533
|
);
|
|
8108
8534
|
}
|
|
8109
8535
|
this.apiKey = options.apiKey;
|
|
8110
8536
|
this.model = options.model ?? DEFAULT_MODEL4;
|
|
8111
|
-
this.baseUrl = options.baseUrl ??
|
|
8537
|
+
this.baseUrl = options.baseUrl ?? CEREBRAS_BASE_URL;
|
|
8538
|
+
this.gzipCompression = options.gzipCompression ?? true;
|
|
8112
8539
|
this.temperature = options.temperature;
|
|
8113
|
-
this.
|
|
8540
|
+
this.maxTokens = options.maxTokens;
|
|
8541
|
+
this.responseFormat = options.responseFormat;
|
|
8542
|
+
this.parallelToolCalls = options.parallelToolCalls;
|
|
8543
|
+
this.toolChoice = options.toolChoice;
|
|
8544
|
+
this.seed = options.seed;
|
|
8545
|
+
this.topP = options.topP;
|
|
8546
|
+
this.frequencyPenalty = options.frequencyPenalty;
|
|
8547
|
+
this.presencePenalty = options.presencePenalty;
|
|
8548
|
+
this.stop = options.stop;
|
|
8114
8549
|
}
|
|
8115
8550
|
/**
|
|
8116
|
-
* Pre-call DNS / TLS warmup for the
|
|
8117
|
-
*
|
|
8118
|
-
* and HTTP/2 are already up by the time the first
|
|
8119
|
-
* ``streamGenerateContent`` call lands. Best-effort: 5 s timeout, all
|
|
8120
|
-
* exceptions swallowed at debug level.
|
|
8551
|
+
* Pre-call DNS / TLS warmup for the Cerebras inference endpoint.
|
|
8552
|
+
* Best-effort: 5 s timeout, all exceptions swallowed at debug level.
|
|
8121
8553
|
*/
|
|
8122
8554
|
async warmup() {
|
|
8123
8555
|
try {
|
|
8124
|
-
await fetch(`${this.baseUrl}/models
|
|
8556
|
+
await fetch(`${this.baseUrl}/models`, {
|
|
8125
8557
|
method: "GET",
|
|
8558
|
+
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
8126
8559
|
signal: AbortSignal.timeout(5e3)
|
|
8127
8560
|
});
|
|
8128
8561
|
} catch (err) {
|
|
8129
|
-
getLogger().debug(`
|
|
8562
|
+
getLogger().debug(`Cerebras LLM warmup failed (best-effort): ${String(err)}`);
|
|
8130
8563
|
}
|
|
8131
8564
|
}
|
|
8132
|
-
/** Stream Patter-format LLM chunks from the
|
|
8565
|
+
/** Stream Patter-format LLM chunks from the Cerebras chat completions API. */
|
|
8133
8566
|
async *stream(messages, tools, opts) {
|
|
8134
|
-
const
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
}
|
|
8140
|
-
if (geminiTools) body.tools = geminiTools;
|
|
8141
|
-
const generationConfig = {};
|
|
8142
|
-
if (this.temperature !== void 0) generationConfig.temperature = this.temperature;
|
|
8143
|
-
if (this.maxOutputTokens !== void 0)
|
|
8144
|
-
generationConfig.maxOutputTokens = this.maxOutputTokens;
|
|
8145
|
-
if (Object.keys(generationConfig).length > 0) body.generationConfig = generationConfig;
|
|
8146
|
-
const url = `${this.baseUrl}/models/${encodeURIComponent(this.model)}:streamGenerateContent?alt=sse&key=${encodeURIComponent(this.apiKey)}`;
|
|
8147
|
-
const response = await fetch(url, {
|
|
8148
|
-
method: "POST",
|
|
8149
|
-
headers: { "Content-Type": "application/json" },
|
|
8150
|
-
body: JSON.stringify(body),
|
|
8151
|
-
signal: mergeAbortSignals(opts?.signal, AbortSignal.timeout(3e4))
|
|
8152
|
-
});
|
|
8153
|
-
if (!response.ok) {
|
|
8154
|
-
const errText = await response.text();
|
|
8155
|
-
getLogger().error(`Gemini API error: ${response.status} ${errText}`);
|
|
8156
|
-
return;
|
|
8157
|
-
}
|
|
8158
|
-
const reader = response.body?.getReader();
|
|
8159
|
-
if (!reader) return;
|
|
8160
|
-
const decoder = new TextDecoder();
|
|
8161
|
-
let buffer = "";
|
|
8162
|
-
let nextIndex = 0;
|
|
8163
|
-
let lastUsage;
|
|
8164
|
-
try {
|
|
8165
|
-
while (true) {
|
|
8166
|
-
const { done, value } = await reader.read();
|
|
8167
|
-
if (done) break;
|
|
8168
|
-
buffer += decoder.decode(value, { stream: true });
|
|
8169
|
-
const lines = buffer.split("\n");
|
|
8170
|
-
buffer = lines.pop() || "";
|
|
8171
|
-
for (const line of lines) {
|
|
8172
|
-
const trimmed = line.trim();
|
|
8173
|
-
if (!trimmed.startsWith("data: ")) continue;
|
|
8174
|
-
const data = trimmed.slice(6);
|
|
8175
|
-
if (!data) continue;
|
|
8176
|
-
let payload;
|
|
8177
|
-
try {
|
|
8178
|
-
payload = JSON.parse(data);
|
|
8179
|
-
} catch {
|
|
8180
|
-
continue;
|
|
8181
|
-
}
|
|
8182
|
-
if (payload.usageMetadata) {
|
|
8183
|
-
lastUsage = payload.usageMetadata;
|
|
8184
|
-
}
|
|
8185
|
-
const candidate = payload.candidates?.[0];
|
|
8186
|
-
const parts = candidate?.content?.parts ?? [];
|
|
8187
|
-
for (const part of parts) {
|
|
8188
|
-
if (part.functionCall) {
|
|
8189
|
-
const args = part.functionCall.args ?? {};
|
|
8190
|
-
const callId = part.functionCall.id ?? `gemini_call_${nextIndex}`;
|
|
8191
|
-
yield {
|
|
8192
|
-
type: "tool_call",
|
|
8193
|
-
index: nextIndex,
|
|
8194
|
-
id: callId,
|
|
8195
|
-
name: part.functionCall.name ?? "",
|
|
8196
|
-
arguments: JSON.stringify(args)
|
|
8197
|
-
};
|
|
8198
|
-
nextIndex++;
|
|
8199
|
-
continue;
|
|
8200
|
-
}
|
|
8201
|
-
if (part.text) {
|
|
8202
|
-
yield { type: "text", content: part.text };
|
|
8203
|
-
}
|
|
8204
|
-
}
|
|
8205
|
-
}
|
|
8206
|
-
}
|
|
8207
|
-
} finally {
|
|
8208
|
-
reader.cancel().catch(() => {
|
|
8209
|
-
});
|
|
8210
|
-
}
|
|
8211
|
-
if (lastUsage) {
|
|
8212
|
-
yield {
|
|
8213
|
-
type: "usage",
|
|
8214
|
-
inputTokens: lastUsage.promptTokenCount,
|
|
8215
|
-
outputTokens: lastUsage.candidatesTokenCount,
|
|
8216
|
-
cacheReadInputTokens: lastUsage.cachedContentTokenCount ?? 0
|
|
8217
|
-
};
|
|
8218
|
-
}
|
|
8219
|
-
yield { type: "done" };
|
|
8220
|
-
}
|
|
8221
|
-
};
|
|
8222
|
-
function toGeminiTools(tools) {
|
|
8223
|
-
const functionDeclarations = tools.map((t) => {
|
|
8224
|
-
const fn = t.function ?? t;
|
|
8225
|
-
return {
|
|
8226
|
-
name: String(fn.name ?? ""),
|
|
8227
|
-
description: String(fn.description ?? ""),
|
|
8228
|
-
parameters: fn.parameters ?? { type: "object", properties: {} }
|
|
8567
|
+
const body = {
|
|
8568
|
+
model: this.model,
|
|
8569
|
+
messages,
|
|
8570
|
+
stream: true,
|
|
8571
|
+
stream_options: { include_usage: true }
|
|
8229
8572
|
};
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
}
|
|
8234
|
-
function toGeminiContents(messages) {
|
|
8235
|
-
const systemParts = [];
|
|
8236
|
-
const contents = [];
|
|
8237
|
-
for (const rawMsg of messages) {
|
|
8238
|
-
const role = rawMsg.role;
|
|
8239
|
-
if (role === "system") {
|
|
8240
|
-
if (typeof rawMsg.content === "string" && rawMsg.content) {
|
|
8241
|
-
systemParts.push(rawMsg.content);
|
|
8242
|
-
}
|
|
8243
|
-
continue;
|
|
8573
|
+
if (this.temperature !== void 0) body.temperature = this.temperature;
|
|
8574
|
+
if (this.maxTokens !== void 0) {
|
|
8575
|
+
body.max_completion_tokens = this.maxTokens;
|
|
8244
8576
|
}
|
|
8245
|
-
if (
|
|
8246
|
-
|
|
8247
|
-
|
|
8577
|
+
if (this.responseFormat !== void 0) body.response_format = this.responseFormat;
|
|
8578
|
+
if (this.parallelToolCalls !== void 0) body.parallel_tool_calls = this.parallelToolCalls;
|
|
8579
|
+
if (this.toolChoice !== void 0) body.tool_choice = this.toolChoice;
|
|
8580
|
+
if (this.seed !== void 0) body.seed = this.seed;
|
|
8581
|
+
if (this.topP !== void 0) body.top_p = this.topP;
|
|
8582
|
+
if (this.frequencyPenalty !== void 0) body.frequency_penalty = this.frequencyPenalty;
|
|
8583
|
+
if (this.presencePenalty !== void 0) body.presence_penalty = this.presencePenalty;
|
|
8584
|
+
if (this.stop !== void 0) body.stop = this.stop;
|
|
8585
|
+
if (tools) body.tools = tools;
|
|
8586
|
+
const headers = {
|
|
8587
|
+
"Content-Type": "application/json",
|
|
8588
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
8589
|
+
// Identify the SDK in upstream logs/rate-limit attribution.
|
|
8590
|
+
"User-Agent": `getpatter/${VERSION}`
|
|
8591
|
+
};
|
|
8592
|
+
let payload = JSON.stringify(body);
|
|
8593
|
+
if (this.gzipCompression) {
|
|
8594
|
+
const compressed = await gzipEncode(payload);
|
|
8595
|
+
if (compressed) {
|
|
8596
|
+
payload = compressed;
|
|
8597
|
+
headers["Content-Encoding"] = "gzip";
|
|
8248
8598
|
}
|
|
8249
|
-
continue;
|
|
8250
8599
|
}
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
|
|
8255
|
-
|
|
8256
|
-
|
|
8257
|
-
|
|
8600
|
+
const maxAttempts = 2;
|
|
8601
|
+
let lastErrText = "";
|
|
8602
|
+
let lastStatus = 0;
|
|
8603
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
8604
|
+
const idle = createStreamIdleWatchdog();
|
|
8605
|
+
const response = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
8606
|
+
method: "POST",
|
|
8607
|
+
headers,
|
|
8608
|
+
body: payload,
|
|
8609
|
+
signal: mergeAbortSignals(opts?.signal, idle.signal)
|
|
8610
|
+
});
|
|
8611
|
+
if (response.ok) {
|
|
8258
8612
|
try {
|
|
8259
|
-
|
|
8260
|
-
|
|
8261
|
-
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
functionCall: {
|
|
8266
|
-
name: tc.function?.name ?? "",
|
|
8267
|
-
args,
|
|
8268
|
-
id: tc.id
|
|
8613
|
+
yield* parseOpenAISseStream(response, idle.touch);
|
|
8614
|
+
} catch (err) {
|
|
8615
|
+
if (idle.fired && !opts?.signal?.aborted) {
|
|
8616
|
+
throw new PatterConnectionError(
|
|
8617
|
+
`Cerebras stream idle timeout \u2014 no data for ${LLM_STREAM_IDLE_TIMEOUT_MS / 1e3}s`
|
|
8618
|
+
);
|
|
8269
8619
|
}
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
continue;
|
|
8274
|
-
}
|
|
8275
|
-
if (role === "tool") {
|
|
8276
|
-
const raw = rawMsg.content;
|
|
8277
|
-
let response;
|
|
8278
|
-
if (typeof raw === "string") {
|
|
8279
|
-
try {
|
|
8280
|
-
const parsed = JSON.parse(raw);
|
|
8281
|
-
response = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { result: parsed };
|
|
8282
|
-
} catch {
|
|
8283
|
-
response = { result: raw };
|
|
8620
|
+
throw err;
|
|
8621
|
+
} finally {
|
|
8622
|
+
idle.clear();
|
|
8284
8623
|
}
|
|
8285
|
-
|
|
8286
|
-
response = raw ?? {};
|
|
8624
|
+
return;
|
|
8287
8625
|
}
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
|
|
8292
|
-
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
|
|
8296
|
-
}
|
|
8297
|
-
|
|
8298
|
-
|
|
8626
|
+
idle.clear();
|
|
8627
|
+
lastStatus = response.status;
|
|
8628
|
+
lastErrText = await response.text().catch(() => "");
|
|
8629
|
+
const isRetriable = response.status === 429 || response.status >= 500;
|
|
8630
|
+
const isLastAttempt = attempt >= maxAttempts - 1;
|
|
8631
|
+
if (!isRetriable || isLastAttempt) {
|
|
8632
|
+
if (response.status === 404 && lastErrText.includes("model_not_found")) {
|
|
8633
|
+
getLogger().error(
|
|
8634
|
+
`Cerebras: model "${this.model}" not available on your tier. Override via \`new CerebrasLLM({ model: '<id>' })\` and list tier-available ids with \`GET ${this.baseUrl}/models\` (common: llama3.1-8b, qwen-3-235b-a22b-instruct-2507, llama-3.3-70b on paid). Raw response: ${lastErrText.slice(0, 200)}`
|
|
8635
|
+
);
|
|
8636
|
+
} else {
|
|
8637
|
+
getLogger().error(`Cerebras API error: ${response.status} ${lastErrText.slice(0, 200)}`);
|
|
8638
|
+
}
|
|
8639
|
+
throw new PatterConnectionError(
|
|
8640
|
+
`Cerebras API returned ${response.status}: ${lastErrText.slice(0, 200)}`
|
|
8641
|
+
);
|
|
8642
|
+
}
|
|
8643
|
+
const advisoryMs = parseRateLimitResetMs(response.headers);
|
|
8644
|
+
const exponentialMs = RETRY_BACKOFF_BASE_MS * Math.pow(2, attempt);
|
|
8645
|
+
const delayMs = Math.min(5e3, Math.max(advisoryMs, exponentialMs));
|
|
8646
|
+
getLogger().warn(
|
|
8647
|
+
`Cerebras API ${response.status} (attempt ${attempt + 1}/${maxAttempts}); retrying after ${delayMs}ms`
|
|
8648
|
+
);
|
|
8649
|
+
await new Promise((resolve2, reject) => {
|
|
8650
|
+
const t = setTimeout(resolve2, delayMs);
|
|
8651
|
+
opts?.signal?.addEventListener(
|
|
8652
|
+
"abort",
|
|
8653
|
+
() => {
|
|
8654
|
+
clearTimeout(t);
|
|
8655
|
+
reject(opts.signal.reason);
|
|
8656
|
+
},
|
|
8657
|
+
{ once: true }
|
|
8658
|
+
);
|
|
8299
8659
|
});
|
|
8300
|
-
continue;
|
|
8301
8660
|
}
|
|
8661
|
+
throw new PatterError(`Cerebras API error ${lastStatus}: ${lastErrText || "request failed"}`);
|
|
8302
8662
|
}
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
|
|
8663
|
+
};
|
|
8664
|
+
async function gzipEncode(data) {
|
|
8665
|
+
const CompressionCtor = globalThis.CompressionStream;
|
|
8666
|
+
if (!CompressionCtor) return null;
|
|
8667
|
+
const stream = new CompressionCtor("gzip");
|
|
8668
|
+
const writer = stream.writable.getWriter();
|
|
8669
|
+
const encoder = new TextEncoder();
|
|
8670
|
+
await writer.write(encoder.encode(data));
|
|
8671
|
+
await writer.close();
|
|
8672
|
+
const chunks = [];
|
|
8673
|
+
const reader = stream.readable.getReader();
|
|
8674
|
+
while (true) {
|
|
8675
|
+
const { done, value } = await reader.read();
|
|
8676
|
+
if (done) break;
|
|
8677
|
+
if (value) chunks.push(value);
|
|
8678
|
+
}
|
|
8679
|
+
const total = chunks.reduce((n, c) => n + c.length, 0);
|
|
8680
|
+
const out = new Uint8Array(total);
|
|
8681
|
+
let offset = 0;
|
|
8682
|
+
for (const c of chunks) {
|
|
8683
|
+
out.set(c, offset);
|
|
8684
|
+
offset += c.length;
|
|
8685
|
+
}
|
|
8686
|
+
return out;
|
|
8687
|
+
}
|
|
8688
|
+
function parseRateLimitResetMs(headers) {
|
|
8689
|
+
const candidates = [
|
|
8690
|
+
headers.get("x-ratelimit-reset-tokens-minute"),
|
|
8691
|
+
headers.get("x-ratelimit-reset-requests-minute"),
|
|
8692
|
+
// Some upstreams send the standard ``retry-after`` (seconds).
|
|
8693
|
+
headers.get("retry-after")
|
|
8694
|
+
];
|
|
8695
|
+
let bestMs = 0;
|
|
8696
|
+
for (const raw of candidates) {
|
|
8697
|
+
if (!raw) continue;
|
|
8698
|
+
const parsed = Number.parseFloat(raw);
|
|
8699
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
8700
|
+
const ms = parsed * 1e3;
|
|
8701
|
+
if (ms > bestMs) bestMs = ms;
|
|
8311
8702
|
}
|
|
8312
8703
|
}
|
|
8313
|
-
return
|
|
8704
|
+
return bestMs;
|
|
8314
8705
|
}
|
|
8315
8706
|
|
|
8707
|
+
// src/llm/cerebras.ts
|
|
8708
|
+
var LLM4 = class extends CerebrasLLMProvider {
|
|
8709
|
+
static providerKey = "cerebras";
|
|
8710
|
+
constructor(opts = {}) {
|
|
8711
|
+
const key = opts.apiKey ?? process.env.CEREBRAS_API_KEY;
|
|
8712
|
+
if (!key) {
|
|
8713
|
+
throw new Error(
|
|
8714
|
+
"Cerebras LLM requires an apiKey. Pass { apiKey: 'csk-...' } or set CEREBRAS_API_KEY."
|
|
8715
|
+
);
|
|
8716
|
+
}
|
|
8717
|
+
super({
|
|
8718
|
+
apiKey: key,
|
|
8719
|
+
model: opts.model,
|
|
8720
|
+
baseUrl: opts.baseUrl,
|
|
8721
|
+
gzipCompression: opts.gzipCompression,
|
|
8722
|
+
temperature: opts.temperature,
|
|
8723
|
+
maxTokens: opts.maxTokens,
|
|
8724
|
+
responseFormat: opts.responseFormat,
|
|
8725
|
+
parallelToolCalls: opts.parallelToolCalls,
|
|
8726
|
+
toolChoice: opts.toolChoice,
|
|
8727
|
+
seed: opts.seed,
|
|
8728
|
+
topP: opts.topP,
|
|
8729
|
+
frequencyPenalty: opts.frequencyPenalty,
|
|
8730
|
+
presencePenalty: opts.presencePenalty,
|
|
8731
|
+
stop: opts.stop
|
|
8732
|
+
});
|
|
8733
|
+
}
|
|
8734
|
+
};
|
|
8735
|
+
|
|
8316
8736
|
// src/llm/google.ts
|
|
8737
|
+
init_esm_shims();
|
|
8317
8738
|
var LLM5 = class extends GoogleLLMProvider {
|
|
8318
8739
|
static providerKey = "google";
|
|
8319
8740
|
constructor(opts = {}) {
|
|
@@ -8518,11 +8939,12 @@ var OpenAICompatibleLLMProvider = class {
|
|
|
8518
8939
|
const caller = opts?.caller;
|
|
8519
8940
|
const callee = opts?.callee;
|
|
8520
8941
|
const body = this.buildBody(messages, tools, callId);
|
|
8942
|
+
const idle = createStreamIdleWatchdog(this.timeoutMs);
|
|
8521
8943
|
const response = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
8522
8944
|
method: "POST",
|
|
8523
8945
|
headers: this.buildHeaders(callId, caller, callee),
|
|
8524
8946
|
body: JSON.stringify(body),
|
|
8525
|
-
signal: mergeAbortSignals(opts?.signal,
|
|
8947
|
+
signal: mergeAbortSignals(opts?.signal, idle.signal)
|
|
8526
8948
|
});
|
|
8527
8949
|
if (!response.ok) {
|
|
8528
8950
|
const errText = await response.text();
|
|
@@ -8533,7 +8955,18 @@ var OpenAICompatibleLLMProvider = class {
|
|
|
8533
8955
|
`LLM API returned ${response.status}: ${errText.slice(0, 200)}`
|
|
8534
8956
|
);
|
|
8535
8957
|
}
|
|
8536
|
-
|
|
8958
|
+
try {
|
|
8959
|
+
yield* parseOpenAISseStream(response, idle.touch);
|
|
8960
|
+
} catch (err) {
|
|
8961
|
+
if (idle.fired && !opts?.signal?.aborted) {
|
|
8962
|
+
throw new PatterConnectionError(
|
|
8963
|
+
`LLM stream idle timeout \u2014 no data for ${Math.round(this.timeoutMs / 1e3)}s`
|
|
8964
|
+
);
|
|
8965
|
+
}
|
|
8966
|
+
throw err;
|
|
8967
|
+
} finally {
|
|
8968
|
+
idle.clear();
|
|
8969
|
+
}
|
|
8537
8970
|
}
|
|
8538
8971
|
};
|
|
8539
8972
|
var LLM6 = class extends OpenAICompatibleLLMProvider {
|
|
@@ -8637,13 +9070,437 @@ var LLM9 = class extends OpenAICompatibleLLMProvider {
|
|
|
8637
9070
|
}
|
|
8638
9071
|
};
|
|
8639
9072
|
|
|
9073
|
+
// src/providers/smart-turn.ts
|
|
9074
|
+
init_esm_shims();
|
|
9075
|
+
import * as fs3 from "fs";
|
|
9076
|
+
import * as path2 from "path";
|
|
9077
|
+
var SMART_TURN_MODEL_ENV_VAR = "PATTER_SMART_TURN_MODEL";
|
|
9078
|
+
var SMART_TURN_SAMPLE_RATE = 16e3;
|
|
9079
|
+
var SMART_TURN_MAX_SECONDS = 8;
|
|
9080
|
+
var SMART_TURN_MAX_SAMPLES = SMART_TURN_SAMPLE_RATE * SMART_TURN_MAX_SECONDS;
|
|
9081
|
+
var DEFAULT_SMART_TURN_THRESHOLD = 0.5;
|
|
9082
|
+
var N_FFT = 400;
|
|
9083
|
+
var HOP_LENGTH = 160;
|
|
9084
|
+
var N_MELS = 80;
|
|
9085
|
+
var N_FRAMES = 800;
|
|
9086
|
+
var MEL_FLOOR = 1e-10;
|
|
9087
|
+
var NORM_EPS = 1e-7;
|
|
9088
|
+
var DOWNLOAD_HINT = `Download a smart-turn-v3 ONNX file from https://huggingface.co/pipecat-ai/smart-turn-v3 and either set the ${SMART_TURN_MODEL_ENV_VAR} environment variable to its path or pass modelPath to SmartTurnDetector.load(). The model is not bundled with the SDK (~30 MB).`;
|
|
9089
|
+
function resolveSmartTurnModelPath(modelPath) {
|
|
9090
|
+
let resolved = modelPath;
|
|
9091
|
+
if (!resolved) {
|
|
9092
|
+
resolved = (process.env[SMART_TURN_MODEL_ENV_VAR] ?? "").trim();
|
|
9093
|
+
if (!resolved) {
|
|
9094
|
+
throw new Error(
|
|
9095
|
+
`SmartTurnDetector has no model file configured. ${DOWNLOAD_HINT}`
|
|
9096
|
+
);
|
|
9097
|
+
}
|
|
9098
|
+
}
|
|
9099
|
+
if (!fs3.existsSync(resolved)) {
|
|
9100
|
+
throw new Error(`Smart-turn model file not found: ${resolved}. ${DOWNLOAD_HINT}`);
|
|
9101
|
+
}
|
|
9102
|
+
if (!fs3.statSync(resolved).isFile()) {
|
|
9103
|
+
throw new Error(`Smart-turn model path is not a file: ${resolved}. ${DOWNLOAD_HINT}`);
|
|
9104
|
+
}
|
|
9105
|
+
return path2.resolve(resolved);
|
|
9106
|
+
}
|
|
9107
|
+
function hertzToMelSlaney(freq) {
|
|
9108
|
+
const minLogHertz = 1e3;
|
|
9109
|
+
const minLogMel = 15;
|
|
9110
|
+
const logstep = 27 / Math.log(6.4);
|
|
9111
|
+
if (freq >= minLogHertz) {
|
|
9112
|
+
return minLogMel + Math.log(freq / minLogHertz) * logstep;
|
|
9113
|
+
}
|
|
9114
|
+
return 3 * freq / 200;
|
|
9115
|
+
}
|
|
9116
|
+
function melToHertzSlaney(mels) {
|
|
9117
|
+
const minLogHertz = 1e3;
|
|
9118
|
+
const minLogMel = 15;
|
|
9119
|
+
const logstep = Math.log(6.4) / 27;
|
|
9120
|
+
if (mels >= minLogMel) {
|
|
9121
|
+
return minLogHertz * Math.exp(logstep * (mels - minLogMel));
|
|
9122
|
+
}
|
|
9123
|
+
return 200 * mels / 3;
|
|
9124
|
+
}
|
|
9125
|
+
var melFilterbankCache = null;
|
|
9126
|
+
function melFilterbank() {
|
|
9127
|
+
if (melFilterbankCache) return melFilterbankCache;
|
|
9128
|
+
const numBins = 1 + N_FFT / 2;
|
|
9129
|
+
const fftFreqs = new Float64Array(numBins);
|
|
9130
|
+
for (let k = 0; k < numBins; k++) {
|
|
9131
|
+
fftFreqs[k] = k * (SMART_TURN_SAMPLE_RATE / 2) / (numBins - 1);
|
|
9132
|
+
}
|
|
9133
|
+
const melMin = hertzToMelSlaney(0);
|
|
9134
|
+
const melMax = hertzToMelSlaney(SMART_TURN_SAMPLE_RATE / 2);
|
|
9135
|
+
const filterFreqs = new Float64Array(N_MELS + 2);
|
|
9136
|
+
for (let i = 0; i < N_MELS + 2; i++) {
|
|
9137
|
+
filterFreqs[i] = melToHertzSlaney(melMin + (melMax - melMin) * i / (N_MELS + 1));
|
|
9138
|
+
}
|
|
9139
|
+
const filters = [];
|
|
9140
|
+
for (let m = 0; m < N_MELS; m++) {
|
|
9141
|
+
const lower = filterFreqs[m];
|
|
9142
|
+
const center = filterFreqs[m + 1];
|
|
9143
|
+
const upper = filterFreqs[m + 2];
|
|
9144
|
+
const enorm = 2 / (upper - lower);
|
|
9145
|
+
const dense = new Float64Array(numBins);
|
|
9146
|
+
let startBin = -1;
|
|
9147
|
+
let endBin = -1;
|
|
9148
|
+
for (let k = 0; k < numBins; k++) {
|
|
9149
|
+
const down = (fftFreqs[k] - lower) / (center - lower);
|
|
9150
|
+
const up = (upper - fftFreqs[k]) / (upper - center);
|
|
9151
|
+
const w = Math.max(0, Math.min(down, up)) * enorm;
|
|
9152
|
+
dense[k] = w;
|
|
9153
|
+
if (w > 0) {
|
|
9154
|
+
if (startBin === -1) startBin = k;
|
|
9155
|
+
endBin = k;
|
|
9156
|
+
}
|
|
9157
|
+
}
|
|
9158
|
+
if (startBin === -1) {
|
|
9159
|
+
filters.push({ startBin: 0, weights: new Float64Array(0) });
|
|
9160
|
+
} else {
|
|
9161
|
+
filters.push({ startBin, weights: dense.slice(startBin, endBin + 1) });
|
|
9162
|
+
}
|
|
9163
|
+
}
|
|
9164
|
+
melFilterbankCache = filters;
|
|
9165
|
+
return filters;
|
|
9166
|
+
}
|
|
9167
|
+
var hannWindowCache = null;
|
|
9168
|
+
function hannWindow() {
|
|
9169
|
+
if (!hannWindowCache) {
|
|
9170
|
+
const w = new Float64Array(N_FFT);
|
|
9171
|
+
for (let n = 0; n < N_FFT; n++) {
|
|
9172
|
+
w[n] = 0.5 - 0.5 * Math.cos(2 * Math.PI * n / N_FFT);
|
|
9173
|
+
}
|
|
9174
|
+
hannWindowCache = w;
|
|
9175
|
+
}
|
|
9176
|
+
return hannWindowCache;
|
|
9177
|
+
}
|
|
9178
|
+
var dft25Cos = null;
|
|
9179
|
+
var dft25Sin = null;
|
|
9180
|
+
function dft25Tables() {
|
|
9181
|
+
if (!dft25Cos || !dft25Sin) {
|
|
9182
|
+
dft25Cos = new Float64Array(25 * 25);
|
|
9183
|
+
dft25Sin = new Float64Array(25 * 25);
|
|
9184
|
+
for (let k = 0; k < 25; k++) {
|
|
9185
|
+
for (let j = 0; j < 25; j++) {
|
|
9186
|
+
const angle = -2 * Math.PI * k * j / 25;
|
|
9187
|
+
dft25Cos[k * 25 + j] = Math.cos(angle);
|
|
9188
|
+
dft25Sin[k * 25 + j] = Math.sin(angle);
|
|
9189
|
+
}
|
|
9190
|
+
}
|
|
9191
|
+
}
|
|
9192
|
+
return { cos: dft25Cos, sin: dft25Sin };
|
|
9193
|
+
}
|
|
9194
|
+
var fftTwiddleCos = /* @__PURE__ */ new Map();
|
|
9195
|
+
var fftTwiddleSin = /* @__PURE__ */ new Map();
|
|
9196
|
+
var fftScratch = /* @__PURE__ */ new Map();
|
|
9197
|
+
function fftTables(n) {
|
|
9198
|
+
let cos = fftTwiddleCos.get(n);
|
|
9199
|
+
let sin = fftTwiddleSin.get(n);
|
|
9200
|
+
if (!cos || !sin) {
|
|
9201
|
+
const half = n / 2;
|
|
9202
|
+
cos = new Float64Array(half);
|
|
9203
|
+
sin = new Float64Array(half);
|
|
9204
|
+
for (let k = 0; k < half; k++) {
|
|
9205
|
+
const angle = -2 * Math.PI * k / n;
|
|
9206
|
+
cos[k] = Math.cos(angle);
|
|
9207
|
+
sin[k] = Math.sin(angle);
|
|
9208
|
+
}
|
|
9209
|
+
fftTwiddleCos.set(n, cos);
|
|
9210
|
+
fftTwiddleSin.set(n, sin);
|
|
9211
|
+
}
|
|
9212
|
+
return { cos, sin };
|
|
9213
|
+
}
|
|
9214
|
+
function fftScratchFor(n) {
|
|
9215
|
+
let bufs = fftScratch.get(n);
|
|
9216
|
+
if (!bufs) {
|
|
9217
|
+
bufs = [
|
|
9218
|
+
new Float64Array(n),
|
|
9219
|
+
new Float64Array(n),
|
|
9220
|
+
new Float64Array(n),
|
|
9221
|
+
new Float64Array(n)
|
|
9222
|
+
];
|
|
9223
|
+
fftScratch.set(n, bufs);
|
|
9224
|
+
}
|
|
9225
|
+
return bufs;
|
|
9226
|
+
}
|
|
9227
|
+
var dft25OutRe = new Float64Array(25);
|
|
9228
|
+
var dft25OutIm = new Float64Array(25);
|
|
9229
|
+
function fftComplex(re, im) {
|
|
9230
|
+
const n = re.length;
|
|
9231
|
+
if (n === 25) {
|
|
9232
|
+
const { cos: cos2, sin: sin2 } = dft25Tables();
|
|
9233
|
+
for (let k = 0; k < 25; k++) {
|
|
9234
|
+
let sumRe = 0;
|
|
9235
|
+
let sumIm = 0;
|
|
9236
|
+
const row = k * 25;
|
|
9237
|
+
for (let j = 0; j < 25; j++) {
|
|
9238
|
+
const c = cos2[row + j];
|
|
9239
|
+
const s = sin2[row + j];
|
|
9240
|
+
sumRe += re[j] * c - im[j] * s;
|
|
9241
|
+
sumIm += re[j] * s + im[j] * c;
|
|
9242
|
+
}
|
|
9243
|
+
dft25OutRe[k] = sumRe;
|
|
9244
|
+
dft25OutIm[k] = sumIm;
|
|
9245
|
+
}
|
|
9246
|
+
re.set(dft25OutRe);
|
|
9247
|
+
im.set(dft25OutIm);
|
|
9248
|
+
return;
|
|
9249
|
+
}
|
|
9250
|
+
if (n === 1) return;
|
|
9251
|
+
const half = n / 2;
|
|
9252
|
+
const [evenRe, evenIm, oddRe, oddIm] = fftScratchFor(half);
|
|
9253
|
+
for (let i = 0; i < half; i++) {
|
|
9254
|
+
evenRe[i] = re[2 * i];
|
|
9255
|
+
evenIm[i] = im[2 * i];
|
|
9256
|
+
oddRe[i] = re[2 * i + 1];
|
|
9257
|
+
oddIm[i] = im[2 * i + 1];
|
|
9258
|
+
}
|
|
9259
|
+
fftComplex(evenRe.subarray(0, half), evenIm.subarray(0, half));
|
|
9260
|
+
fftComplex(oddRe.subarray(0, half), oddIm.subarray(0, half));
|
|
9261
|
+
const { cos, sin } = fftTables(n);
|
|
9262
|
+
for (let k = 0; k < half; k++) {
|
|
9263
|
+
const wr = cos[k];
|
|
9264
|
+
const wi = sin[k];
|
|
9265
|
+
const tr = wr * oddRe[k] - wi * oddIm[k];
|
|
9266
|
+
const ti = wr * oddIm[k] + wi * oddRe[k];
|
|
9267
|
+
re[k] = evenRe[k] + tr;
|
|
9268
|
+
im[k] = evenIm[k] + ti;
|
|
9269
|
+
re[k + half] = evenRe[k] - tr;
|
|
9270
|
+
im[k + half] = evenIm[k] - ti;
|
|
9271
|
+
}
|
|
9272
|
+
}
|
|
9273
|
+
function prepareInputWindow(samples) {
|
|
9274
|
+
const out = new Float64Array(SMART_TURN_MAX_SAMPLES);
|
|
9275
|
+
const n = samples.length;
|
|
9276
|
+
if (n >= SMART_TURN_MAX_SAMPLES) {
|
|
9277
|
+
const offset = n - SMART_TURN_MAX_SAMPLES;
|
|
9278
|
+
for (let i = 0; i < SMART_TURN_MAX_SAMPLES; i++) out[i] = samples[offset + i];
|
|
9279
|
+
} else {
|
|
9280
|
+
const padding = SMART_TURN_MAX_SAMPLES - n;
|
|
9281
|
+
for (let i = 0; i < n; i++) out[padding + i] = samples[i];
|
|
9282
|
+
}
|
|
9283
|
+
let mean = 0;
|
|
9284
|
+
for (let i = 0; i < SMART_TURN_MAX_SAMPLES; i++) mean += out[i];
|
|
9285
|
+
mean /= SMART_TURN_MAX_SAMPLES;
|
|
9286
|
+
let variance = 0;
|
|
9287
|
+
for (let i = 0; i < SMART_TURN_MAX_SAMPLES; i++) {
|
|
9288
|
+
const d = out[i] - mean;
|
|
9289
|
+
variance += d * d;
|
|
9290
|
+
}
|
|
9291
|
+
variance /= SMART_TURN_MAX_SAMPLES;
|
|
9292
|
+
const scale = 1 / Math.sqrt(variance + NORM_EPS);
|
|
9293
|
+
for (let i = 0; i < SMART_TURN_MAX_SAMPLES; i++) out[i] = (out[i] - mean) * scale;
|
|
9294
|
+
return out;
|
|
9295
|
+
}
|
|
9296
|
+
async function computeWhisperLogMelFeatures(window) {
|
|
9297
|
+
if (window.length !== SMART_TURN_MAX_SAMPLES) {
|
|
9298
|
+
throw new Error(
|
|
9299
|
+
`expected ${SMART_TURN_MAX_SAMPLES} samples, got ${window.length}; run prepareInputWindow() first`
|
|
9300
|
+
);
|
|
9301
|
+
}
|
|
9302
|
+
const half = N_FFT / 2;
|
|
9303
|
+
const paddedLen = SMART_TURN_MAX_SAMPLES + N_FFT;
|
|
9304
|
+
const numBins = 1 + N_FFT / 2;
|
|
9305
|
+
const padded = new Float64Array(paddedLen);
|
|
9306
|
+
for (let i = 0; i < half; i++) padded[i] = window[half - i];
|
|
9307
|
+
padded.set(window, half);
|
|
9308
|
+
for (let i = 0; i < half; i++) {
|
|
9309
|
+
padded[half + SMART_TURN_MAX_SAMPLES + i] = window[SMART_TURN_MAX_SAMPLES - 2 - i];
|
|
9310
|
+
}
|
|
9311
|
+
const hann = hannWindow();
|
|
9312
|
+
const filters = melFilterbank();
|
|
9313
|
+
const totalFrames = 1 + Math.floor((paddedLen - N_FFT) / HOP_LENGTH);
|
|
9314
|
+
const logSpec = new Float64Array(N_MELS * N_FRAMES);
|
|
9315
|
+
const re = new Float64Array(N_FFT);
|
|
9316
|
+
const im = new Float64Array(N_FFT);
|
|
9317
|
+
const power = new Float64Array(numBins);
|
|
9318
|
+
let maxLog = -Infinity;
|
|
9319
|
+
for (let t = 0; t < totalFrames - 1; t++) {
|
|
9320
|
+
const start = t * HOP_LENGTH;
|
|
9321
|
+
for (let j = 0; j < N_FFT; j++) {
|
|
9322
|
+
re[j] = padded[start + j] * hann[j];
|
|
9323
|
+
im[j] = 0;
|
|
9324
|
+
}
|
|
9325
|
+
fftComplex(re, im);
|
|
9326
|
+
for (let k = 0; k < numBins; k++) {
|
|
9327
|
+
power[k] = re[k] * re[k] + im[k] * im[k];
|
|
9328
|
+
}
|
|
9329
|
+
for (let m = 0; m < N_MELS; m++) {
|
|
9330
|
+
const { startBin, weights } = filters[m];
|
|
9331
|
+
let acc = 0;
|
|
9332
|
+
for (let j = 0; j < weights.length; j++) {
|
|
9333
|
+
acc += power[startBin + j] * weights[j];
|
|
9334
|
+
}
|
|
9335
|
+
const v = Math.log10(Math.max(acc, MEL_FLOOR));
|
|
9336
|
+
logSpec[m * N_FRAMES + t] = v;
|
|
9337
|
+
if (v > maxLog) maxLog = v;
|
|
9338
|
+
}
|
|
9339
|
+
if ((t & 127) === 127) {
|
|
9340
|
+
await new Promise((resolve2) => setImmediate(resolve2));
|
|
9341
|
+
}
|
|
9342
|
+
}
|
|
9343
|
+
const floor = maxLog - 8;
|
|
9344
|
+
const out = new Float32Array(N_MELS * N_FRAMES);
|
|
9345
|
+
for (let i = 0; i < logSpec.length; i++) {
|
|
9346
|
+
out[i] = (Math.max(logSpec[i], floor) + 4) / 4;
|
|
9347
|
+
}
|
|
9348
|
+
return out;
|
|
9349
|
+
}
|
|
9350
|
+
async function featuresFromPcm16(pcm16Window) {
|
|
9351
|
+
const numSamples = Math.floor(pcm16Window.length / 2);
|
|
9352
|
+
const samples = new Float64Array(numSamples);
|
|
9353
|
+
for (let i = 0; i < numSamples; i++) {
|
|
9354
|
+
samples[i] = pcm16Window.readInt16LE(i * 2) / 32768;
|
|
9355
|
+
}
|
|
9356
|
+
return computeWhisperLogMelFeatures(prepareInputWindow(samples));
|
|
9357
|
+
}
|
|
9358
|
+
var SmartTurnDetector = class _SmartTurnDetector {
|
|
9359
|
+
constructor(runtime, session, thresholdValue) {
|
|
9360
|
+
this.runtime = runtime;
|
|
9361
|
+
this.session = session;
|
|
9362
|
+
this.thresholdValue = thresholdValue;
|
|
9363
|
+
}
|
|
9364
|
+
runtime;
|
|
9365
|
+
session;
|
|
9366
|
+
thresholdValue;
|
|
9367
|
+
closed = false;
|
|
9368
|
+
/**
|
|
9369
|
+
* Load the smart-turn v3 ONNX model and return a ready detector.
|
|
9370
|
+
* Throws with download instructions when no model file is configured
|
|
9371
|
+
* (see {@link SMART_TURN_MODEL_ENV_VAR}), and with install instructions
|
|
9372
|
+
* when `onnxruntime-node` is missing.
|
|
9373
|
+
*/
|
|
9374
|
+
static async load(options = {}) {
|
|
9375
|
+
const threshold = options.threshold ?? DEFAULT_SMART_TURN_THRESHOLD;
|
|
9376
|
+
if (!(threshold >= 0 && threshold <= 1)) {
|
|
9377
|
+
throw new Error("threshold must be within [0.0, 1.0]");
|
|
9378
|
+
}
|
|
9379
|
+
const modelPath = resolveSmartTurnModelPath(options.modelPath);
|
|
9380
|
+
const runtime = await loadOnnxRuntime("SmartTurnDetector");
|
|
9381
|
+
const session = await runtime.InferenceSession.create(modelPath, {
|
|
9382
|
+
interOpNumThreads: 1,
|
|
9383
|
+
intraOpNumThreads: 1,
|
|
9384
|
+
executionMode: "sequential",
|
|
9385
|
+
graphOptimizationLevel: "all",
|
|
9386
|
+
executionProviders: options.forceCpu === false ? void 0 : ["cpu"]
|
|
9387
|
+
});
|
|
9388
|
+
return new _SmartTurnDetector(runtime, session, threshold);
|
|
9389
|
+
}
|
|
9390
|
+
/**
|
|
9391
|
+
* Like {@link load}, but degrade instead of throw.
|
|
9392
|
+
*
|
|
9393
|
+
* Resolves to `undefined` — after a single clear warning — when semantic
|
|
9394
|
+
* turn detection is not provisioned: the optional `onnxruntime-node`
|
|
9395
|
+
* dependency is missing, no model file is configured, or the configured
|
|
9396
|
+
* file cannot be loaded. Intended for deployments where the detector is
|
|
9397
|
+
* a soft upgrade:
|
|
9398
|
+
*
|
|
9399
|
+
* ```ts
|
|
9400
|
+
* const agent = phone.agent({
|
|
9401
|
+
* ...,
|
|
9402
|
+
* turnDetector: await SmartTurnDetector.maybeLoad(),
|
|
9403
|
+
* });
|
|
9404
|
+
* ```
|
|
9405
|
+
*
|
|
9406
|
+
* `turnDetector: undefined` keeps the plain VAD-silence endpointing, so
|
|
9407
|
+
* the agent starts (and the call behaves) exactly as if the feature were
|
|
9408
|
+
* never enabled — it never crashes the app.
|
|
9409
|
+
*
|
|
9410
|
+
* An out-of-range `threshold` still throws: that is a configuration bug,
|
|
9411
|
+
* not a provisioning gap. Mirror of the Python
|
|
9412
|
+
* `SmartTurnDetector.maybe_load`.
|
|
9413
|
+
*/
|
|
9414
|
+
static async maybeLoad(options = {}) {
|
|
9415
|
+
const threshold = options.threshold ?? DEFAULT_SMART_TURN_THRESHOLD;
|
|
9416
|
+
if (!(threshold >= 0 && threshold <= 1)) {
|
|
9417
|
+
throw new Error("threshold must be within [0.0, 1.0]");
|
|
9418
|
+
}
|
|
9419
|
+
try {
|
|
9420
|
+
return await _SmartTurnDetector.load(options);
|
|
9421
|
+
} catch (err) {
|
|
9422
|
+
getLogger().warn(
|
|
9423
|
+
`Semantic turn detection unavailable \u2014 falling back to plain VAD-silence endpointing: ${err instanceof Error ? err.message : String(err)}`
|
|
9424
|
+
);
|
|
9425
|
+
return void 0;
|
|
9426
|
+
}
|
|
9427
|
+
}
|
|
9428
|
+
/**
|
|
9429
|
+
* Internal factory used by tests — bypasses onnxruntime-node loading.
|
|
9430
|
+
* @internal
|
|
9431
|
+
*/
|
|
9432
|
+
static fromOnnxSession(runtime, session, options = {}) {
|
|
9433
|
+
return new _SmartTurnDetector(
|
|
9434
|
+
runtime,
|
|
9435
|
+
session,
|
|
9436
|
+
options.threshold ?? DEFAULT_SMART_TURN_THRESHOLD
|
|
9437
|
+
);
|
|
9438
|
+
}
|
|
9439
|
+
/** Identifier of the underlying model (`smart-turn-v3`). */
|
|
9440
|
+
get model() {
|
|
9441
|
+
return "smart-turn-v3";
|
|
9442
|
+
}
|
|
9443
|
+
/** Identifier of the runtime backend (`ONNX`). */
|
|
9444
|
+
get provider() {
|
|
9445
|
+
return "ONNX";
|
|
9446
|
+
}
|
|
9447
|
+
/** Input sample rate the model expects (16 000 Hz). */
|
|
9448
|
+
get sampleRate() {
|
|
9449
|
+
return SMART_TURN_SAMPLE_RATE;
|
|
9450
|
+
}
|
|
9451
|
+
/** Maximum audio context the model consumes per prediction (8 s). */
|
|
9452
|
+
get maxWindowSeconds() {
|
|
9453
|
+
return SMART_TURN_MAX_SECONDS;
|
|
9454
|
+
}
|
|
9455
|
+
/** End-of-turn probability at/above which the turn is complete. */
|
|
9456
|
+
get threshold() {
|
|
9457
|
+
return this.thresholdValue;
|
|
9458
|
+
}
|
|
9459
|
+
/**
|
|
9460
|
+
* End-of-turn probability for the given recent-audio window.
|
|
9461
|
+
*
|
|
9462
|
+
* @param pcm16Window Mono int16 little-endian PCM at 16 kHz — ideally
|
|
9463
|
+
* the full audio of the caller's current turn, up to 8 s (the
|
|
9464
|
+
* handler keeps a rolling 8 s buffer). Longer input is truncated to
|
|
9465
|
+
* the most recent 8 s; shorter input is left-padded with silence,
|
|
9466
|
+
* matching the reference preprocessing exactly.
|
|
9467
|
+
* @returns Probability in `[0, 1]` that the turn is COMPLETE (the
|
|
9468
|
+
* graph applies the sigmoid internally). Returns 0 for an empty
|
|
9469
|
+
* window.
|
|
9470
|
+
*/
|
|
9471
|
+
async predict(pcm16Window) {
|
|
9472
|
+
if (this.closed || this.session === null) {
|
|
9473
|
+
throw new Error("SmartTurnDetector is closed");
|
|
9474
|
+
}
|
|
9475
|
+
if (pcm16Window.length < 2) {
|
|
9476
|
+
return 0;
|
|
9477
|
+
}
|
|
9478
|
+
const features = await featuresFromPcm16(pcm16Window);
|
|
9479
|
+
const { Tensor } = this.runtime;
|
|
9480
|
+
const feeds = {
|
|
9481
|
+
input_features: new Tensor("float32", features, [1, N_MELS, N_FRAMES])
|
|
9482
|
+
};
|
|
9483
|
+
const results = await this.session.run(feeds);
|
|
9484
|
+
const first = Object.values(results)[0];
|
|
9485
|
+
const data = first?.data;
|
|
9486
|
+
const probability = data?.[0] ?? 0;
|
|
9487
|
+
return Math.min(1, Math.max(0, probability));
|
|
9488
|
+
}
|
|
9489
|
+
/** Release the ONNX session. Idempotent. */
|
|
9490
|
+
async close() {
|
|
9491
|
+
if (this.closed) return;
|
|
9492
|
+
this.closed = true;
|
|
9493
|
+
this.session = null;
|
|
9494
|
+
}
|
|
9495
|
+
};
|
|
9496
|
+
|
|
8640
9497
|
// src/providers/deepfilternet-filter.ts
|
|
8641
9498
|
init_esm_shims();
|
|
8642
9499
|
function log() {
|
|
8643
9500
|
return getLogger();
|
|
8644
9501
|
}
|
|
8645
9502
|
var DEEPFILTERNET_SR = 48e3;
|
|
8646
|
-
async function
|
|
9503
|
+
async function loadOnnxRuntime2() {
|
|
8647
9504
|
try {
|
|
8648
9505
|
const specifier = "onnxruntime-node";
|
|
8649
9506
|
const mod = await import(specifier);
|
|
@@ -8750,7 +9607,7 @@ var DeepFilterNetFilter = class {
|
|
|
8750
9607
|
return null;
|
|
8751
9608
|
}
|
|
8752
9609
|
if (this.ort === null) {
|
|
8753
|
-
this.ort = await
|
|
9610
|
+
this.ort = await loadOnnxRuntime2();
|
|
8754
9611
|
}
|
|
8755
9612
|
if (this.ort === null) {
|
|
8756
9613
|
if (!this.warned && !this.silenceWarnings) {
|
|
@@ -9019,6 +9876,10 @@ var ChatContext = class _ChatContext {
|
|
|
9019
9876
|
} else {
|
|
9020
9877
|
this.items = maxMessages > 0 ? [...this.items.slice(-maxMessages)] : [];
|
|
9021
9878
|
}
|
|
9879
|
+
const start = this.items.length > 0 && this.items[0].role === "system" ? 1 : 0;
|
|
9880
|
+
while (this.items.length > start && this.items[start].role === "tool") {
|
|
9881
|
+
this.items.splice(start, 1);
|
|
9882
|
+
}
|
|
9022
9883
|
}
|
|
9023
9884
|
// -------------------------------------------------------------------------
|
|
9024
9885
|
// Provider format conversion
|
|
@@ -9054,6 +9915,10 @@ var ChatContext = class _ChatContext {
|
|
|
9054
9915
|
}
|
|
9055
9916
|
continue;
|
|
9056
9917
|
}
|
|
9918
|
+
if (msg.role === "tool") {
|
|
9919
|
+
messages.push({ role: "user", content: `[tool result] ${msg.content}` });
|
|
9920
|
+
continue;
|
|
9921
|
+
}
|
|
9057
9922
|
messages.push({ role: msg.role, content: msg.content });
|
|
9058
9923
|
}
|
|
9059
9924
|
return { system, messages };
|
|
@@ -9268,11 +10133,13 @@ var IVRActivity = class {
|
|
|
9268
10133
|
}
|
|
9269
10134
|
/** Record the current user-turn state (e.g. `"listening"`, `"away"`). */
|
|
9270
10135
|
noteUserState(state) {
|
|
10136
|
+
if (!this.started) return;
|
|
9271
10137
|
this.currentUserState = state;
|
|
9272
10138
|
this.scheduleSilenceCheck();
|
|
9273
10139
|
}
|
|
9274
10140
|
/** Record the current agent-turn state (e.g. `"idle"`, `"listening"`). */
|
|
9275
10141
|
noteAgentState(state) {
|
|
10142
|
+
if (!this.started) return;
|
|
9276
10143
|
this.currentAgentState = state;
|
|
9277
10144
|
this.scheduleSilenceCheck();
|
|
9278
10145
|
}
|
|
@@ -9352,8 +10219,8 @@ var IVRActivity = class {
|
|
|
9352
10219
|
|
|
9353
10220
|
// src/audio/background-audio.ts
|
|
9354
10221
|
init_esm_shims();
|
|
9355
|
-
import { promises as
|
|
9356
|
-
import
|
|
10222
|
+
import { promises as fs4 } from "fs";
|
|
10223
|
+
import path3 from "path";
|
|
9357
10224
|
import { fileURLToPath } from "url";
|
|
9358
10225
|
var BuiltinAudioClip = {
|
|
9359
10226
|
CITY_AMBIENCE: "city-ambience.ogg",
|
|
@@ -9366,8 +10233,8 @@ var BuiltinAudioClip = {
|
|
|
9366
10233
|
};
|
|
9367
10234
|
function builtinClipPath(clip) {
|
|
9368
10235
|
const meta = typeof import.meta !== "undefined" ? import.meta : void 0;
|
|
9369
|
-
const here = meta?.url ?
|
|
9370
|
-
return
|
|
10236
|
+
const here = meta?.url ? path3.dirname(fileURLToPath(meta.url)) : typeof __dirname !== "undefined" ? __dirname : process.cwd();
|
|
10237
|
+
return path3.resolve(here, "..", "resources", "audio", clip);
|
|
9371
10238
|
}
|
|
9372
10239
|
var INT16_MIN = -32768;
|
|
9373
10240
|
var INT16_MAX = 32767;
|
|
@@ -9536,7 +10403,7 @@ var BackgroundAudioPlayer = class {
|
|
|
9536
10403
|
return source.decode(source.path);
|
|
9537
10404
|
case "builtin": {
|
|
9538
10405
|
const p = builtinClipPath(source.clip);
|
|
9539
|
-
const header = await
|
|
10406
|
+
const header = await fs4.readFile(p, { flag: "r" }).then((buf) => buf.subarray(0, 4));
|
|
9540
10407
|
if (header.toString("ascii") !== "OggS") {
|
|
9541
10408
|
throw new Error(`Bundled clip ${source.clip} is not a valid Ogg file`);
|
|
9542
10409
|
}
|
|
@@ -9547,170 +10414,25 @@ var BackgroundAudioPlayer = class {
|
|
|
9547
10414
|
applyGain(pcm, gain) {
|
|
9548
10415
|
if (gain === 1) return pcm;
|
|
9549
10416
|
const n = pcm.length >> 1;
|
|
9550
|
-
const out = Buffer.allocUnsafe(pcm.length);
|
|
9551
|
-
for (let i = 0; i < n; i++) {
|
|
9552
|
-
out.writeInt16LE(clipInt16(Math.round(pcm.readInt16LE(i * 2) * gain)), i * 2);
|
|
9553
|
-
}
|
|
9554
|
-
return out;
|
|
9555
|
-
}
|
|
9556
|
-
resampleTo(dstSr) {
|
|
9557
|
-
if (this.pcm === null) return Buffer.alloc(0);
|
|
9558
|
-
if (dstSr === this.sourceSr) return this.pcm;
|
|
9559
|
-
const cached = this.resampleCache.get(dstSr);
|
|
9560
|
-
if (cached) return cached;
|
|
9561
|
-
const resampled = resamplePcm(this.pcm, this.sourceSr, dstSr);
|
|
9562
|
-
this.resampleCache.set(dstSr, resampled);
|
|
9563
|
-
return resampled;
|
|
9564
|
-
}
|
|
9565
|
-
};
|
|
9566
|
-
function isAudioConfig(value) {
|
|
9567
|
-
return typeof value === "object" && value !== null && "source" in value && typeof value.source === "object";
|
|
9568
|
-
}
|
|
9569
|
-
|
|
9570
|
-
// src/providers/twilio-adapter.ts
|
|
9571
|
-
init_esm_shims();
|
|
9572
|
-
var TWILIO_API_BASE = "https://api.twilio.com/2010-04-01";
|
|
9573
|
-
var TwilioAdapter = class _TwilioAdapter {
|
|
9574
|
-
accountSid;
|
|
9575
|
-
region;
|
|
9576
|
-
baseUrl;
|
|
9577
|
-
authHeader;
|
|
9578
|
-
constructor(accountSid, authToken, opts = {}) {
|
|
9579
|
-
if (!accountSid) throw new Error("TwilioAdapter: accountSid is required");
|
|
9580
|
-
if (!authToken) throw new Error("TwilioAdapter: authToken is required");
|
|
9581
|
-
this.accountSid = accountSid;
|
|
9582
|
-
this.region = opts.region;
|
|
9583
|
-
this.baseUrl = opts.region ? `https://api.${opts.region}.twilio.com/2010-04-01` : TWILIO_API_BASE;
|
|
9584
|
-
this.authHeader = `Basic ${Buffer.from(`${accountSid}:${authToken}`).toString("base64")}`;
|
|
9585
|
-
}
|
|
9586
|
-
async request(method, path3, body) {
|
|
9587
|
-
const url = `${this.baseUrl}/Accounts/${encodeURIComponent(this.accountSid)}${path3}`;
|
|
9588
|
-
const headers = { Authorization: this.authHeader };
|
|
9589
|
-
if (body) headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
9590
|
-
const response = await fetch(url, {
|
|
9591
|
-
method,
|
|
9592
|
-
headers,
|
|
9593
|
-
body: body ? body.toString() : void 0,
|
|
9594
|
-
signal: AbortSignal.timeout(3e4)
|
|
9595
|
-
});
|
|
9596
|
-
const text = await response.text();
|
|
9597
|
-
if (!response.ok) {
|
|
9598
|
-
throw new Error(`Twilio ${method} ${path3} failed: ${response.status} ${text}`);
|
|
9599
|
-
}
|
|
9600
|
-
if (!text) return {};
|
|
9601
|
-
try {
|
|
9602
|
-
return JSON.parse(text);
|
|
9603
|
-
} catch (e) {
|
|
9604
|
-
throw new Error(`Twilio returned non-JSON response: ${String(e)}`);
|
|
9605
|
-
}
|
|
9606
|
-
}
|
|
9607
|
-
/**
|
|
9608
|
-
* Provision a local phone number in the given country.
|
|
9609
|
-
*
|
|
9610
|
-
* Lists available local numbers, then purchases the first match.
|
|
9611
|
-
*/
|
|
9612
|
-
async provisionNumber(opts) {
|
|
9613
|
-
const country = encodeURIComponent(opts.countryCode);
|
|
9614
|
-
const queryParts = ["PageSize=1"];
|
|
9615
|
-
if (opts.areaCode) queryParts.push(`AreaCode=${encodeURIComponent(opts.areaCode)}`);
|
|
9616
|
-
const path3 = `/AvailablePhoneNumbers/${country}/Local.json?${queryParts.join("&")}`;
|
|
9617
|
-
const available = await this.request("GET", path3);
|
|
9618
|
-
const first = available.available_phone_numbers?.[0]?.phone_number;
|
|
9619
|
-
if (!first) {
|
|
9620
|
-
throw new Error(`TwilioAdapter: no numbers available for country ${opts.countryCode}`);
|
|
9621
|
-
}
|
|
9622
|
-
const body = new URLSearchParams({ PhoneNumber: first });
|
|
9623
|
-
const purchased = await this.request(
|
|
9624
|
-
"POST",
|
|
9625
|
-
"/IncomingPhoneNumbers.json",
|
|
9626
|
-
body
|
|
9627
|
-
);
|
|
9628
|
-
if (!purchased.sid || !purchased.phone_number) {
|
|
9629
|
-
throw new Error("TwilioAdapter: malformed response from IncomingPhoneNumbers.create");
|
|
9630
|
-
}
|
|
9631
|
-
return { phoneNumber: purchased.phone_number, sid: purchased.sid };
|
|
9632
|
-
}
|
|
9633
|
-
/** Update an already-purchased number to point at our voice webhook. */
|
|
9634
|
-
async configureNumber(phoneNumberSid, opts) {
|
|
9635
|
-
if (!phoneNumberSid) throw new Error("TwilioAdapter: phoneNumberSid is required");
|
|
9636
|
-
const body = new URLSearchParams({
|
|
9637
|
-
VoiceUrl: opts.voiceUrl,
|
|
9638
|
-
VoiceMethod: "POST"
|
|
9639
|
-
});
|
|
9640
|
-
if (opts.statusCallback) body.set("StatusCallback", opts.statusCallback);
|
|
9641
|
-
await this.request(
|
|
9642
|
-
"POST",
|
|
9643
|
-
`/IncomingPhoneNumbers/${encodeURIComponent(phoneNumberSid)}.json`,
|
|
9644
|
-
body
|
|
9645
|
-
);
|
|
9646
|
-
}
|
|
9647
|
-
/** Place an outbound call. Returns the Twilio call SID. */
|
|
9648
|
-
async initiateCall(opts) {
|
|
9649
|
-
if (!opts.url && !opts.streamUrl) {
|
|
9650
|
-
throw new Error("TwilioAdapter: initiateCall requires either url or streamUrl");
|
|
9651
|
-
}
|
|
9652
|
-
const body = new URLSearchParams({
|
|
9653
|
-
From: opts.from,
|
|
9654
|
-
To: opts.to
|
|
9655
|
-
});
|
|
9656
|
-
if (opts.url) {
|
|
9657
|
-
body.set("Url", opts.url);
|
|
9658
|
-
} else if (opts.streamUrl) {
|
|
9659
|
-
body.set("Twiml", _TwilioAdapter.generateStreamTwiml(opts.streamUrl));
|
|
9660
|
-
}
|
|
9661
|
-
if (opts.statusCallback) body.set("StatusCallback", opts.statusCallback);
|
|
9662
|
-
if (opts.machineDetection) body.set("MachineDetection", opts.machineDetection);
|
|
9663
|
-
if (opts.extraParams) {
|
|
9664
|
-
for (const [key, value] of Object.entries(opts.extraParams)) {
|
|
9665
|
-
body.set(key, value);
|
|
9666
|
-
}
|
|
9667
|
-
}
|
|
9668
|
-
const call = await this.request("POST", "/Calls.json", body);
|
|
9669
|
-
if (!call.sid) {
|
|
9670
|
-
throw new Error("TwilioAdapter: Calls.create returned no SID");
|
|
9671
|
-
}
|
|
9672
|
-
return { callSid: call.sid };
|
|
9673
|
-
}
|
|
9674
|
-
/**
|
|
9675
|
-
* Build a ``<Response><Connect><Stream url="...">`` TwiML document.
|
|
9676
|
-
*
|
|
9677
|
-
* ``parameters`` is forwarded as ``<Parameter name="..." value="..."/>``
|
|
9678
|
-
* children of ``<Stream>``. Twilio Media Streams strips query-string params
|
|
9679
|
-
* from the ``<Stream url=...>`` before the WS handshake, so
|
|
9680
|
-
* ``<Parameter>`` tags are the supported way to pre-populate
|
|
9681
|
-
* ``start.customParameters`` on the WS ``start`` frame. Used by the
|
|
9682
|
-
* inbound path to carry caller / callee through to the bridge.
|
|
9683
|
-
*
|
|
9684
|
-
* Mirrors the Python adapter's ``generate_stream_twiml``.
|
|
9685
|
-
*/
|
|
9686
|
-
static generateStreamTwiml(streamUrl, parameters) {
|
|
9687
|
-
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
9688
|
-
const escapedUrl = esc(streamUrl);
|
|
9689
|
-
let paramTags = "";
|
|
9690
|
-
if (parameters) {
|
|
9691
|
-
for (const [name, value] of Object.entries(parameters)) {
|
|
9692
|
-
if (value == null) continue;
|
|
9693
|
-
paramTags += `<Parameter name="${esc(name)}" value="${esc(String(value))}"/>`;
|
|
9694
|
-
}
|
|
10417
|
+
const out = Buffer.allocUnsafe(pcm.length);
|
|
10418
|
+
for (let i = 0; i < n; i++) {
|
|
10419
|
+
out.writeInt16LE(clipInt16(Math.round(pcm.readInt16LE(i * 2) * gain)), i * 2);
|
|
9695
10420
|
}
|
|
9696
|
-
return
|
|
10421
|
+
return out;
|
|
9697
10422
|
}
|
|
9698
|
-
|
|
9699
|
-
|
|
9700
|
-
if (
|
|
9701
|
-
const
|
|
9702
|
-
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
|
|
9706
|
-
body
|
|
9707
|
-
);
|
|
9708
|
-
} catch (err) {
|
|
9709
|
-
getLogger().warn(`[TwilioAdapter] endCall failed for ${callSid}: ${String(err)}`);
|
|
9710
|
-
throw err;
|
|
9711
|
-
}
|
|
10423
|
+
resampleTo(dstSr) {
|
|
10424
|
+
if (this.pcm === null) return Buffer.alloc(0);
|
|
10425
|
+
if (dstSr === this.sourceSr) return this.pcm;
|
|
10426
|
+
const cached = this.resampleCache.get(dstSr);
|
|
10427
|
+
if (cached) return cached;
|
|
10428
|
+
const resampled = resamplePcm(this.pcm, this.sourceSr, dstSr);
|
|
10429
|
+
this.resampleCache.set(dstSr, resampled);
|
|
10430
|
+
return resampled;
|
|
9712
10431
|
}
|
|
9713
10432
|
};
|
|
10433
|
+
function isAudioConfig(value) {
|
|
10434
|
+
return typeof value === "object" && value !== null && "source" in value && typeof value.source === "object";
|
|
10435
|
+
}
|
|
9714
10436
|
|
|
9715
10437
|
// src/providers/telnyx-adapter.ts
|
|
9716
10438
|
init_esm_shims();
|
|
@@ -9725,8 +10447,8 @@ var TelnyxAdapter = class {
|
|
|
9725
10447
|
this.apiKey = apiKey;
|
|
9726
10448
|
this.connectionId = connectionId;
|
|
9727
10449
|
}
|
|
9728
|
-
async request(method,
|
|
9729
|
-
const url = `${this.baseUrl}${
|
|
10450
|
+
async request(method, path4, body) {
|
|
10451
|
+
const url = `${this.baseUrl}${path4}`;
|
|
9730
10452
|
const headers = {
|
|
9731
10453
|
Authorization: `Bearer ${this.apiKey}`
|
|
9732
10454
|
};
|
|
@@ -9739,7 +10461,7 @@ var TelnyxAdapter = class {
|
|
|
9739
10461
|
});
|
|
9740
10462
|
const text = await response.text();
|
|
9741
10463
|
if (!response.ok) {
|
|
9742
|
-
throw new Error(`Telnyx ${method} ${
|
|
10464
|
+
throw new Error(`Telnyx ${method} ${path4} failed: ${response.status} ${text}`);
|
|
9743
10465
|
}
|
|
9744
10466
|
if (!text) return {};
|
|
9745
10467
|
try {
|
|
@@ -9780,10 +10502,15 @@ var TelnyxAdapter = class {
|
|
|
9780
10502
|
if (!phoneNumber) throw new Error("TelnyxAdapter: phoneNumber is required");
|
|
9781
10503
|
if (!opts.connectionId) throw new Error("TelnyxAdapter: connectionId is required");
|
|
9782
10504
|
try {
|
|
10505
|
+
await this.request(
|
|
10506
|
+
"PATCH",
|
|
10507
|
+
`/phone_numbers/${encodeURIComponent(phoneNumber)}`,
|
|
10508
|
+
{ connection_id: opts.connectionId }
|
|
10509
|
+
);
|
|
9783
10510
|
await this.request(
|
|
9784
10511
|
"PATCH",
|
|
9785
10512
|
`/phone_numbers/${encodeURIComponent(phoneNumber)}/voice`,
|
|
9786
|
-
{
|
|
10513
|
+
{ tech_prefix_enabled: false }
|
|
9787
10514
|
);
|
|
9788
10515
|
} catch (err) {
|
|
9789
10516
|
const status = err instanceof Error ? err.message.replace(/\+\d{7,15}/g, "[REDACTED]") : String(err);
|
|
@@ -9883,6 +10610,7 @@ var TelnyxSTT = class {
|
|
|
9883
10610
|
this.transcriptionEngine = transcriptionEngine;
|
|
9884
10611
|
this.sampleRate = sampleRate;
|
|
9885
10612
|
this.baseUrl = baseUrl;
|
|
10613
|
+
this.patterCtorArgs = [apiKey, language, transcriptionEngine, sampleRate, baseUrl];
|
|
9886
10614
|
}
|
|
9887
10615
|
apiKey;
|
|
9888
10616
|
language;
|
|
@@ -9894,6 +10622,17 @@ var TelnyxSTT = class {
|
|
|
9894
10622
|
ws = null;
|
|
9895
10623
|
callbacks = /* @__PURE__ */ new Set();
|
|
9896
10624
|
headerSent = false;
|
|
10625
|
+
/** Construction args replayed by clone(). */
|
|
10626
|
+
patterCtorArgs;
|
|
10627
|
+
/**
|
|
10628
|
+
* Fresh adapter built with this instance's construction arguments —
|
|
10629
|
+
* called per call by the stream handler so concurrent calls never share
|
|
10630
|
+
* connection state (sockets/queues; cross-call transcript bleed).
|
|
10631
|
+
*/
|
|
10632
|
+
clone() {
|
|
10633
|
+
const ctor = this.constructor;
|
|
10634
|
+
return new ctor(...this.patterCtorArgs);
|
|
10635
|
+
}
|
|
9897
10636
|
/** Open the streaming WebSocket and arm message handlers. */
|
|
9898
10637
|
async connect() {
|
|
9899
10638
|
const params = new URLSearchParams({
|
|
@@ -9905,11 +10644,11 @@ var TelnyxSTT = class {
|
|
|
9905
10644
|
this.ws = new WebSocket7(url, {
|
|
9906
10645
|
headers: { Authorization: `Bearer ${this.apiKey}` }
|
|
9907
10646
|
});
|
|
9908
|
-
await new Promise((
|
|
10647
|
+
await new Promise((resolve2, reject) => {
|
|
9909
10648
|
const timer = setTimeout(() => reject(new Error("Telnyx STT connect timeout")), 1e4);
|
|
9910
10649
|
this.ws.once("open", () => {
|
|
9911
10650
|
clearTimeout(timer);
|
|
9912
|
-
|
|
10651
|
+
resolve2();
|
|
9913
10652
|
});
|
|
9914
10653
|
this.ws.once("error", (err) => {
|
|
9915
10654
|
clearTimeout(timer);
|
|
@@ -9931,7 +10670,13 @@ var TelnyxSTT = class {
|
|
|
9931
10670
|
confidence: data.confidence ?? 0
|
|
9932
10671
|
};
|
|
9933
10672
|
for (const cb of this.callbacks) {
|
|
9934
|
-
|
|
10673
|
+
try {
|
|
10674
|
+
Promise.resolve(cb(transcript)).catch(
|
|
10675
|
+
(err) => getLogger().error(`STT transcript callback failed: ${String(err)}`)
|
|
10676
|
+
);
|
|
10677
|
+
} catch (err) {
|
|
10678
|
+
getLogger().error(`STT transcript callback threw: ${String(err)}`);
|
|
10679
|
+
}
|
|
9935
10680
|
}
|
|
9936
10681
|
});
|
|
9937
10682
|
this.ws.on("error", (err) => {
|
|
@@ -10028,11 +10773,11 @@ var TelnyxTTS = class {
|
|
|
10028
10773
|
ws = new WebSocket8(url, {
|
|
10029
10774
|
headers: { Authorization: `Bearer ${this.apiKey}` }
|
|
10030
10775
|
});
|
|
10031
|
-
await new Promise((
|
|
10776
|
+
await new Promise((resolve2, reject) => {
|
|
10032
10777
|
const timer = setTimeout(() => reject(new Error("Telnyx TTS connect timeout")), 1e4);
|
|
10033
10778
|
ws.once("open", () => {
|
|
10034
10779
|
clearTimeout(timer);
|
|
10035
|
-
|
|
10780
|
+
resolve2();
|
|
10036
10781
|
});
|
|
10037
10782
|
ws.once("error", (err) => {
|
|
10038
10783
|
clearTimeout(timer);
|
|
@@ -10071,7 +10816,7 @@ var TelnyxTTS = class {
|
|
|
10071
10816
|
while (true) {
|
|
10072
10817
|
let frameTimer;
|
|
10073
10818
|
const item = queue.length > 0 ? queue.shift() : await Promise.race([
|
|
10074
|
-
new Promise((
|
|
10819
|
+
new Promise((resolve2) => waiters.push(resolve2)),
|
|
10075
10820
|
new Promise((_, reject) => {
|
|
10076
10821
|
frameTimer = setTimeout(
|
|
10077
10822
|
() => reject(new Error("Telnyx TTS frame timeout")),
|
|
@@ -10095,15 +10840,682 @@ var TelnyxTTS = class {
|
|
|
10095
10840
|
}
|
|
10096
10841
|
};
|
|
10097
10842
|
|
|
10843
|
+
// src/evals/index.ts
|
|
10844
|
+
init_esm_shims();
|
|
10845
|
+
|
|
10846
|
+
// src/evals/case.ts
|
|
10847
|
+
init_esm_shims();
|
|
10848
|
+
function evalResultToDict(result) {
|
|
10849
|
+
return {
|
|
10850
|
+
case: result.caseName,
|
|
10851
|
+
score: result.judge.score,
|
|
10852
|
+
passed: result.judge.passed,
|
|
10853
|
+
reasoning: result.judge.reasoning,
|
|
10854
|
+
transcript: result.transcript.map((t) => ({ role: t.role, text: t.text })),
|
|
10855
|
+
duration_s: Math.round(result.durationS * 1e3) / 1e3,
|
|
10856
|
+
error: result.error
|
|
10857
|
+
};
|
|
10858
|
+
}
|
|
10859
|
+
|
|
10860
|
+
// src/evals/llm-judge.ts
|
|
10861
|
+
init_esm_shims();
|
|
10862
|
+
var JUDGE_SYSTEM = 'You are a strict but fair evaluator of voice-AI agents. You will be given: (1) the expected behavior for the agent, (2) a rubric, (3) a transcript of the conversation. Return a JSON object with exactly three keys:\n - "score": float between 0.0 and 1.0\n - "passed": boolean (true when score >= threshold)\n - "reasoning": short string explaining the score\nDo not return any text outside the JSON object.';
|
|
10863
|
+
var LLMJudge = class {
|
|
10864
|
+
model;
|
|
10865
|
+
passThreshold;
|
|
10866
|
+
apiKey;
|
|
10867
|
+
backend;
|
|
10868
|
+
constructor(options = {}) {
|
|
10869
|
+
this.model = options.model ?? "gpt-4o-mini";
|
|
10870
|
+
this.apiKey = options.apiKey;
|
|
10871
|
+
this.passThreshold = options.passThreshold ?? 0.7;
|
|
10872
|
+
this.backend = options.backend;
|
|
10873
|
+
}
|
|
10874
|
+
/** Return a {@link JudgeResult} for the given transcript. */
|
|
10875
|
+
async judgeCase(evalCase, transcript) {
|
|
10876
|
+
const prompt = this.buildPrompt(evalCase, transcript);
|
|
10877
|
+
const raw = this.backend ? await this.backend.judge(prompt) : await this.callOpenAI(prompt);
|
|
10878
|
+
return this.parse(raw);
|
|
10879
|
+
}
|
|
10880
|
+
buildPrompt(evalCase, transcript) {
|
|
10881
|
+
const lines = [
|
|
10882
|
+
`EXPECTED BEHAVIOR: ${evalCase.expectedBehavior}`,
|
|
10883
|
+
`RUBRIC: ${evalCase.rubric}`,
|
|
10884
|
+
`PASS THRESHOLD: ${this.passThreshold}`,
|
|
10885
|
+
"TRANSCRIPT:"
|
|
10886
|
+
];
|
|
10887
|
+
for (const turn of transcript) {
|
|
10888
|
+
lines.push(` ${turn.role || "?"}: ${turn.text ?? ""}`);
|
|
10889
|
+
}
|
|
10890
|
+
return lines.join("\n");
|
|
10891
|
+
}
|
|
10892
|
+
/** Call OpenAI chat completions directly over fetch (no SDK dependency). */
|
|
10893
|
+
async callOpenAI(prompt) {
|
|
10894
|
+
const apiKey = this.apiKey || process.env.OPENAI_API_KEY;
|
|
10895
|
+
if (!apiKey) {
|
|
10896
|
+
throw new Error(
|
|
10897
|
+
"LLMJudge requires an OpenAI API key. Set OPENAI_API_KEY or pass apiKey to the LLMJudge constructor."
|
|
10898
|
+
);
|
|
10899
|
+
}
|
|
10900
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
10901
|
+
method: "POST",
|
|
10902
|
+
headers: {
|
|
10903
|
+
"Content-Type": "application/json",
|
|
10904
|
+
Authorization: `Bearer ${apiKey}`
|
|
10905
|
+
},
|
|
10906
|
+
body: JSON.stringify({
|
|
10907
|
+
model: this.model,
|
|
10908
|
+
messages: [
|
|
10909
|
+
{ role: "system", content: JUDGE_SYSTEM },
|
|
10910
|
+
{ role: "user", content: prompt }
|
|
10911
|
+
],
|
|
10912
|
+
response_format: { type: "json_object" },
|
|
10913
|
+
temperature: 0
|
|
10914
|
+
})
|
|
10915
|
+
});
|
|
10916
|
+
if (!response.ok) {
|
|
10917
|
+
const errText = await response.text();
|
|
10918
|
+
throw new Error(`LLMJudge OpenAI call failed: ${response.status} ${errText.slice(0, 200)}`);
|
|
10919
|
+
}
|
|
10920
|
+
const data = await response.json();
|
|
10921
|
+
const content = data.choices?.[0]?.message?.content;
|
|
10922
|
+
if (!content) {
|
|
10923
|
+
throw new Error(
|
|
10924
|
+
`LLMJudge response had no choices/content: ${JSON.stringify(data).slice(0, 200)}`
|
|
10925
|
+
);
|
|
10926
|
+
}
|
|
10927
|
+
return content;
|
|
10928
|
+
}
|
|
10929
|
+
/** Parse the judge's JSON — tolerant of extra whitespace / code fences. */
|
|
10930
|
+
parse(raw) {
|
|
10931
|
+
let text = raw.trim();
|
|
10932
|
+
if (text.startsWith("```")) {
|
|
10933
|
+
text = text.replace(/^```(?:json)?\s*/, "").replace(/\s*```$/, "");
|
|
10934
|
+
}
|
|
10935
|
+
let data;
|
|
10936
|
+
try {
|
|
10937
|
+
data = JSON.parse(text);
|
|
10938
|
+
} catch {
|
|
10939
|
+
getLogger().warn(`LLMJudge: invalid JSON, defaulting to fail: ${JSON.stringify(raw)}`);
|
|
10940
|
+
return {
|
|
10941
|
+
score: 0,
|
|
10942
|
+
passed: false,
|
|
10943
|
+
reasoning: `Judge returned invalid JSON: ${raw.slice(0, 200)}`
|
|
10944
|
+
};
|
|
10945
|
+
}
|
|
10946
|
+
const scoreRaw = Number(data.score ?? 0);
|
|
10947
|
+
let score = Number.isFinite(scoreRaw) ? scoreRaw : 0;
|
|
10948
|
+
score = Math.max(0, Math.min(1, score));
|
|
10949
|
+
const passed = score >= this.passThreshold;
|
|
10950
|
+
const reasoning = String(data.reasoning ?? "");
|
|
10951
|
+
return { score, passed, reasoning };
|
|
10952
|
+
}
|
|
10953
|
+
};
|
|
10954
|
+
|
|
10955
|
+
// src/evals/runner.ts
|
|
10956
|
+
init_esm_shims();
|
|
10957
|
+
import { readFile } from "fs/promises";
|
|
10958
|
+
import { extname, basename } from "path";
|
|
10959
|
+
var EvalRunner = class {
|
|
10960
|
+
judge;
|
|
10961
|
+
constructor(options = {}) {
|
|
10962
|
+
this.judge = options.judge ?? new LLMJudge();
|
|
10963
|
+
}
|
|
10964
|
+
/**
|
|
10965
|
+
* Run every case in ``suite`` sequentially.
|
|
10966
|
+
*
|
|
10967
|
+
* ``agentFactory`` is required only for cases that do NOT carry their own
|
|
10968
|
+
* ``agent`` (the legacy ``reply()`` path).
|
|
10969
|
+
*/
|
|
10970
|
+
async run(suite, agentFactory) {
|
|
10971
|
+
const results = [];
|
|
10972
|
+
for (const evalCase of suite.cases) {
|
|
10973
|
+
results.push(await this.runCase(evalCase, agentFactory));
|
|
10974
|
+
}
|
|
10975
|
+
return results;
|
|
10976
|
+
}
|
|
10977
|
+
/**
|
|
10978
|
+
* Run a single case and return its {@link EvalResult}.
|
|
10979
|
+
*
|
|
10980
|
+
* Routes through the real-pipeline {@link EvalSession} when
|
|
10981
|
+
* ``evalCase.agent`` is set; otherwise uses the legacy ``reply()``-callable
|
|
10982
|
+
* ``agentFactory`` (unchanged behaviour).
|
|
10983
|
+
*/
|
|
10984
|
+
async runCase(evalCase, agentFactory) {
|
|
10985
|
+
const start = Date.now();
|
|
10986
|
+
const transcript = [];
|
|
10987
|
+
let error = null;
|
|
10988
|
+
try {
|
|
10989
|
+
if (evalCase.agent !== void 0) {
|
|
10990
|
+
await this.runTurnsWithSession(evalCase, transcript);
|
|
10991
|
+
} else {
|
|
10992
|
+
if (agentFactory === void 0) {
|
|
10993
|
+
throw new Error(
|
|
10994
|
+
`case ${JSON.stringify(evalCase.name)} has no agent and no agentFactory was supplied`
|
|
10995
|
+
);
|
|
10996
|
+
}
|
|
10997
|
+
await this.runTurnsWithReply(evalCase, agentFactory, transcript);
|
|
10998
|
+
}
|
|
10999
|
+
} catch (exc) {
|
|
11000
|
+
error = formatError(exc);
|
|
11001
|
+
getLogger().error(`eval case=${JSON.stringify(evalCase.name)} raised: ${error}`);
|
|
11002
|
+
}
|
|
11003
|
+
if (error !== null && transcript.length === 0) {
|
|
11004
|
+
return {
|
|
11005
|
+
caseName: evalCase.name,
|
|
11006
|
+
transcript,
|
|
11007
|
+
judge: { score: 0, passed: false, reasoning: error },
|
|
11008
|
+
durationS: (Date.now() - start) / 1e3,
|
|
11009
|
+
error
|
|
11010
|
+
};
|
|
11011
|
+
}
|
|
11012
|
+
let judgeResult;
|
|
11013
|
+
try {
|
|
11014
|
+
judgeResult = await this.judge.judgeCase(evalCase, transcript);
|
|
11015
|
+
} catch (exc) {
|
|
11016
|
+
const judgeError = `judge error: ${exc instanceof Error ? exc.message : String(exc)}`;
|
|
11017
|
+
return {
|
|
11018
|
+
caseName: evalCase.name,
|
|
11019
|
+
transcript,
|
|
11020
|
+
judge: { score: 0, passed: false, reasoning: judgeError },
|
|
11021
|
+
durationS: (Date.now() - start) / 1e3,
|
|
11022
|
+
error: judgeError
|
|
11023
|
+
};
|
|
11024
|
+
}
|
|
11025
|
+
return {
|
|
11026
|
+
caseName: evalCase.name,
|
|
11027
|
+
transcript,
|
|
11028
|
+
judge: judgeResult,
|
|
11029
|
+
durationS: (Date.now() - start) / 1e3,
|
|
11030
|
+
error
|
|
11031
|
+
};
|
|
11032
|
+
}
|
|
11033
|
+
/**
|
|
11034
|
+
* Legacy path — drives the case against a ``reply()`` callable.
|
|
11035
|
+
*
|
|
11036
|
+
* Appends into ``transcript`` in place so a mid-case exception still
|
|
11037
|
+
* leaves the partial transcript for the judge (existing semantics).
|
|
11038
|
+
*/
|
|
11039
|
+
async runTurnsWithReply(evalCase, agentFactory, transcript) {
|
|
11040
|
+
const agent = await agentFactory();
|
|
11041
|
+
if (evalCase.firstMessage) {
|
|
11042
|
+
transcript.push({ role: "agent", text: evalCase.firstMessage });
|
|
11043
|
+
}
|
|
11044
|
+
for (const turn of evalCase.turns) {
|
|
11045
|
+
transcript.push({ role: "user", text: turn.user });
|
|
11046
|
+
const reply = typeof agent === "function" ? await agent(turn.user) : "";
|
|
11047
|
+
transcript.push({ role: "agent", text: reply || "" });
|
|
11048
|
+
logMissingExpected(evalCase, turn, reply || "");
|
|
11049
|
+
}
|
|
11050
|
+
}
|
|
11051
|
+
/**
|
|
11052
|
+
* Real-pipeline path — drives the case through {@link EvalSession}.
|
|
11053
|
+
*
|
|
11054
|
+
* The agent's REAL handler emits its own ``firstMessage`` (a
|
|
11055
|
+
* ``evalCase.firstMessage`` overrides the agent's), tools/hooks/guardrails
|
|
11056
|
+
* run for real, and the transcript mirrors what the pipeline actually
|
|
11057
|
+
* said. Appends into ``transcript`` in place (partial-on-error, same as
|
|
11058
|
+
* the legacy path).
|
|
11059
|
+
*/
|
|
11060
|
+
async runTurnsWithSession(evalCase, transcript) {
|
|
11061
|
+
const { EvalSession: EvalSession2 } = await import("./session-N3CBCYYN.mjs");
|
|
11062
|
+
if (!evalCase.agent) {
|
|
11063
|
+
throw new Error(`case ${JSON.stringify(evalCase.name)} has no agent \u2014 use the reply-factory path`);
|
|
11064
|
+
}
|
|
11065
|
+
let agent = evalCase.agent;
|
|
11066
|
+
if (evalCase.firstMessage) {
|
|
11067
|
+
agent = { ...agent, firstMessage: evalCase.firstMessage };
|
|
11068
|
+
}
|
|
11069
|
+
const session = await EvalSession2.create({
|
|
11070
|
+
agent,
|
|
11071
|
+
llmProvider: evalCase.llmProvider
|
|
11072
|
+
});
|
|
11073
|
+
try {
|
|
11074
|
+
if (agent.firstMessage) {
|
|
11075
|
+
transcript.push({ role: "agent", text: agent.firstMessage });
|
|
11076
|
+
}
|
|
11077
|
+
for (const turn of evalCase.turns) {
|
|
11078
|
+
transcript.push({ role: "user", text: turn.user });
|
|
11079
|
+
const result = await session.userSays(turn.user);
|
|
11080
|
+
transcript.push({ role: "agent", text: result.agentText });
|
|
11081
|
+
logMissingExpected(evalCase, turn, result.agentText);
|
|
11082
|
+
}
|
|
11083
|
+
} finally {
|
|
11084
|
+
await session.close();
|
|
11085
|
+
}
|
|
11086
|
+
}
|
|
11087
|
+
/** Render a JSON report suitable for CI artefacts. */
|
|
11088
|
+
report(suite, results) {
|
|
11089
|
+
const total = results.length;
|
|
11090
|
+
const passed = results.filter((r) => r.judge.passed).length;
|
|
11091
|
+
const payload = {
|
|
11092
|
+
suite: suite.name,
|
|
11093
|
+
total,
|
|
11094
|
+
passed,
|
|
11095
|
+
failed: total - passed,
|
|
11096
|
+
pass_rate: total > 0 ? passed / total : 0,
|
|
11097
|
+
cases: results.map((r) => evalResultToDict(r))
|
|
11098
|
+
};
|
|
11099
|
+
return JSON.stringify(payload, null, 2);
|
|
11100
|
+
}
|
|
11101
|
+
};
|
|
11102
|
+
function formatError(exc) {
|
|
11103
|
+
if (exc instanceof Error) {
|
|
11104
|
+
return `${exc.name}: ${exc.message}`;
|
|
11105
|
+
}
|
|
11106
|
+
return String(exc);
|
|
11107
|
+
}
|
|
11108
|
+
function logMissingExpected(evalCase, turn, reply) {
|
|
11109
|
+
for (const needle of turn.expectedContains ?? []) {
|
|
11110
|
+
if (!reply.toLowerCase().includes(needle.toLowerCase())) {
|
|
11111
|
+
getLogger().info(
|
|
11112
|
+
`case=${JSON.stringify(evalCase.name)} expectedContains=${JSON.stringify(needle)} missing in reply`
|
|
11113
|
+
);
|
|
11114
|
+
}
|
|
11115
|
+
}
|
|
11116
|
+
}
|
|
11117
|
+
async function loadSuite(path4) {
|
|
11118
|
+
const text = await readFile(path4, "utf-8");
|
|
11119
|
+
const ext = extname(path4).toLowerCase();
|
|
11120
|
+
let data;
|
|
11121
|
+
if (ext === ".yaml" || ext === ".yml") {
|
|
11122
|
+
let yaml;
|
|
11123
|
+
try {
|
|
11124
|
+
const moduleName = "yaml";
|
|
11125
|
+
yaml = await import(moduleName);
|
|
11126
|
+
} catch {
|
|
11127
|
+
throw new Error(
|
|
11128
|
+
"Loading YAML suites requires the optional 'yaml' package. Install with: npm install yaml \u2014 or use a JSON suite file."
|
|
11129
|
+
);
|
|
11130
|
+
}
|
|
11131
|
+
data = yaml.parse(text);
|
|
11132
|
+
} else {
|
|
11133
|
+
data = JSON.parse(text);
|
|
11134
|
+
}
|
|
11135
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
11136
|
+
throw new Error(`Eval suite ${path4} must be a mapping, got ${typeOf(data)}`);
|
|
11137
|
+
}
|
|
11138
|
+
const record = data;
|
|
11139
|
+
const casesRaw = record.cases ?? [];
|
|
11140
|
+
if (!Array.isArray(casesRaw)) {
|
|
11141
|
+
throw new Error(`Eval suite ${path4}: 'cases' must be a list`);
|
|
11142
|
+
}
|
|
11143
|
+
const cases = casesRaw.map((c, i) => {
|
|
11144
|
+
if (c === null || typeof c !== "object" || Array.isArray(c)) {
|
|
11145
|
+
throw new Error(`Eval suite ${path4}: case ${i} must be a mapping`);
|
|
11146
|
+
}
|
|
11147
|
+
const caseRecord = c;
|
|
11148
|
+
const turnsRaw = caseRecord.turns ?? [];
|
|
11149
|
+
const turns = (Array.isArray(turnsRaw) ? turnsRaw : []).filter((t) => t !== null && typeof t === "object").map((t) => ({
|
|
11150
|
+
user: String(t.user ?? ""),
|
|
11151
|
+
expectedContains: toStringArray(t.expected_contains ?? t.expectedContains)
|
|
11152
|
+
}));
|
|
11153
|
+
return {
|
|
11154
|
+
name: String(caseRecord.name ?? `case_${i}`),
|
|
11155
|
+
turns,
|
|
11156
|
+
expectedBehavior: String(caseRecord.expected_behavior ?? caseRecord.expectedBehavior ?? ""),
|
|
11157
|
+
rubric: String(caseRecord.rubric ?? ""),
|
|
11158
|
+
tags: toStringArray(caseRecord.tags),
|
|
11159
|
+
firstMessage: String(caseRecord.first_message ?? caseRecord.firstMessage ?? "")
|
|
11160
|
+
};
|
|
11161
|
+
});
|
|
11162
|
+
return {
|
|
11163
|
+
name: String(record.name ?? basename(path4, extname(path4))),
|
|
11164
|
+
cases,
|
|
11165
|
+
metadata: record.metadata ?? {}
|
|
11166
|
+
};
|
|
11167
|
+
}
|
|
11168
|
+
function toStringArray(value) {
|
|
11169
|
+
if (!Array.isArray(value)) return [];
|
|
11170
|
+
return value.map((v) => String(v));
|
|
11171
|
+
}
|
|
11172
|
+
function typeOf(value) {
|
|
11173
|
+
if (value === null) return "null";
|
|
11174
|
+
if (Array.isArray(value)) return "array";
|
|
11175
|
+
return typeof value;
|
|
11176
|
+
}
|
|
11177
|
+
|
|
11178
|
+
// src/evals/scripted-llm.ts
|
|
11179
|
+
init_esm_shims();
|
|
11180
|
+
function textTurn(text, options = {}) {
|
|
11181
|
+
return [
|
|
11182
|
+
{ type: "text", content: text },
|
|
11183
|
+
{
|
|
11184
|
+
type: "usage",
|
|
11185
|
+
inputTokens: options.inputTokens ?? 8,
|
|
11186
|
+
outputTokens: options.outputTokens ?? 8
|
|
11187
|
+
}
|
|
11188
|
+
];
|
|
11189
|
+
}
|
|
11190
|
+
function toolCallTurn(name, args, options = {}) {
|
|
11191
|
+
return [
|
|
11192
|
+
{
|
|
11193
|
+
type: "tool_call",
|
|
11194
|
+
index: 0,
|
|
11195
|
+
id: options.callId ?? "call_1",
|
|
11196
|
+
name,
|
|
11197
|
+
arguments: JSON.stringify(args ?? {})
|
|
11198
|
+
},
|
|
11199
|
+
{ type: "usage", inputTokens: 8, outputTokens: 4 }
|
|
11200
|
+
];
|
|
11201
|
+
}
|
|
11202
|
+
var ScriptedLLMProvider = class {
|
|
11203
|
+
/** Stable pricing/dashboard key (no real pricing entry — cost is 0). */
|
|
11204
|
+
static providerKey = "scripted";
|
|
11205
|
+
calls = [];
|
|
11206
|
+
scripts;
|
|
11207
|
+
constructor(turns) {
|
|
11208
|
+
this.scripts = (turns ?? []).map((chunks) => chunks.map((c) => ({ ...c })));
|
|
11209
|
+
}
|
|
11210
|
+
/** Append another scripted turn (chunk list) to the script queue. */
|
|
11211
|
+
addTurn(chunks) {
|
|
11212
|
+
this.scripts.push(chunks.map((c) => ({ ...c })));
|
|
11213
|
+
}
|
|
11214
|
+
async *stream(messages, tools, opts) {
|
|
11215
|
+
this.calls.push({
|
|
11216
|
+
messages: messages.map((m) => ({ ...m })),
|
|
11217
|
+
tools: tools ? tools.map((t) => ({ ...t })) : null,
|
|
11218
|
+
callId: opts?.callId ?? null
|
|
11219
|
+
});
|
|
11220
|
+
const script = this.scripts.shift();
|
|
11221
|
+
if (script === void 0) {
|
|
11222
|
+
yield { type: "done" };
|
|
11223
|
+
return;
|
|
11224
|
+
}
|
|
11225
|
+
for (const chunk of script) {
|
|
11226
|
+
if (opts?.signal?.aborted) return;
|
|
11227
|
+
yield { ...chunk };
|
|
11228
|
+
}
|
|
11229
|
+
}
|
|
11230
|
+
};
|
|
11231
|
+
|
|
11232
|
+
// src/evals/assertions.ts
|
|
11233
|
+
init_esm_shims();
|
|
11234
|
+
import { AssertionError } from "assert";
|
|
11235
|
+
function expect(result) {
|
|
11236
|
+
return new TurnExpectation(result);
|
|
11237
|
+
}
|
|
11238
|
+
function deepEqual(a, b) {
|
|
11239
|
+
if (a === b) return true;
|
|
11240
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
11241
|
+
return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
|
|
11242
|
+
}
|
|
11243
|
+
if (a !== null && b !== null && typeof a === "object" && typeof b === "object" && !Array.isArray(a) && !Array.isArray(b)) {
|
|
11244
|
+
const ak = Object.keys(a);
|
|
11245
|
+
const bk = Object.keys(b);
|
|
11246
|
+
return ak.length === bk.length && ak.every(
|
|
11247
|
+
(k) => deepEqual(a[k], b[k])
|
|
11248
|
+
);
|
|
11249
|
+
}
|
|
11250
|
+
return false;
|
|
11251
|
+
}
|
|
11252
|
+
function isSubset(subset, actual) {
|
|
11253
|
+
if (subset !== null && typeof subset === "object" && !Array.isArray(subset)) {
|
|
11254
|
+
if (actual === null || typeof actual !== "object" || Array.isArray(actual)) {
|
|
11255
|
+
return false;
|
|
11256
|
+
}
|
|
11257
|
+
const actualRecord = actual;
|
|
11258
|
+
return Object.entries(subset).every(
|
|
11259
|
+
([key, value]) => key in actualRecord && isSubset(value, actualRecord[key])
|
|
11260
|
+
);
|
|
11261
|
+
}
|
|
11262
|
+
return deepEqual(subset, actual);
|
|
11263
|
+
}
|
|
11264
|
+
var TurnExpectation = class {
|
|
11265
|
+
turnResult;
|
|
11266
|
+
constructor(result) {
|
|
11267
|
+
this.turnResult = result;
|
|
11268
|
+
}
|
|
11269
|
+
/** The wrapped {@link TurnResult} (escape hatch for ad-hoc asserts). */
|
|
11270
|
+
get result() {
|
|
11271
|
+
return this.turnResult;
|
|
11272
|
+
}
|
|
11273
|
+
// -- tools -----------------------------------------------------------------
|
|
11274
|
+
/**
|
|
11275
|
+
* Assert that tool ``name`` ran this turn.
|
|
11276
|
+
*
|
|
11277
|
+
* ``argsSubset`` (optional) must be recursively contained in the args of
|
|
11278
|
+
* at least one matching invocation — extra argument keys are allowed,
|
|
11279
|
+
* listed keys must match exactly.
|
|
11280
|
+
*/
|
|
11281
|
+
toolCalled(name, argsSubset) {
|
|
11282
|
+
const matches = this.turnResult.toolCalls.filter((tc) => tc.name === name);
|
|
11283
|
+
if (matches.length === 0) {
|
|
11284
|
+
const called = this.turnResult.toolCalls.map((tc) => tc.name);
|
|
11285
|
+
throw new AssertionError({
|
|
11286
|
+
message: `expected tool ${JSON.stringify(name)} to be called this turn; tools called: ${called.length > 0 ? JSON.stringify(called) : "none"}`
|
|
11287
|
+
});
|
|
11288
|
+
}
|
|
11289
|
+
if (argsSubset !== void 0 && !matches.some((tc) => isSubset(argsSubset, tc.arguments))) {
|
|
11290
|
+
throw new AssertionError({
|
|
11291
|
+
message: `tool ${JSON.stringify(name)} was called, but no invocation matched argsSubset=${JSON.stringify(argsSubset)}; observed args: ` + JSON.stringify(matches.map((tc) => tc.arguments))
|
|
11292
|
+
});
|
|
11293
|
+
}
|
|
11294
|
+
return this;
|
|
11295
|
+
}
|
|
11296
|
+
/** Assert that no tool ran this turn (or that ``name`` did not). */
|
|
11297
|
+
noToolCalled(name) {
|
|
11298
|
+
if (name === void 0) {
|
|
11299
|
+
if (this.turnResult.toolCalls.length > 0) {
|
|
11300
|
+
throw new AssertionError({
|
|
11301
|
+
message: "expected no tool calls this turn; tools called: " + JSON.stringify(this.turnResult.toolCalls.map((tc) => tc.name))
|
|
11302
|
+
});
|
|
11303
|
+
}
|
|
11304
|
+
return this;
|
|
11305
|
+
}
|
|
11306
|
+
const offenders = this.turnResult.toolCalls.filter((tc) => tc.name === name);
|
|
11307
|
+
if (offenders.length > 0) {
|
|
11308
|
+
throw new AssertionError({
|
|
11309
|
+
message: `expected tool ${JSON.stringify(name)} NOT to be called this turn; it ran ${offenders.length} time(s) with args ` + JSON.stringify(offenders.map((tc) => tc.arguments))
|
|
11310
|
+
});
|
|
11311
|
+
}
|
|
11312
|
+
return this;
|
|
11313
|
+
}
|
|
11314
|
+
agentTextContains(first, ...rest) {
|
|
11315
|
+
let needles;
|
|
11316
|
+
let caseSensitive = false;
|
|
11317
|
+
if (Array.isArray(first)) {
|
|
11318
|
+
needles = [...first];
|
|
11319
|
+
const options = rest[0];
|
|
11320
|
+
caseSensitive = options?.caseSensitive ?? false;
|
|
11321
|
+
} else {
|
|
11322
|
+
needles = [first, ...rest].filter(
|
|
11323
|
+
(n) => typeof n === "string"
|
|
11324
|
+
);
|
|
11325
|
+
}
|
|
11326
|
+
const haystack = this.turnResult.agentText;
|
|
11327
|
+
const cmpHaystack = caseSensitive ? haystack : haystack.toLowerCase();
|
|
11328
|
+
const missing = needles.filter(
|
|
11329
|
+
(n) => !cmpHaystack.includes(caseSensitive ? n : n.toLowerCase())
|
|
11330
|
+
);
|
|
11331
|
+
if (missing.length > 0) {
|
|
11332
|
+
throw new AssertionError({
|
|
11333
|
+
message: `agent text is missing ${JSON.stringify(missing)}; agent said: ` + JSON.stringify(haystack)
|
|
11334
|
+
});
|
|
11335
|
+
}
|
|
11336
|
+
return this;
|
|
11337
|
+
}
|
|
11338
|
+
// -- semantic judge ----------------------------------------------------------
|
|
11339
|
+
/**
|
|
11340
|
+
* Score this turn against ``intent`` with the LLM judge.
|
|
11341
|
+
*
|
|
11342
|
+
* Builds a synthetic {@link EvalCase} whose ``expectedBehavior`` is
|
|
11343
|
+
* ``intent`` and judges the turn's full history snapshot. Throws
|
|
11344
|
+
* ``AssertionError`` when the judge fails the turn; returns the
|
|
11345
|
+
* {@link JudgeResult} otherwise (chain-ending, async).
|
|
11346
|
+
*/
|
|
11347
|
+
async judge(llmJudge, options) {
|
|
11348
|
+
const { intent, rubric } = options;
|
|
11349
|
+
const evalCase = {
|
|
11350
|
+
name: "inline-judge",
|
|
11351
|
+
turns: [],
|
|
11352
|
+
expectedBehavior: intent,
|
|
11353
|
+
rubric: rubric ?? `Pass when the agent's behavior matches: ${intent}`
|
|
11354
|
+
};
|
|
11355
|
+
const transcript = historyTranscript(this.turnResult.historySnapshot);
|
|
11356
|
+
const verdict = await llmJudge.judgeCase(evalCase, transcript);
|
|
11357
|
+
getLogger().info(
|
|
11358
|
+
`judge intent=${JSON.stringify(intent)} score=${verdict.score.toFixed(2)} passed=${verdict.passed}`
|
|
11359
|
+
);
|
|
11360
|
+
if (!verdict.passed) {
|
|
11361
|
+
throw new AssertionError({
|
|
11362
|
+
message: `LLM judge failed the turn (score=${verdict.score.toFixed(2)}): ${verdict.reasoning} \u2014 intent was ${JSON.stringify(intent)}; agent said ` + JSON.stringify(this.turnResult.agentText)
|
|
11363
|
+
});
|
|
11364
|
+
}
|
|
11365
|
+
return verdict;
|
|
11366
|
+
}
|
|
11367
|
+
};
|
|
11368
|
+
|
|
10098
11369
|
// src/observability/index.ts
|
|
10099
11370
|
init_esm_shims();
|
|
10100
11371
|
|
|
11372
|
+
// src/observability/attributes.ts
|
|
11373
|
+
init_esm_shims();
|
|
11374
|
+
var DEFAULT_SIDE = "uut";
|
|
11375
|
+
var _scopeStack = [];
|
|
11376
|
+
function _currentScope() {
|
|
11377
|
+
return _scopeStack.length > 0 ? _scopeStack[_scopeStack.length - 1] : null;
|
|
11378
|
+
}
|
|
11379
|
+
function _tryLoadOtelApi() {
|
|
11380
|
+
try {
|
|
11381
|
+
return __require("@opentelemetry/api");
|
|
11382
|
+
} catch {
|
|
11383
|
+
return null;
|
|
11384
|
+
}
|
|
11385
|
+
}
|
|
11386
|
+
function recordPatterAttrs(attrs) {
|
|
11387
|
+
if (!isTracingEnabled()) return;
|
|
11388
|
+
const scope = _currentScope();
|
|
11389
|
+
if (scope === null) return;
|
|
11390
|
+
const api = _tryLoadOtelApi();
|
|
11391
|
+
if (!api) return;
|
|
11392
|
+
const full = { ...attrs };
|
|
11393
|
+
if (full["patter.call_id"] === void 0) full["patter.call_id"] = scope.callId;
|
|
11394
|
+
if (full["patter.side"] === void 0) full["patter.side"] = scope.side;
|
|
11395
|
+
try {
|
|
11396
|
+
const active = api.trace.getActiveSpan?.() ?? null;
|
|
11397
|
+
if (active && (active.isRecording === void 0 || active.isRecording())) {
|
|
11398
|
+
for (const [k, v] of Object.entries(full)) {
|
|
11399
|
+
try {
|
|
11400
|
+
active.setAttribute(k, v);
|
|
11401
|
+
} catch {
|
|
11402
|
+
}
|
|
11403
|
+
}
|
|
11404
|
+
return;
|
|
11405
|
+
}
|
|
11406
|
+
} catch {
|
|
11407
|
+
}
|
|
11408
|
+
try {
|
|
11409
|
+
const tracer = api.trace.getTracer("getpatter.observability");
|
|
11410
|
+
const span = tracer.startSpan("patter.billable", { attributes: full });
|
|
11411
|
+
try {
|
|
11412
|
+
span.end();
|
|
11413
|
+
} catch {
|
|
11414
|
+
}
|
|
11415
|
+
} catch {
|
|
11416
|
+
}
|
|
11417
|
+
}
|
|
11418
|
+
async function patterCallScope(options, fn) {
|
|
11419
|
+
if (!options.callId) {
|
|
11420
|
+
throw new Error("patterCallScope requires non-empty callId");
|
|
11421
|
+
}
|
|
11422
|
+
const frame = {
|
|
11423
|
+
callId: options.callId,
|
|
11424
|
+
side: options.side ?? DEFAULT_SIDE
|
|
11425
|
+
};
|
|
11426
|
+
_scopeStack.push(frame);
|
|
11427
|
+
try {
|
|
11428
|
+
return await fn();
|
|
11429
|
+
} finally {
|
|
11430
|
+
const idx = _scopeStack.lastIndexOf(frame);
|
|
11431
|
+
if (idx >= 0) _scopeStack.splice(idx, 1);
|
|
11432
|
+
}
|
|
11433
|
+
}
|
|
11434
|
+
function attachSpanExporter(patterInstance, exporter, options = {}) {
|
|
11435
|
+
const side = options.side ?? DEFAULT_SIDE;
|
|
11436
|
+
patterInstance._patterSide = side;
|
|
11437
|
+
if (!isTracingEnabled()) {
|
|
11438
|
+
getLogger().debug(
|
|
11439
|
+
`attachSpanExporter: ${ENV_FLAG} not enabled or tracer unavailable; only side= stored`
|
|
11440
|
+
);
|
|
11441
|
+
return;
|
|
11442
|
+
}
|
|
11443
|
+
let sdkTraceBase = null;
|
|
11444
|
+
let sdkTraceNode = null;
|
|
11445
|
+
try {
|
|
11446
|
+
sdkTraceBase = __require("@opentelemetry/sdk-trace-base");
|
|
11447
|
+
} catch {
|
|
11448
|
+
sdkTraceBase = null;
|
|
11449
|
+
}
|
|
11450
|
+
try {
|
|
11451
|
+
sdkTraceNode = __require("@opentelemetry/sdk-trace-node");
|
|
11452
|
+
} catch {
|
|
11453
|
+
sdkTraceNode = null;
|
|
11454
|
+
}
|
|
11455
|
+
if (!sdkTraceBase) {
|
|
11456
|
+
getLogger().warn(
|
|
11457
|
+
"attachSpanExporter: @opentelemetry/sdk-trace-base is not installed; spans will not be exported. Install @opentelemetry/sdk-trace-base + @opentelemetry/sdk-trace-node."
|
|
11458
|
+
);
|
|
11459
|
+
return;
|
|
11460
|
+
}
|
|
11461
|
+
const api = _tryLoadOtelApi();
|
|
11462
|
+
if (!api) return;
|
|
11463
|
+
let provider = null;
|
|
11464
|
+
try {
|
|
11465
|
+
const tracerApi = api.trace;
|
|
11466
|
+
const existing = tracerApi.getTracerProvider?.() ?? null;
|
|
11467
|
+
if (existing && typeof existing.addSpanProcessor === "function") {
|
|
11468
|
+
provider = existing;
|
|
11469
|
+
}
|
|
11470
|
+
} catch {
|
|
11471
|
+
provider = null;
|
|
11472
|
+
}
|
|
11473
|
+
if (!provider) {
|
|
11474
|
+
if (!sdkTraceNode) {
|
|
11475
|
+
getLogger().warn(
|
|
11476
|
+
"attachSpanExporter: no SDK TracerProvider registered and @opentelemetry/sdk-trace-node is not installed; cannot wire exporter."
|
|
11477
|
+
);
|
|
11478
|
+
return;
|
|
11479
|
+
}
|
|
11480
|
+
try {
|
|
11481
|
+
provider = new sdkTraceNode.NodeTracerProvider();
|
|
11482
|
+
const trace = api.trace;
|
|
11483
|
+
trace.setGlobalTracerProvider?.(provider);
|
|
11484
|
+
} catch (e) {
|
|
11485
|
+
getLogger().debug(
|
|
11486
|
+
`attachSpanExporter: failed to construct NodeTracerProvider: ${String(
|
|
11487
|
+
e?.message ?? e
|
|
11488
|
+
)}`
|
|
11489
|
+
);
|
|
11490
|
+
return;
|
|
11491
|
+
}
|
|
11492
|
+
}
|
|
11493
|
+
let seen = provider._patterAttachedExporters;
|
|
11494
|
+
if (!seen) {
|
|
11495
|
+
seen = /* @__PURE__ */ new Set();
|
|
11496
|
+
provider._patterAttachedExporters = seen;
|
|
11497
|
+
}
|
|
11498
|
+
if (seen.has(exporter)) return;
|
|
11499
|
+
try {
|
|
11500
|
+
const processor = new sdkTraceBase.SimpleSpanProcessor(exporter);
|
|
11501
|
+
provider.addSpanProcessor?.(processor);
|
|
11502
|
+
seen.add(exporter);
|
|
11503
|
+
} catch (e) {
|
|
11504
|
+
getLogger().debug(
|
|
11505
|
+
`attachSpanExporter: failed to register exporter: ${String(
|
|
11506
|
+
e?.message ?? e
|
|
11507
|
+
)}`
|
|
11508
|
+
);
|
|
11509
|
+
}
|
|
11510
|
+
}
|
|
11511
|
+
|
|
10101
11512
|
// src/index.ts
|
|
10102
11513
|
var hermes = Object.freeze({ LLM: LLM8 });
|
|
10103
11514
|
var openclaw = Object.freeze({ LLM: LLM9 });
|
|
10104
11515
|
var openaiCompatible = Object.freeze({ LLM: LLM6 });
|
|
10105
11516
|
var custom = Object.freeze({ LLM: LLM7 });
|
|
10106
11517
|
export {
|
|
11518
|
+
AGENT_BACKLOG_CAP_S,
|
|
10107
11519
|
AllProvidersFailedError,
|
|
10108
11520
|
LLM2 as AnthropicLLM,
|
|
10109
11521
|
STT6 as AssemblyAISTT,
|
|
@@ -10134,7 +11546,12 @@ export {
|
|
|
10134
11546
|
TTS as ElevenLabsTTS,
|
|
10135
11547
|
TTS2 as ElevenLabsWebSocketTTS,
|
|
10136
11548
|
ErrorCode,
|
|
11549
|
+
EvalRunner,
|
|
11550
|
+
EvalSession,
|
|
10137
11551
|
EventBus,
|
|
11552
|
+
FakeAudioSender,
|
|
11553
|
+
FakeSTT,
|
|
11554
|
+
FakeTTS,
|
|
10138
11555
|
FallbackLLMProvider,
|
|
10139
11556
|
GEMINI_DEFAULT_INPUT_SR,
|
|
10140
11557
|
GEMINI_DEFAULT_OUTPUT_SR,
|
|
@@ -10148,8 +11565,10 @@ export {
|
|
|
10148
11565
|
KrispFrameDuration,
|
|
10149
11566
|
KrispSampleRate,
|
|
10150
11567
|
KrispVivaFilter,
|
|
11568
|
+
LLMJudge,
|
|
10151
11569
|
LLMLoop,
|
|
10152
11570
|
TTS6 as LMNTTTS,
|
|
11571
|
+
LocalCallRecorder,
|
|
10153
11572
|
MetricsStore,
|
|
10154
11573
|
MinWordsStrategy,
|
|
10155
11574
|
Ngrok,
|
|
@@ -10183,11 +11602,13 @@ export {
|
|
|
10183
11602
|
PlivoAdapter,
|
|
10184
11603
|
PricingUnit,
|
|
10185
11604
|
ProvisionError,
|
|
11605
|
+
RECORDING_SAMPLE_RATE,
|
|
10186
11606
|
RateLimitError,
|
|
10187
11607
|
RemoteMessageHandler,
|
|
10188
11608
|
RimeAudioFormat,
|
|
10189
11609
|
RimeModel,
|
|
10190
11610
|
TTS5 as RimeTTS,
|
|
11611
|
+
SMART_TURN_MODEL_ENV_VAR,
|
|
10191
11612
|
SPAN_BARGEIN,
|
|
10192
11613
|
SPAN_CALL,
|
|
10193
11614
|
SPAN_ENDPOINT,
|
|
@@ -10195,8 +11616,10 @@ export {
|
|
|
10195
11616
|
SPAN_STT,
|
|
10196
11617
|
SPAN_TOOL,
|
|
10197
11618
|
SPAN_TTS,
|
|
11619
|
+
ScriptedLLMProvider,
|
|
10198
11620
|
SentenceChunker,
|
|
10199
11621
|
SileroVAD,
|
|
11622
|
+
SmartTurnDetector,
|
|
10200
11623
|
STT5 as SonioxSTT,
|
|
10201
11624
|
SpeechEvents,
|
|
10202
11625
|
SpeechmaticsAudioEncoding,
|
|
@@ -10218,6 +11641,7 @@ export {
|
|
|
10218
11641
|
TestSession,
|
|
10219
11642
|
TfidfLoopDetector,
|
|
10220
11643
|
Tool,
|
|
11644
|
+
TurnExpectation,
|
|
10221
11645
|
Carrier2 as Twilio,
|
|
10222
11646
|
TwilioAdapter,
|
|
10223
11647
|
ULTRAVOX_DEFAULT_API_BASE,
|
|
@@ -10225,6 +11649,7 @@ export {
|
|
|
10225
11649
|
UltravoxRealtimeAdapter,
|
|
10226
11650
|
STT2 as WhisperSTT,
|
|
10227
11651
|
assemblyai,
|
|
11652
|
+
attachSpanExporter,
|
|
10228
11653
|
builtinClipPath,
|
|
10229
11654
|
calculateRealtimeCost,
|
|
10230
11655
|
calculateSttCost,
|
|
@@ -10241,7 +11666,9 @@ export {
|
|
|
10241
11666
|
deepgram,
|
|
10242
11667
|
defineTool,
|
|
10243
11668
|
elevenlabs,
|
|
11669
|
+
evalResultToDict,
|
|
10244
11670
|
evaluateStrategies as evaluateBargeInStrategies,
|
|
11671
|
+
expect,
|
|
10245
11672
|
filterEmoji,
|
|
10246
11673
|
filterForTTS,
|
|
10247
11674
|
filterMarkdown,
|
|
@@ -10251,11 +11678,13 @@ export {
|
|
|
10251
11678
|
guardrail,
|
|
10252
11679
|
hashCaller,
|
|
10253
11680
|
hermes,
|
|
11681
|
+
historyTranscript,
|
|
10254
11682
|
initTracing,
|
|
10255
11683
|
isRemoteUrl,
|
|
10256
11684
|
isTracingEnabled,
|
|
10257
11685
|
isWebSocketUrl,
|
|
10258
11686
|
lmnt,
|
|
11687
|
+
loadSuite,
|
|
10259
11688
|
makeAuthMiddleware,
|
|
10260
11689
|
mergePricing,
|
|
10261
11690
|
mixPcm,
|
|
@@ -10268,7 +11697,9 @@ export {
|
|
|
10268
11697
|
openclaw,
|
|
10269
11698
|
openclawConsult,
|
|
10270
11699
|
openclawPostCallNotifier,
|
|
11700
|
+
patterCallScope,
|
|
10271
11701
|
pcm16ToMulaw,
|
|
11702
|
+
recordPatterAttrs,
|
|
10272
11703
|
resample16kTo8k,
|
|
10273
11704
|
resample24kTo16k,
|
|
10274
11705
|
resample8kTo16k,
|
|
@@ -10280,11 +11711,15 @@ export {
|
|
|
10280
11711
|
scheduleOnce,
|
|
10281
11712
|
selectSoundFromList,
|
|
10282
11713
|
setLogger,
|
|
11714
|
+
shutdownTracing,
|
|
10283
11715
|
soniox,
|
|
10284
11716
|
speechmatics,
|
|
10285
11717
|
startSpan,
|
|
10286
11718
|
startTunnel,
|
|
11719
|
+
textTurn,
|
|
10287
11720
|
tool,
|
|
11721
|
+
toolCallTurn,
|
|
10288
11722
|
ultravox,
|
|
10289
|
-
whisper
|
|
11723
|
+
whisper,
|
|
11724
|
+
withSpan
|
|
10290
11725
|
};
|