getpatter 0.6.3 → 0.6.5

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/index.mjs CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  OpenAILLMProvider,
20
20
  PRICING_LAST_UPDATED,
21
21
  PRICING_VERSION,
22
+ PatterConfigError,
22
23
  PatterConnectionError,
23
24
  PatterError,
24
25
  PipelineHookExecutor,
@@ -52,9 +53,11 @@ import {
52
53
  mergePricing,
53
54
  mountApi,
54
55
  mountDashboard,
56
+ openclawConsult,
57
+ openclawPostCallNotifier,
55
58
  resolveLogRoot,
56
59
  startSpan
57
- } from "./chunk-Z6W5XFWS.mjs";
60
+ } from "./chunk-CRPJLVHB.mjs";
58
61
  import {
59
62
  OpenAIRealtime2Adapter,
60
63
  OpenAIRealtimeAdapter,
@@ -73,8 +76,9 @@ import {
73
76
  pcm16ToMulaw,
74
77
  resample16kTo8k,
75
78
  resample24kTo16k,
76
- resample8kTo16k
77
- } from "./chunk-CL2U3YET.mjs";
79
+ resample8kTo16k,
80
+ validateRealtimeTurnDetection
81
+ } from "./chunk-BO227NTF.mjs";
78
82
  import {
79
83
  MinWordsStrategy,
80
84
  evaluateStrategies,
@@ -89,7 +93,7 @@ import {
89
93
  } from "./chunk-6GR5MHHQ.mjs";
90
94
  import {
91
95
  SileroVAD
92
- } from "./chunk-R2T4JABZ.mjs";
96
+ } from "./chunk-3VVATR6A.mjs";
93
97
  import {
94
98
  __dirname,
95
99
  __require,
@@ -111,6 +115,9 @@ var Realtime = class {
111
115
  voice;
112
116
  reasoningEffort;
113
117
  inputAudioTranscriptionModel;
118
+ noiseReduction;
119
+ turnDetection;
120
+ gateResponseOnTranscript;
114
121
  constructor(opts = {}) {
115
122
  const key = opts.apiKey ?? process.env.OPENAI_API_KEY;
116
123
  if (!key) {
@@ -118,11 +125,20 @@ var Realtime = class {
118
125
  "OpenAI Realtime requires an apiKey. Pass { apiKey: 'sk-...' } or set OPENAI_API_KEY in the environment."
119
126
  );
120
127
  }
128
+ if (opts.noiseReduction !== void 0 && opts.noiseReduction !== "near_field" && opts.noiseReduction !== "far_field") {
129
+ throw new Error(
130
+ `noiseReduction must be 'near_field' or 'far_field', got ${JSON.stringify(opts.noiseReduction)}`
131
+ );
132
+ }
133
+ validateRealtimeTurnDetection(opts.turnDetection);
121
134
  this.apiKey = key;
122
135
  this.model = opts.model ?? "gpt-realtime-mini";
123
136
  this.voice = opts.voice ?? "alloy";
124
137
  this.reasoningEffort = opts.reasoningEffort;
125
138
  this.inputAudioTranscriptionModel = opts.inputAudioTranscriptionModel;
139
+ this.noiseReduction = opts.noiseReduction;
140
+ this.turnDetection = opts.turnDetection;
141
+ this.gateResponseOnTranscript = opts.gateResponseOnTranscript;
126
142
  }
127
143
  };
128
144
 
@@ -135,6 +151,9 @@ var Realtime2 = class {
135
151
  voice;
136
152
  reasoningEffort;
137
153
  inputAudioTranscriptionModel;
154
+ noiseReduction;
155
+ turnDetection;
156
+ gateResponseOnTranscript;
138
157
  constructor(opts = {}) {
139
158
  const key = opts.apiKey ?? process.env.OPENAI_API_KEY;
140
159
  if (!key) {
@@ -142,11 +161,20 @@ var Realtime2 = class {
142
161
  "OpenAI Realtime 2 requires an apiKey. Pass { apiKey: 'sk-...' } or set OPENAI_API_KEY in the environment."
143
162
  );
144
163
  }
164
+ if (opts.noiseReduction !== void 0 && opts.noiseReduction !== "near_field" && opts.noiseReduction !== "far_field") {
165
+ throw new Error(
166
+ `noiseReduction must be 'near_field' or 'far_field', got ${JSON.stringify(opts.noiseReduction)}`
167
+ );
168
+ }
169
+ validateRealtimeTurnDetection(opts.turnDetection);
145
170
  this.apiKey = key;
146
171
  this.model = opts.model ?? "gpt-realtime-2";
147
172
  this.voice = opts.voice ?? "alloy";
148
173
  this.reasoningEffort = opts.reasoningEffort;
149
174
  this.inputAudioTranscriptionModel = opts.inputAudioTranscriptionModel;
175
+ this.noiseReduction = opts.noiseReduction;
176
+ this.turnDetection = opts.turnDetection;
177
+ this.gateResponseOnTranscript = opts.gateResponseOnTranscript;
150
178
  }
151
179
  };
152
180
 
@@ -573,7 +601,7 @@ function resolvePersistRoot(persist) {
573
601
  if (typeof persist === "string") return resolveLogRoot(persist);
574
602
  const envRoot = resolveLogRoot();
575
603
  if (envRoot !== null) return envRoot;
576
- return resolveLogRoot("auto");
604
+ return null;
577
605
  }
578
606
  function closeParkedConnections(slot) {
579
607
  if (slot.stt) {
@@ -857,7 +885,12 @@ var Patter = class {
857
885
  ...working,
858
886
  provider: "openai_realtime",
859
887
  model: working.model ?? engine.model,
860
- voice: working.voice ?? engine.voice
888
+ voice: working.voice ?? engine.voice,
889
+ // Explicit agent() kwargs win over the engine marker value
890
+ // (same precedence as Python: explicit kwarg > engine > default).
891
+ openaiRealtimeNoiseReduction: working.openaiRealtimeNoiseReduction ?? engine.noiseReduction,
892
+ realtimeTurnDetection: working.realtimeTurnDetection ?? engine.turnDetection,
893
+ openaiRealtimeGateResponseOnTranscript: working.openaiRealtimeGateResponseOnTranscript ?? engine.gateResponseOnTranscript
861
894
  };
862
895
  if (!this.localConfig.openaiKey) {
863
896
  this.localConfig = { ...this.localConfig, openaiKey: engine.apiKey };
@@ -882,6 +915,11 @@ var Patter = class {
882
915
  throw new Error(`provider must be one of: ${valid.join(", ")}. Got: '${working.provider}'`);
883
916
  }
884
917
  }
918
+ if (working.consult && working.provider === "elevenlabs_convai") {
919
+ getLogger().warn(
920
+ "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."
921
+ );
922
+ }
885
923
  if (working.llm !== void 0) {
886
924
  const llm = working.llm;
887
925
  if (!llm || typeof llm.stream !== "function") {
@@ -982,7 +1020,7 @@ var Patter = class {
982
1020
  const telephonyProvider = carrier.kind;
983
1021
  const wantsCarrierManagement = opts.manageWebhook !== false || wantsCloudflared;
984
1022
  if (wantsCarrierManagement) {
985
- const { autoConfigureCarrier } = await import("./carrier-config-3WDQXP5J.mjs");
1023
+ const { autoConfigureCarrier } = await import("./carrier-config-7YGNRBPO.mjs");
986
1024
  await autoConfigureCarrier({
987
1025
  telephonyProvider,
988
1026
  twilioSid: carrier.kind === "twilio" ? carrier.accountSid : void 0,
@@ -1020,7 +1058,8 @@ var Patter = class {
1020
1058
  opts.onMetrics,
1021
1059
  opts.pricing,
1022
1060
  opts.dashboard ?? true,
1023
- opts.dashboardToken ?? ""
1061
+ opts.dashboardToken ?? "",
1062
+ opts.allowInsecureDashboard ?? false
1024
1063
  );
1025
1064
  this.embeddedServer.popPrewarmAudio = this.popPrewarmAudio;
1026
1065
  this.embeddedServer.popPrewarmedConnections = this.popPrewarmedConnections;
@@ -1039,7 +1078,7 @@ var Patter = class {
1039
1078
  }
1040
1079
  /** Run the agent in interactive terminal-test mode (no real telephony). */
1041
1080
  async test(opts) {
1042
- const { TestSession: TestSession2 } = await import("./test-mode-MDBQ4ECE.mjs");
1081
+ const { TestSession: TestSession2 } = await import("./test-mode-HGHI2AUV.mjs");
1043
1082
  const session = new TestSession2();
1044
1083
  await session.run({
1045
1084
  agent: opts.agent,
@@ -1218,7 +1257,7 @@ var Patter = class {
1218
1257
  }
1219
1258
  if (wantsRealtimePark) {
1220
1259
  tasks.push((async () => {
1221
- const { OpenAIRealtime2Adapter: OpenAIRealtime2Adapter2 } = await import("./openai-realtime-2-CNFARP25.mjs");
1260
+ const { OpenAIRealtime2Adapter: OpenAIRealtime2Adapter2 } = await import("./openai-realtime-2-L5EKAAUH.mjs");
1222
1261
  const apiKey = process.env.OPENAI_API_KEY ?? "";
1223
1262
  if (!apiKey) {
1224
1263
  getLogger().debug(`Park OpenAI Realtime skipped for ${callId}: no OPENAI_API_KEY`);
@@ -1432,8 +1471,8 @@ var Patter = class {
1432
1471
  if (!options.to) {
1433
1472
  throw new Error("'to' phone number is required");
1434
1473
  }
1435
- if (!options.to.startsWith("+")) {
1436
- throw new Error(`'to' must be in E.164 format (e.g., '+1234567890'). Got: '${options.to}'`);
1474
+ if (!/^\+[1-9]\d{6,14}$/.test(options.to)) {
1475
+ throw new Error("'to' must be E.164 format (+<country><digits>). Got value with invalid format.");
1437
1476
  }
1438
1477
  if (options.wait && !this.embeddedServer) {
1439
1478
  throw new PatterConnectionError(
@@ -1444,9 +1483,6 @@ var Patter = class {
1444
1483
  let callId = "";
1445
1484
  const effectiveRingTimeout = options.ringTimeout === void 0 ? 25 : options.ringTimeout;
1446
1485
  const wantsAmd = options.machineDetection !== false || Boolean(options.voicemailMessage);
1447
- if (this.embeddedServer) {
1448
- this.embeddedServer.onMachineDetection = options.onMachineDetection;
1449
- }
1450
1486
  if (options.agent.prewarm !== false) {
1451
1487
  this.spawnProviderWarmup(options.agent);
1452
1488
  }
@@ -1491,6 +1527,12 @@ var Patter = class {
1491
1527
  };
1492
1528
  if (this.embeddedServer) {
1493
1529
  this.embeddedServer.metricsStore.recordCallInitiated(initiatedPayload);
1530
+ if (options.onMachineDetection) {
1531
+ this.embeddedServer.onMachineDetectionByCallSid.set(
1532
+ telnyxCallId,
1533
+ options.onMachineDetection
1534
+ );
1535
+ }
1494
1536
  }
1495
1537
  try {
1496
1538
  const { notifyDashboard: notifyDashboard2 } = await import("./persistence-LVIAHESK.mjs");
@@ -1556,6 +1598,12 @@ var Patter = class {
1556
1598
  };
1557
1599
  if (this.embeddedServer) {
1558
1600
  this.embeddedServer.metricsStore.recordCallInitiated(initiatedPayload);
1601
+ if (options.onMachineDetection) {
1602
+ this.embeddedServer.onMachineDetectionByCallSid.set(
1603
+ plivoCallId,
1604
+ options.onMachineDetection
1605
+ );
1606
+ }
1559
1607
  }
1560
1608
  try {
1561
1609
  const { notifyDashboard: notifyDashboard2 } = await import("./persistence-LVIAHESK.mjs");
@@ -1625,6 +1673,12 @@ var Patter = class {
1625
1673
  };
1626
1674
  if (this.embeddedServer) {
1627
1675
  this.embeddedServer.metricsStore.recordCallInitiated(initiatedPayload);
1676
+ if (options.onMachineDetection) {
1677
+ this.embeddedServer.onMachineDetectionByCallSid.set(
1678
+ twilioCallSid,
1679
+ options.onMachineDetection
1680
+ );
1681
+ }
1628
1682
  if (twilioNotificationsPath) {
1629
1683
  getLogger().info(
1630
1684
  `Outbound call ${twilioCallSid} placed. Twilio notifications: https://api.twilio.com${twilioNotificationsPath} (check here if the call drops with no audio).`
@@ -2105,8 +2159,8 @@ var FallbackLLMProvider = class {
2105
2159
  * markers are filtered out so callers can concatenate the yielded strings
2106
2160
  * directly.
2107
2161
  */
2108
- async *completeStream(messages, tools) {
2109
- for await (const chunk of this.stream(messages, tools)) {
2162
+ async *completeStream(messages, tools, opts) {
2163
+ for await (const chunk of this.stream(messages, tools, opts)) {
2110
2164
  if (chunk.type === "text") {
2111
2165
  yield chunk.content ?? "";
2112
2166
  }
@@ -2116,14 +2170,15 @@ var FallbackLLMProvider = class {
2116
2170
  // LLMProvider implementation
2117
2171
  // -----------------------------------------------------------------------
2118
2172
  /** Streaming entry point — yields chunks from the first provider that succeeds. */
2119
- async *stream(messages, tools) {
2173
+ async *stream(messages, tools, opts) {
2120
2174
  const errors = [];
2121
2175
  const result = yield* this.tryProviders(
2122
2176
  messages,
2123
2177
  tools,
2124
2178
  /* availableOnly */
2125
2179
  true,
2126
- errors
2180
+ errors,
2181
+ opts
2127
2182
  );
2128
2183
  if (result === "done") return;
2129
2184
  getLogger().warn(
@@ -2134,7 +2189,8 @@ var FallbackLLMProvider = class {
2134
2189
  tools,
2135
2190
  /* availableOnly */
2136
2191
  false,
2137
- errors
2192
+ errors,
2193
+ opts
2138
2194
  );
2139
2195
  if (retryResult === "done") return;
2140
2196
  throw new AllProvidersFailedError(
@@ -2144,7 +2200,7 @@ var FallbackLLMProvider = class {
2144
2200
  // -----------------------------------------------------------------------
2145
2201
  // Internals
2146
2202
  // -----------------------------------------------------------------------
2147
- async *tryProviders(messages, tools, availableOnly, errors) {
2203
+ async *tryProviders(messages, tools, availableOnly, errors, opts) {
2148
2204
  for (let i = 0; i < this.providers.length; i++) {
2149
2205
  if (availableOnly && !this.availability[i]) continue;
2150
2206
  for (let attempt = 0; attempt < this.maxRetryPerProvider; attempt++) {
@@ -2153,7 +2209,7 @@ var FallbackLLMProvider = class {
2153
2209
  `FallbackLLMProvider: trying provider ${i}${attempt > 0 ? ` (retry ${attempt})` : ""}`
2154
2210
  );
2155
2211
  let yieldedTokens = false;
2156
- const gen = this.providers[i].stream(messages, tools);
2212
+ const gen = this.providers[i].stream(messages, tools, opts);
2157
2213
  while (true) {
2158
2214
  let iterResult;
2159
2215
  try {
@@ -2273,6 +2329,11 @@ var PatterTool = class {
2273
2329
  maxDurationSec;
2274
2330
  recording;
2275
2331
  started = false;
2332
+ /** Cached in-progress (or completed) start promise so concurrent execute()
2333
+ * callers all await the same boot sequence instead of each racing into
2334
+ * phone.serve(). Reset to null on failure so callers can retry after a
2335
+ * transient error. */
2336
+ startPromise = null;
2276
2337
  constructor(opts) {
2277
2338
  if (!opts.phone) {
2278
2339
  throw new Error("PatterTool: `phone` (a Patter instance) is required.");
@@ -2324,8 +2385,21 @@ var PatterTool = class {
2324
2385
  * `serve()` provides here. No `onCallEnd` callback is wired: the SDK's own
2325
2386
  * per-callId completion registry resolves the result, so the user's
2326
2387
  * `onCallEnd` slot is left free.
2388
+ *
2389
+ * Idempotent and concurrency-safe: concurrent callers all await the same
2390
+ * in-progress boot instead of each racing into `phone.serve()`.
2327
2391
  */
2328
2392
  async start() {
2393
+ if (this.startPromise) return this.startPromise;
2394
+ this.startPromise = this._doStart();
2395
+ try {
2396
+ await this.startPromise;
2397
+ } catch (err) {
2398
+ this.startPromise = null;
2399
+ throw err;
2400
+ }
2401
+ }
2402
+ async _doStart() {
2329
2403
  if (this.started) return;
2330
2404
  if (!this.agent) {
2331
2405
  throw new Error(
@@ -2351,6 +2425,7 @@ var PatterTool = class {
2351
2425
  }
2352
2426
  }
2353
2427
  this.started = false;
2428
+ this.startPromise = null;
2354
2429
  }
2355
2430
  // --- Execution ----------------------------------------------------------
2356
2431
  /**
@@ -2715,7 +2790,8 @@ var UltravoxRealtimeAdapter = class {
2715
2790
  "X-API-Key": this.apiKey,
2716
2791
  "Content-Type": "application/json"
2717
2792
  },
2718
- body: JSON.stringify(body)
2793
+ body: JSON.stringify(body),
2794
+ signal: AbortSignal.timeout(15e3)
2719
2795
  });
2720
2796
  if (!resp.ok) {
2721
2797
  const text = await resp.text().catch(() => "");
@@ -2726,12 +2802,36 @@ var UltravoxRealtimeAdapter = class {
2726
2802
  this.ws = new WebSocket(call.joinUrl);
2727
2803
  await new Promise((resolve, reject) => {
2728
2804
  const ws = this.ws;
2805
+ let settled = false;
2806
+ const timer = setTimeout(() => {
2807
+ if (settled) return;
2808
+ settled = true;
2809
+ ws.off("open", onOpen);
2810
+ ws.off("error", onError);
2811
+ this.ws = null;
2812
+ try {
2813
+ ws.close();
2814
+ } catch {
2815
+ }
2816
+ reject(new Error("Ultravox WS connect timeout"));
2817
+ }, 15e3);
2729
2818
  const onOpen = () => {
2819
+ if (settled) return;
2820
+ settled = true;
2821
+ clearTimeout(timer);
2730
2822
  ws.off("error", onError);
2731
2823
  resolve();
2732
2824
  };
2733
2825
  const onError = (err) => {
2826
+ if (settled) return;
2827
+ settled = true;
2828
+ clearTimeout(timer);
2734
2829
  ws.off("open", onOpen);
2830
+ this.ws = null;
2831
+ try {
2832
+ ws.close();
2833
+ } catch {
2834
+ }
2735
2835
  reject(err);
2736
2836
  };
2737
2837
  ws.once("open", onOpen);
@@ -3570,7 +3670,7 @@ var STT = class extends DeepgramSTT {
3570
3670
  {
3571
3671
  endpointingMs: opts.endpointingMs ?? 150,
3572
3672
  utteranceEndMs: opts.utteranceEndMs === null ? null : opts.utteranceEndMs ?? 1e3,
3573
- smartFormat: opts.smartFormat ?? true,
3673
+ smartFormat: opts.smartFormat ?? false,
3574
3674
  interimResults: opts.interimResults ?? true,
3575
3675
  ...opts.vadEvents !== void 0 ? { vadEvents: opts.vadEvents } : {}
3576
3676
  }
@@ -3888,7 +3988,7 @@ var CartesiaSTT = class {
3888
3988
  });
3889
3989
  ws.once("error", (err) => {
3890
3990
  clearTimeout(timer);
3891
- reject(err);
3991
+ reject(new Error(`Cartesia STT park connect failed: ${describeWarmupError(err)}`));
3892
3992
  });
3893
3993
  });
3894
3994
  return ws;
@@ -4243,7 +4343,7 @@ var SonioxSTT = class _SonioxSTT {
4243
4343
  /** Stable pricing/dashboard key — read by stream-handler/metrics. */
4244
4344
  static providerKey = "soniox";
4245
4345
  ws = null;
4246
- callbacks = [];
4346
+ callbacks = /* @__PURE__ */ new Set();
4247
4347
  final = new TokenAccumulator();
4248
4348
  keepaliveTimer = null;
4249
4349
  apiKey;
@@ -4405,16 +4505,13 @@ var SonioxSTT = class _SonioxSTT {
4405
4505
  if (audio.length === 0) return;
4406
4506
  this.ws.send(audio);
4407
4507
  }
4408
- /** Register a transcript listener (max 10 concurrent listeners). */
4508
+ /** Register a transcript listener. */
4409
4509
  onTranscript(callback) {
4410
- if (this.callbacks.length >= 10) {
4411
- getLogger().warn(
4412
- "SonioxSTT: maximum of 10 onTranscript callbacks reached; replacing the last callback."
4413
- );
4414
- this.callbacks[this.callbacks.length - 1] = callback;
4415
- return;
4416
- }
4417
- this.callbacks.push(callback);
4510
+ this.callbacks.add(callback);
4511
+ }
4512
+ /** Unregister a previously registered transcript listener. */
4513
+ offTranscript(callback) {
4514
+ this.callbacks.delete(callback);
4418
4515
  }
4419
4516
  /** Send the empty-frame stream terminator and close the WebSocket. */
4420
4517
  close() {
@@ -4495,12 +4592,6 @@ var VALID_DOMAINS = /* @__PURE__ */ new Set([
4495
4592
  AssemblyAIDomain.GENERAL,
4496
4593
  AssemblyAIDomain.MEDICAL_V1
4497
4594
  ]);
4498
- var AssemblyAISTTNotConnectedError = class extends Error {
4499
- constructor(message = "AssemblyAISTT is not connected") {
4500
- super(message);
4501
- this.name = "AssemblyAISTTNotConnectedError";
4502
- }
4503
- };
4504
4595
  var AssemblyAISTT = class _AssemblyAISTT {
4505
4596
  constructor(apiKey, options = {}) {
4506
4597
  this.apiKey = apiKey;
@@ -4824,9 +4915,10 @@ var AssemblyAISTT = class _AssemblyAISTT {
4824
4915
  */
4825
4916
  updateConfiguration(params) {
4826
4917
  if (!this.ws || this.ws.readyState !== WebSocket4.OPEN) {
4827
- throw new AssemblyAISTTNotConnectedError(
4828
- "AssemblyAISTT.updateConfiguration: WebSocket is not open"
4918
+ getLogger().debug(
4919
+ "AssemblyAISTT.updateConfiguration: WebSocket is not open \u2014 dropping update (call teardown)."
4829
4920
  );
4921
+ return;
4830
4922
  }
4831
4923
  const payload = {
4832
4924
  type: AssemblyAIClientFrame.UPDATE_CONFIGURATION
@@ -4848,9 +4940,10 @@ var AssemblyAISTT = class _AssemblyAISTT {
4848
4940
  /** Force the server to finalize the current turn (for barge-in). */
4849
4941
  forceEndpoint() {
4850
4942
  if (!this.ws || this.ws.readyState !== WebSocket4.OPEN) {
4851
- throw new AssemblyAISTTNotConnectedError(
4852
- "AssemblyAISTT.forceEndpoint: WebSocket is not open"
4943
+ getLogger().debug(
4944
+ "AssemblyAISTT.forceEndpoint: WebSocket is not open \u2014 dropping request (call teardown)."
4853
4945
  );
4946
+ return;
4854
4947
  }
4855
4948
  this.ws.send(JSON.stringify({ type: AssemblyAIClientFrame.FORCE_ENDPOINT }));
4856
4949
  }
@@ -4865,6 +4958,14 @@ var AssemblyAISTT = class _AssemblyAISTT {
4865
4958
  async close() {
4866
4959
  this.closing = true;
4867
4960
  if (!this.ws) return;
4961
+ if (this.chunkBufferBytes > 0 && this.ws.readyState === WebSocket4.OPEN) {
4962
+ try {
4963
+ this.ws.send(Buffer.concat(this.chunkBuffer, this.chunkBufferBytes));
4964
+ } catch {
4965
+ }
4966
+ this.chunkBuffer = [];
4967
+ this.chunkBufferBytes = 0;
4968
+ }
4868
4969
  try {
4869
4970
  this.ws.send(JSON.stringify({ type: AssemblyAIClientFrame.TERMINATE }));
4870
4971
  } catch {
@@ -6068,7 +6169,7 @@ var TTS3 = class extends OpenAITTS {
6068
6169
  opts.model ?? "gpt-4o-mini-tts",
6069
6170
  opts.instructions ?? null,
6070
6171
  opts.speed ?? null,
6071
- opts.antiAlias ?? false
6172
+ opts.antiAlias ?? true
6072
6173
  );
6073
6174
  }
6074
6175
  };
@@ -6242,7 +6343,6 @@ init_esm_shims();
6242
6343
  // src/providers/inworld-tts.ts
6243
6344
  init_esm_shims();
6244
6345
  var INWORLD_BASE_URL = "https://api.inworld.ai/tts/v1/voice:stream";
6245
- var INWORLD_VOICES_URL = "https://api.inworld.ai/tts/v1/voices";
6246
6346
  var InworldModel = {
6247
6347
  TTS_2: "inworld-tts-2",
6248
6348
  TTS_1_5_MAX: "inworld-tts-1.5-max",
@@ -6331,7 +6431,8 @@ var InworldTTS = class {
6331
6431
  */
6332
6432
  async warmup() {
6333
6433
  try {
6334
- await fetch(INWORLD_VOICES_URL, {
6434
+ const voicesUrl = new URL(this.baseUrl).origin + "/tts/v1/voices";
6435
+ await fetch(voicesUrl, {
6335
6436
  method: "GET",
6336
6437
  headers: {
6337
6438
  Authorization: `Basic ${this.authToken}`
@@ -6588,58 +6689,87 @@ var AnthropicLLMProvider = class {
6588
6689
  const toolIndexByBlock = /* @__PURE__ */ new Map();
6589
6690
  const toolIdByBlock = /* @__PURE__ */ new Map();
6590
6691
  let nextIndex = 0;
6591
- while (true) {
6592
- const { done, value } = await reader.read();
6593
- if (done) break;
6594
- buffer += decoder.decode(value, { stream: true });
6595
- const lines = buffer.split("\n");
6596
- buffer = lines.pop() || "";
6597
- for (const line of lines) {
6598
- const trimmed = line.trim();
6599
- if (!trimmed.startsWith("data: ")) continue;
6600
- const data = trimmed.slice(6);
6601
- if (!data || data === "[DONE]") continue;
6602
- let event;
6603
- try {
6604
- event = JSON.parse(data);
6605
- } catch {
6606
- continue;
6607
- }
6608
- if (event.type === "content_block_start" && event.content_block?.type === "tool_use") {
6609
- const blockIdx = event.index ?? 0;
6610
- const toolId = event.content_block.id ?? "";
6611
- const toolName = event.content_block.name ?? "";
6612
- const patterIndex = nextIndex++;
6613
- toolIndexByBlock.set(blockIdx, patterIndex);
6614
- toolIdByBlock.set(blockIdx, toolId);
6615
- yield {
6616
- type: "tool_call",
6617
- index: patterIndex,
6618
- id: toolId,
6619
- name: toolName,
6620
- arguments: ""
6621
- };
6622
- continue;
6623
- }
6624
- if (event.type === "content_block_delta") {
6625
- if (event.delta?.type === "text_delta" && event.delta.text) {
6626
- yield { type: "text", content: event.delta.text };
6692
+ let inputTokens = 0;
6693
+ let outputTokens = 0;
6694
+ let cacheReadTokens = 0;
6695
+ let cacheWriteTokens = 0;
6696
+ try {
6697
+ while (true) {
6698
+ const { done, value } = await reader.read();
6699
+ if (done) break;
6700
+ buffer += decoder.decode(value, { stream: true });
6701
+ const lines = buffer.split("\n");
6702
+ buffer = lines.pop() || "";
6703
+ for (const line of lines) {
6704
+ const trimmed = line.trim();
6705
+ if (!trimmed.startsWith("data: ")) continue;
6706
+ const data = trimmed.slice(6);
6707
+ if (!data || data === "[DONE]") continue;
6708
+ let event;
6709
+ try {
6710
+ event = JSON.parse(data);
6711
+ } catch {
6712
+ continue;
6713
+ }
6714
+ if (event.type === "message_start" && event.message?.usage) {
6715
+ const u = event.message.usage;
6716
+ if (u.input_tokens) inputTokens = u.input_tokens;
6717
+ if (u.cache_creation_input_tokens) cacheWriteTokens = u.cache_creation_input_tokens;
6718
+ if (u.cache_read_input_tokens) cacheReadTokens = u.cache_read_input_tokens;
6627
6719
  continue;
6628
6720
  }
6629
- if (event.delta?.type === "input_json_delta" && event.delta.partial_json) {
6721
+ if (event.type === "message_delta" && event.usage?.output_tokens) {
6722
+ outputTokens = event.usage.output_tokens;
6723
+ continue;
6724
+ }
6725
+ if (event.type === "content_block_start" && event.content_block?.type === "tool_use") {
6630
6726
  const blockIdx = event.index ?? 0;
6631
- const patterIndex = toolIndexByBlock.get(blockIdx);
6632
- if (patterIndex !== void 0) {
6633
- yield {
6634
- type: "tool_call",
6635
- index: patterIndex,
6636
- id: toolIdByBlock.get(blockIdx),
6637
- arguments: event.delta.partial_json
6638
- };
6727
+ const toolId = event.content_block.id ?? "";
6728
+ const toolName = event.content_block.name ?? "";
6729
+ const patterIndex = nextIndex++;
6730
+ toolIndexByBlock.set(blockIdx, patterIndex);
6731
+ toolIdByBlock.set(blockIdx, toolId);
6732
+ yield {
6733
+ type: "tool_call",
6734
+ index: patterIndex,
6735
+ id: toolId,
6736
+ name: toolName,
6737
+ arguments: ""
6738
+ };
6739
+ continue;
6740
+ }
6741
+ if (event.type === "content_block_delta") {
6742
+ if (event.delta?.type === "text_delta" && event.delta.text) {
6743
+ yield { type: "text", content: event.delta.text };
6744
+ continue;
6745
+ }
6746
+ if (event.delta?.type === "input_json_delta" && event.delta.partial_json) {
6747
+ const blockIdx = event.index ?? 0;
6748
+ const patterIndex = toolIndexByBlock.get(blockIdx);
6749
+ if (patterIndex !== void 0) {
6750
+ yield {
6751
+ type: "tool_call",
6752
+ index: patterIndex,
6753
+ id: toolIdByBlock.get(blockIdx),
6754
+ arguments: event.delta.partial_json
6755
+ };
6756
+ }
6639
6757
  }
6640
6758
  }
6641
6759
  }
6642
6760
  }
6761
+ } finally {
6762
+ reader.cancel().catch(() => {
6763
+ });
6764
+ }
6765
+ if (inputTokens > 0 || outputTokens > 0 || cacheReadTokens > 0 || cacheWriteTokens > 0) {
6766
+ yield {
6767
+ type: "usage",
6768
+ inputTokens,
6769
+ outputTokens,
6770
+ cacheReadInputTokens: cacheReadTokens,
6771
+ cacheWriteInputTokens: cacheWriteTokens
6772
+ };
6643
6773
  }
6644
6774
  yield { type: "done" };
6645
6775
  }
@@ -6699,16 +6829,17 @@ function toAnthropicMessages(messages) {
6699
6829
  }
6700
6830
  if (role === "tool") {
6701
6831
  const contentStr = typeof rawMsg.content === "string" ? rawMsg.content : JSON.stringify(rawMsg.content);
6702
- out.push({
6703
- role: "user",
6704
- content: [
6705
- {
6706
- type: "tool_result",
6707
- tool_use_id: rawMsg.tool_call_id ?? "",
6708
- content: contentStr
6709
- }
6710
- ]
6711
- });
6832
+ const toolResultBlock = {
6833
+ type: "tool_result",
6834
+ tool_use_id: rawMsg.tool_call_id ?? "",
6835
+ content: contentStr
6836
+ };
6837
+ const prev = out.length > 0 ? out[out.length - 1] : void 0;
6838
+ if (prev && prev.role === "user" && Array.isArray(prev.content) && prev.content.length > 0 && prev.content.every((b) => b["type"] === "tool_result")) {
6839
+ prev.content.push(toolResultBlock);
6840
+ } else {
6841
+ out.push({ role: "user", content: [toolResultBlock] });
6842
+ }
6712
6843
  continue;
6713
6844
  }
6714
6845
  }
@@ -6848,50 +6979,55 @@ async function* parseOpenAISseStream(response) {
6848
6979
  if (!reader) return;
6849
6980
  const decoder = new TextDecoder();
6850
6981
  let buffer = "";
6851
- while (true) {
6852
- const { done, value } = await reader.read();
6853
- if (done) break;
6854
- buffer += decoder.decode(value, { stream: true });
6855
- const lines = buffer.split("\n");
6856
- buffer = lines.pop() || "";
6857
- for (const line of lines) {
6858
- const trimmed = line.trim();
6859
- if (!trimmed || !trimmed.startsWith("data: ")) continue;
6860
- const data = trimmed.slice(6);
6861
- if (data === "[DONE]") continue;
6862
- let chunk;
6863
- try {
6864
- chunk = JSON.parse(data);
6865
- } catch {
6866
- continue;
6867
- }
6868
- const usage = chunk.usage ?? chunk.x_groq?.usage;
6869
- if (usage) {
6870
- const cached = chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0;
6871
- yield {
6872
- type: "usage",
6873
- inputTokens: usage.prompt_tokens,
6874
- outputTokens: usage.completion_tokens,
6875
- cacheReadInputTokens: cached
6876
- };
6877
- }
6878
- const delta = chunk.choices?.[0]?.delta;
6879
- if (!delta) continue;
6880
- if (delta.content) {
6881
- yield { type: "text", content: delta.content };
6882
- }
6883
- if (delta.tool_calls) {
6884
- for (const tc of delta.tool_calls) {
6982
+ try {
6983
+ while (true) {
6984
+ const { done, value } = await reader.read();
6985
+ if (done) break;
6986
+ buffer += decoder.decode(value, { stream: true });
6987
+ const lines = buffer.split("\n");
6988
+ buffer = lines.pop() || "";
6989
+ for (const line of lines) {
6990
+ const trimmed = line.trim();
6991
+ if (!trimmed || !trimmed.startsWith("data: ")) continue;
6992
+ const data = trimmed.slice(6);
6993
+ if (data === "[DONE]") continue;
6994
+ let chunk;
6995
+ try {
6996
+ chunk = JSON.parse(data);
6997
+ } catch {
6998
+ continue;
6999
+ }
7000
+ const usage = chunk.usage ?? chunk.x_groq?.usage;
7001
+ if (usage) {
7002
+ const cached = chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0;
6885
7003
  yield {
6886
- type: "tool_call",
6887
- index: tc.index,
6888
- id: tc.id,
6889
- name: tc.function?.name,
6890
- arguments: tc.function?.arguments
7004
+ type: "usage",
7005
+ inputTokens: usage.prompt_tokens,
7006
+ outputTokens: usage.completion_tokens,
7007
+ cacheReadInputTokens: cached
6891
7008
  };
6892
7009
  }
7010
+ const delta = chunk.choices?.[0]?.delta;
7011
+ if (!delta) continue;
7012
+ if (delta.content) {
7013
+ yield { type: "text", content: delta.content };
7014
+ }
7015
+ if (delta.tool_calls) {
7016
+ for (const tc of delta.tool_calls) {
7017
+ yield {
7018
+ type: "tool_call",
7019
+ index: tc.index,
7020
+ id: tc.id,
7021
+ name: tc.function?.name,
7022
+ arguments: tc.function?.arguments
7023
+ };
7024
+ }
7025
+ }
6893
7026
  }
6894
7027
  }
7028
+ } finally {
7029
+ reader.cancel().catch(() => {
7030
+ });
6895
7031
  }
6896
7032
  }
6897
7033
 
@@ -7056,11 +7192,21 @@ var CerebrasLLMProvider = class {
7056
7192
  }
7057
7193
  const advisoryMs = parseRateLimitResetMs(response.headers);
7058
7194
  const exponentialMs = RETRY_BACKOFF_BASE_MS * Math.pow(2, attempt);
7059
- const delayMs = Math.max(advisoryMs, exponentialMs);
7195
+ const delayMs = Math.min(5e3, Math.max(advisoryMs, exponentialMs));
7060
7196
  getLogger().warn(
7061
7197
  `Cerebras API ${response.status} (attempt ${attempt + 1}/${maxAttempts}); retrying after ${delayMs}ms`
7062
7198
  );
7063
- await new Promise((r) => setTimeout(r, delayMs));
7199
+ await new Promise((resolve, reject) => {
7200
+ const t = setTimeout(resolve, delayMs);
7201
+ opts?.signal?.addEventListener(
7202
+ "abort",
7203
+ () => {
7204
+ clearTimeout(t);
7205
+ reject(opts.signal.reason);
7206
+ },
7207
+ { once: true }
7208
+ );
7209
+ });
7064
7210
  }
7065
7211
  throw new PatterError(`Cerebras API error ${lastStatus}: ${lastErrText || "request failed"}`);
7066
7212
  }
@@ -7221,47 +7367,52 @@ var GoogleLLMProvider = class {
7221
7367
  let buffer = "";
7222
7368
  let nextIndex = 0;
7223
7369
  let lastUsage;
7224
- while (true) {
7225
- const { done, value } = await reader.read();
7226
- if (done) break;
7227
- buffer += decoder.decode(value, { stream: true });
7228
- const lines = buffer.split("\n");
7229
- buffer = lines.pop() || "";
7230
- for (const line of lines) {
7231
- const trimmed = line.trim();
7232
- if (!trimmed.startsWith("data: ")) continue;
7233
- const data = trimmed.slice(6);
7234
- if (!data) continue;
7235
- let payload;
7236
- try {
7237
- payload = JSON.parse(data);
7238
- } catch {
7239
- continue;
7240
- }
7241
- if (payload.usageMetadata) {
7242
- lastUsage = payload.usageMetadata;
7243
- }
7244
- const candidate = payload.candidates?.[0];
7245
- const parts = candidate?.content?.parts ?? [];
7246
- for (const part of parts) {
7247
- if (part.functionCall) {
7248
- const args = part.functionCall.args ?? {};
7249
- const callId = part.functionCall.id ?? `gemini_call_${nextIndex}`;
7250
- yield {
7251
- type: "tool_call",
7252
- index: nextIndex,
7253
- id: callId,
7254
- name: part.functionCall.name ?? "",
7255
- arguments: JSON.stringify(args)
7256
- };
7257
- nextIndex++;
7370
+ try {
7371
+ while (true) {
7372
+ const { done, value } = await reader.read();
7373
+ if (done) break;
7374
+ buffer += decoder.decode(value, { stream: true });
7375
+ const lines = buffer.split("\n");
7376
+ buffer = lines.pop() || "";
7377
+ for (const line of lines) {
7378
+ const trimmed = line.trim();
7379
+ if (!trimmed.startsWith("data: ")) continue;
7380
+ const data = trimmed.slice(6);
7381
+ if (!data) continue;
7382
+ let payload;
7383
+ try {
7384
+ payload = JSON.parse(data);
7385
+ } catch {
7258
7386
  continue;
7259
7387
  }
7260
- if (part.text) {
7261
- yield { type: "text", content: part.text };
7388
+ if (payload.usageMetadata) {
7389
+ lastUsage = payload.usageMetadata;
7390
+ }
7391
+ const candidate = payload.candidates?.[0];
7392
+ const parts = candidate?.content?.parts ?? [];
7393
+ for (const part of parts) {
7394
+ if (part.functionCall) {
7395
+ const args = part.functionCall.args ?? {};
7396
+ const callId = part.functionCall.id ?? `gemini_call_${nextIndex}`;
7397
+ yield {
7398
+ type: "tool_call",
7399
+ index: nextIndex,
7400
+ id: callId,
7401
+ name: part.functionCall.name ?? "",
7402
+ arguments: JSON.stringify(args)
7403
+ };
7404
+ nextIndex++;
7405
+ continue;
7406
+ }
7407
+ if (part.text) {
7408
+ yield { type: "text", content: part.text };
7409
+ }
7262
7410
  }
7263
7411
  }
7264
7412
  }
7413
+ } finally {
7414
+ reader.cancel().catch(() => {
7415
+ });
7265
7416
  }
7266
7417
  if (lastUsage) {
7267
7418
  yield {
@@ -7355,7 +7506,17 @@ function toGeminiContents(messages) {
7355
7506
  continue;
7356
7507
  }
7357
7508
  }
7358
- return { systemInstruction: systemParts.join("\n\n"), contents };
7509
+ const merged = [];
7510
+ for (const entry of contents) {
7511
+ const prev = merged[merged.length - 1];
7512
+ const isFunctionResponseOnly = (c) => c.role === "user" && c.parts.every((p) => p.functionResponse !== void 0);
7513
+ if (prev && isFunctionResponseOnly(prev) && isFunctionResponseOnly(entry)) {
7514
+ prev.parts.push(...entry.parts);
7515
+ } else {
7516
+ merged.push(entry);
7517
+ }
7518
+ }
7519
+ return { systemInstruction: systemParts.join("\n\n"), contents: merged };
7359
7520
  }
7360
7521
 
7361
7522
  // src/llm/google.ts
@@ -7378,6 +7539,260 @@ var LLM5 = class extends GoogleLLMProvider {
7378
7539
  }
7379
7540
  };
7380
7541
 
7542
+ // src/llm/openai-compatible.ts
7543
+ init_esm_shims();
7544
+ var DEFAULT_TIMEOUT_S = 60;
7545
+ var OpenAICompatibleLLMProvider = class {
7546
+ /**
7547
+ * Stable pricing/dashboard key — read by stream-handler/metrics. Typed as
7548
+ * ``string`` (not the narrowed literal) so the Hermes / OpenClaw presets can
7549
+ * override it with their own key while still extending this class.
7550
+ */
7551
+ static providerKey = "openai_compatible";
7552
+ /** Resolved bearer; undefined for keyless gateways. */
7553
+ apiKey;
7554
+ model;
7555
+ baseUrl;
7556
+ timeoutMs;
7557
+ extraHeaders;
7558
+ sessionUserPrefix;
7559
+ sessionIdHeader;
7560
+ sessionIdPrefix;
7561
+ sessionKeyHeader;
7562
+ sessionKey;
7563
+ temperature;
7564
+ maxTokens;
7565
+ responseFormat;
7566
+ parallelToolCalls;
7567
+ toolChoice;
7568
+ seed;
7569
+ topP;
7570
+ frequencyPenalty;
7571
+ presencePenalty;
7572
+ stop;
7573
+ constructor(options) {
7574
+ if (!options.baseUrl) {
7575
+ throw new Error(
7576
+ 'OpenAICompatibleLLMProvider requires a baseUrl (e.g. "http://127.0.0.1:11434/v1").'
7577
+ );
7578
+ }
7579
+ if (!options.model) {
7580
+ throw new Error("OpenAICompatibleLLMProvider requires a model.");
7581
+ }
7582
+ this.apiKey = options.apiKey ?? (options.apiKeyEnv ? process.env[options.apiKeyEnv] : void 0);
7583
+ this.model = options.model;
7584
+ this.baseUrl = options.baseUrl;
7585
+ this.timeoutMs = (options.timeout ?? DEFAULT_TIMEOUT_S) * 1e3;
7586
+ this.extraHeaders = options.extraHeaders;
7587
+ this.sessionUserPrefix = options.sessionUserPrefix;
7588
+ this.sessionIdHeader = options.sessionIdHeader;
7589
+ this.sessionIdPrefix = options.sessionIdPrefix;
7590
+ this.sessionKeyHeader = options.sessionKeyHeader;
7591
+ this.sessionKey = options.sessionKey;
7592
+ this.temperature = options.temperature;
7593
+ this.maxTokens = options.maxTokens;
7594
+ this.responseFormat = options.responseFormat;
7595
+ this.parallelToolCalls = options.parallelToolCalls;
7596
+ this.toolChoice = options.toolChoice;
7597
+ this.seed = options.seed;
7598
+ this.topP = options.topP;
7599
+ this.frequencyPenalty = options.frequencyPenalty;
7600
+ this.presencePenalty = options.presencePenalty;
7601
+ this.stop = options.stop;
7602
+ }
7603
+ /**
7604
+ * Assemble the request headers. ``User-Agent`` is set first so any
7605
+ * ``extraHeaders`` (and the per-call session headers) layer on top without
7606
+ * silently dropping the SDK attribution, and the ``Authorization`` header is
7607
+ * only added when a key is present (keyless gateways omit it).
7608
+ *
7609
+ * The two session headers are emitted INDEPENDENTLY, each gated on its own
7610
+ * config (decoupled from ``sessionUserPrefix`` and from each other):
7611
+ * - ``sessionIdHeader`` (+ ``callId``) → ``` `${sessionIdPrefix}${callId}` ```
7612
+ * - ``sessionKeyHeader`` (+ ``sessionKey``) → the static ``sessionKey`` value.
7613
+ * ``sessionKey`` is a credential-grade memory scope and is never logged.
7614
+ */
7615
+ buildHeaders(callId) {
7616
+ const headers = {
7617
+ "Content-Type": "application/json",
7618
+ "User-Agent": `getpatter/${VERSION}`,
7619
+ ...this.extraHeaders ?? {}
7620
+ };
7621
+ if (this.apiKey) {
7622
+ headers.Authorization = `Bearer ${this.apiKey}`;
7623
+ }
7624
+ if (this.sessionIdHeader && callId) {
7625
+ headers[this.sessionIdHeader] = `${this.sessionIdPrefix ?? ""}${callId}`;
7626
+ }
7627
+ if (this.sessionKeyHeader && this.sessionKey) {
7628
+ headers[this.sessionKeyHeader] = this.sessionKey;
7629
+ }
7630
+ return headers;
7631
+ }
7632
+ /**
7633
+ * Pre-call DNS / TLS warmup for the configured endpoint. Best-effort:
7634
+ * 5 s timeout, all exceptions swallowed at debug level. The ``Authorization``
7635
+ * header is only sent when a key is present so the operator-grade bearer is
7636
+ * never echoed for keyless gateways (and the key is never logged).
7637
+ */
7638
+ async warmup() {
7639
+ try {
7640
+ const headers = {};
7641
+ if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
7642
+ await fetch(`${this.baseUrl}/models`, {
7643
+ method: "GET",
7644
+ headers,
7645
+ signal: AbortSignal.timeout(5e3)
7646
+ });
7647
+ } catch (err) {
7648
+ getLogger().debug(
7649
+ `OpenAI-compatible LLM warmup failed (best-effort): ${String(err)}`
7650
+ );
7651
+ }
7652
+ }
7653
+ /**
7654
+ * Build the request body. Mirrors the base OpenAI provider's sampling-kwarg
7655
+ * assembly and additionally sets ``user`` for session continuity when
7656
+ * ``sessionUserPrefix`` is set AND a ``callId`` is available — so the default
7657
+ * (prefix unset) behaviour is byte-identical to the base provider.
7658
+ */
7659
+ buildBody(messages, tools, callId) {
7660
+ const body = {
7661
+ model: this.model,
7662
+ messages,
7663
+ stream: true,
7664
+ stream_options: { include_usage: true }
7665
+ };
7666
+ if (this.temperature !== void 0) body.temperature = this.temperature;
7667
+ if (this.maxTokens !== void 0) body.max_completion_tokens = this.maxTokens;
7668
+ if (this.responseFormat !== void 0) body.response_format = this.responseFormat;
7669
+ if (this.parallelToolCalls !== void 0) body.parallel_tool_calls = this.parallelToolCalls;
7670
+ if (this.toolChoice !== void 0) body.tool_choice = this.toolChoice;
7671
+ if (this.seed !== void 0) body.seed = this.seed;
7672
+ if (this.topP !== void 0) body.top_p = this.topP;
7673
+ if (this.frequencyPenalty !== void 0) body.frequency_penalty = this.frequencyPenalty;
7674
+ if (this.presencePenalty !== void 0) body.presence_penalty = this.presencePenalty;
7675
+ if (this.stop !== void 0) body.stop = this.stop;
7676
+ if (tools) body.tools = tools;
7677
+ if (this.sessionUserPrefix !== void 0 && callId) {
7678
+ body.user = `${this.sessionUserPrefix}${callId}`;
7679
+ }
7680
+ return body;
7681
+ }
7682
+ /** Stream Patter-format LLM chunks from the configured chat completions API. */
7683
+ async *stream(messages, tools, opts) {
7684
+ const callId = opts?.callId;
7685
+ const body = this.buildBody(messages, tools, callId);
7686
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
7687
+ method: "POST",
7688
+ headers: this.buildHeaders(callId),
7689
+ body: JSON.stringify(body),
7690
+ signal: mergeAbortSignals(opts?.signal, AbortSignal.timeout(this.timeoutMs))
7691
+ });
7692
+ if (!response.ok) {
7693
+ const errText = await response.text();
7694
+ getLogger().error(
7695
+ `OpenAI-compatible API error: ${response.status} ${errText}`
7696
+ );
7697
+ throw new PatterConnectionError(
7698
+ `LLM API returned ${response.status}: ${errText.slice(0, 200)}`
7699
+ );
7700
+ }
7701
+ yield* parseOpenAISseStream(response);
7702
+ }
7703
+ };
7704
+ var LLM6 = class extends OpenAICompatibleLLMProvider {
7705
+ static providerKey = "openai_compatible";
7706
+ };
7707
+
7708
+ // src/llm/hermes.ts
7709
+ init_esm_shims();
7710
+ var BASE_URL = "http://127.0.0.1:8642/v1";
7711
+ var DEFAULT_MODEL5 = "hermes-agent";
7712
+ var API_KEY_ENV = "API_SERVER_KEY";
7713
+ var MODEL_ENV = "API_SERVER_MODEL_NAME";
7714
+ var SESSION_USER_PREFIX = "patter-call-";
7715
+ var SESSION_ID_HEADER = "X-Hermes-Session-Id";
7716
+ var SESSION_ID_PREFIX = "patter-call-";
7717
+ var SESSION_KEY_HEADER = "X-Hermes-Session-Key";
7718
+ var DEFAULT_TIMEOUT_S2 = 120;
7719
+ var LLM7 = class extends OpenAICompatibleLLMProvider {
7720
+ static providerKey = "hermes";
7721
+ constructor(opts = {}) {
7722
+ const model = opts.model ?? process.env[MODEL_ENV] ?? DEFAULT_MODEL5;
7723
+ const options = {
7724
+ apiKey: opts.apiKey,
7725
+ apiKeyEnv: API_KEY_ENV,
7726
+ baseUrl: opts.baseUrl ?? BASE_URL,
7727
+ model,
7728
+ timeout: opts.timeout ?? DEFAULT_TIMEOUT_S2,
7729
+ sessionUserPrefix: SESSION_USER_PREFIX,
7730
+ sessionIdHeader: SESSION_ID_HEADER,
7731
+ sessionIdPrefix: SESSION_ID_PREFIX,
7732
+ sessionKeyHeader: SESSION_KEY_HEADER,
7733
+ sessionKey: opts.sessionKey,
7734
+ extraHeaders: opts.extraHeaders,
7735
+ temperature: opts.temperature,
7736
+ maxTokens: opts.maxTokens,
7737
+ responseFormat: opts.responseFormat,
7738
+ parallelToolCalls: opts.parallelToolCalls,
7739
+ toolChoice: opts.toolChoice,
7740
+ seed: opts.seed,
7741
+ topP: opts.topP,
7742
+ frequencyPenalty: opts.frequencyPenalty,
7743
+ presencePenalty: opts.presencePenalty,
7744
+ stop: opts.stop
7745
+ };
7746
+ super(options);
7747
+ }
7748
+ };
7749
+
7750
+ // src/llm/openclaw.ts
7751
+ init_esm_shims();
7752
+ var BASE_URL2 = "http://127.0.0.1:18789/v1";
7753
+ var API_KEY_ENV2 = "OPENCLAW_API_KEY";
7754
+ var SESSION_HEADER = "x-openclaw-session-key";
7755
+ var SESSION_USER_PREFIX2 = "patter-call-";
7756
+ var DEFAULT_TIMEOUT_S3 = 120;
7757
+ var OPENCLAW_AGENT_RE = /^[A-Za-z0-9._:/-]+$/;
7758
+ var LLM8 = class extends OpenAICompatibleLLMProvider {
7759
+ static providerKey = "openclaw";
7760
+ constructor(opts) {
7761
+ const agent = opts?.agent;
7762
+ if (!agent || !OPENCLAW_AGENT_RE.test(agent)) {
7763
+ throw new Error(
7764
+ `Invalid OpenClaw agent id: ${JSON.stringify(agent)}. Allowed characters: letters, digits, dot, underscore, colon, slash, dash.`
7765
+ );
7766
+ }
7767
+ const model = agent.includes("/") || agent.includes(":") ? agent : `openclaw/${agent}`;
7768
+ const options = {
7769
+ apiKey: opts.apiKey,
7770
+ apiKeyEnv: API_KEY_ENV2,
7771
+ baseUrl: opts.baseUrl ?? BASE_URL2,
7772
+ model,
7773
+ timeout: opts.timeout ?? DEFAULT_TIMEOUT_S3,
7774
+ sessionUserPrefix: SESSION_USER_PREFIX2,
7775
+ // Wire-identical to the prior behaviour: header value is the raw call id
7776
+ // (empty prefix), and OpenClaw's gateway also derives the session from
7777
+ // the ``user`` field above. No separate memory-scope header.
7778
+ sessionIdHeader: SESSION_HEADER,
7779
+ sessionIdPrefix: "",
7780
+ extraHeaders: opts.extraHeaders,
7781
+ temperature: opts.temperature,
7782
+ maxTokens: opts.maxTokens,
7783
+ responseFormat: opts.responseFormat,
7784
+ parallelToolCalls: opts.parallelToolCalls,
7785
+ toolChoice: opts.toolChoice,
7786
+ seed: opts.seed,
7787
+ topP: opts.topP,
7788
+ frequencyPenalty: opts.frequencyPenalty,
7789
+ presencePenalty: opts.presencePenalty,
7790
+ stop: opts.stop
7791
+ };
7792
+ super(options);
7793
+ }
7794
+ };
7795
+
7381
7796
  // src/providers/deepfilternet-filter.ts
7382
7797
  init_esm_shims();
7383
7798
  function log() {
@@ -7409,6 +7824,57 @@ function float32ToPcm16(samples) {
7409
7824
  }
7410
7825
  return out;
7411
7826
  }
7827
+ var ArbitraryResampler = class {
7828
+ srcRate;
7829
+ dstRate;
7830
+ phase = 0;
7831
+ // fractional position into the current chunk
7832
+ lastSample = 0;
7833
+ // last input sample from the previous chunk
7834
+ hasHistory = false;
7835
+ constructor(srcRate, dstRate) {
7836
+ this.srcRate = srcRate;
7837
+ this.dstRate = dstRate;
7838
+ }
7839
+ /** Process a chunk of PCM16-LE mono audio and return resampled PCM16-LE. */
7840
+ process(pcm) {
7841
+ const sampleCount = Math.floor(pcm.length / 2);
7842
+ if (sampleCount === 0) return Buffer.alloc(0);
7843
+ const step = this.srcRate / this.dstRate;
7844
+ const outArr = [];
7845
+ let phase = this.phase;
7846
+ while (true) {
7847
+ const idx = Math.floor(phase);
7848
+ if (idx >= sampleCount) break;
7849
+ const frac = phase - idx;
7850
+ let s0;
7851
+ let s1;
7852
+ if (idx < 0) {
7853
+ s0 = this.hasHistory ? this.lastSample : 0;
7854
+ s1 = pcm.readInt16LE(0);
7855
+ } else {
7856
+ s0 = pcm.readInt16LE(idx * 2);
7857
+ s1 = idx + 1 < sampleCount ? pcm.readInt16LE((idx + 1) * 2) : s0;
7858
+ }
7859
+ const interp = Math.round(s0 + (s1 - s0) * frac);
7860
+ outArr.push(Math.max(-32768, Math.min(32767, interp)));
7861
+ phase += step;
7862
+ }
7863
+ this.lastSample = pcm.readInt16LE((sampleCount - 1) * 2);
7864
+ this.hasHistory = true;
7865
+ this.phase = phase - sampleCount;
7866
+ const out = Buffer.alloc(outArr.length * 2);
7867
+ for (let j = 0; j < outArr.length; j++) out.writeInt16LE(outArr[j], j * 2);
7868
+ return out;
7869
+ }
7870
+ /** Flush any buffered state and reset. Returns any remaining tail output. */
7871
+ flush() {
7872
+ this.phase = 0;
7873
+ this.lastSample = 0;
7874
+ this.hasHistory = false;
7875
+ return Buffer.alloc(0);
7876
+ }
7877
+ };
7412
7878
  var DeepFilterNetFilter = class {
7413
7879
  modelPath;
7414
7880
  silenceWarnings;
@@ -7416,8 +7882,9 @@ var DeepFilterNetFilter = class {
7416
7882
  ort = null;
7417
7883
  warned = false;
7418
7884
  closed = false;
7419
- // Fix 5: stateful resamplers for src_sr↔48k conversions so chunk-boundary
7885
+ // Stateful resamplers for src_sr↔48k conversions so chunk-boundary
7420
7886
  // samples are not discarded. Lazy-created and torn down on rate change.
7887
+ // Uses ArbitraryResampler which supports any integer rate pair.
7421
7888
  _resamplerSrcRate = null;
7422
7889
  _upsamplerInst = null;
7423
7890
  _downsamplerInst = null;
@@ -7475,8 +7942,8 @@ var DeepFilterNetFilter = class {
7475
7942
  try {
7476
7943
  if (this._resamplerSrcRate !== sampleRate) {
7477
7944
  this._resamplerSrcRate = sampleRate;
7478
- this._upsamplerInst = new StatefulResampler({ srcRate: sampleRate, dstRate: DEEPFILTERNET_SR });
7479
- this._downsamplerInst = new StatefulResampler({ srcRate: DEEPFILTERNET_SR, dstRate: sampleRate });
7945
+ this._upsamplerInst = new ArbitraryResampler(sampleRate, DEEPFILTERNET_SR);
7946
+ this._downsamplerInst = new ArbitraryResampler(DEEPFILTERNET_SR, sampleRate);
7480
7947
  }
7481
7948
  const samples = pcm16ToFloat32(pcmChunk);
7482
7949
  const pcm16Up = this._upsamplerInst.process(float32ToPcm16(new Float32Array(samples)));
@@ -7636,6 +8103,17 @@ var Tool = class {
7636
8103
  parameters;
7637
8104
  handler;
7638
8105
  webhookUrl;
8106
+ reassurance;
8107
+ /**
8108
+ * Per-tool execution timeout in milliseconds. `undefined` uses the
8109
+ * executor default (10 000 ms). Mirrors Python `timeout_s`.
8110
+ */
8111
+ timeoutMs;
8112
+ /**
8113
+ * Enable OpenAI strict mode for this tool's function schema. Off by
8114
+ * default. Mirrors Python `strict` on `Tool`.
8115
+ */
8116
+ strict;
7639
8117
  constructor(opts) {
7640
8118
  if (!opts.name) {
7641
8119
  throw new Error("Tool requires a non-empty name.");
@@ -7653,6 +8131,9 @@ var Tool = class {
7653
8131
  this.parameters = opts.parameters ?? { type: "object", properties: {} };
7654
8132
  if (hasHandler) this.handler = opts.handler;
7655
8133
  if (hasWebhook) this.webhookUrl = opts.webhookUrl;
8134
+ if (opts.reassurance !== void 0) this.reassurance = opts.reassurance;
8135
+ if (opts.timeoutMs !== void 0) this.timeoutMs = opts.timeoutMs;
8136
+ if (opts.strict !== void 0) this.strict = opts.strict;
7656
8137
  }
7657
8138
  };
7658
8139
  function tool(opts) {
@@ -7811,7 +8292,6 @@ var ChatContext = class _ChatContext {
7811
8292
  // src/services/ivr.ts
7812
8293
  init_esm_shims();
7813
8294
  var DTMF_EVENTS = [
7814
- "0",
7815
8295
  "1",
7816
8296
  "2",
7817
8297
  "3",
@@ -7821,6 +8301,7 @@ var DTMF_EVENTS = [
7821
8301
  "7",
7822
8302
  "8",
7823
8303
  "9",
8304
+ "0",
7824
8305
  "*",
7825
8306
  "#",
7826
8307
  "A",
@@ -8497,18 +8978,24 @@ var TelnyxAdapter = class {
8497
8978
  "/number_orders",
8498
8979
  orderBody
8499
8980
  );
8500
- const orderId = order.data?.id ?? "";
8981
+ const orderId = order.data?.id;
8982
+ if (!orderId) throw new Error("TelnyxAdapter: /number_orders returned no order id");
8501
8983
  return { phoneNumber: chosen, orderId };
8502
8984
  }
8503
8985
  /** Attach a number to a Call Control Application. */
8504
8986
  async configureNumber(phoneNumber, opts) {
8505
8987
  if (!phoneNumber) throw new Error("TelnyxAdapter: phoneNumber is required");
8506
8988
  if (!opts.connectionId) throw new Error("TelnyxAdapter: connectionId is required");
8507
- await this.request(
8508
- "PATCH",
8509
- `/phone_numbers/${encodeURIComponent(phoneNumber)}/voice`,
8510
- { connection_id: opts.connectionId, tech_prefix_enabled: false }
8511
- );
8989
+ try {
8990
+ await this.request(
8991
+ "PATCH",
8992
+ `/phone_numbers/${encodeURIComponent(phoneNumber)}/voice`,
8993
+ { connection_id: opts.connectionId, tech_prefix_enabled: false }
8994
+ );
8995
+ } catch (err) {
8996
+ const status = err instanceof Error ? err.message.replace(/\+\d{7,15}/g, "[REDACTED]") : String(err);
8997
+ throw new Error(`TelnyxAdapter: configureNumber failed: ${status}`);
8998
+ }
8512
8999
  }
8513
9000
  /**
8514
9001
  * Place an outbound call on the Call Control Application.
@@ -8612,7 +9099,7 @@ var TelnyxSTT = class {
8612
9099
  /** Stable pricing/dashboard key — read by stream-handler/metrics. */
8613
9100
  static providerKey = "telnyx_stt";
8614
9101
  ws = null;
8615
- callbacks = [];
9102
+ callbacks = /* @__PURE__ */ new Set();
8616
9103
  headerSent = false;
8617
9104
  /** Open the streaming WebSocket and arm message handlers. */
8618
9105
  async connect() {
@@ -8668,14 +9155,13 @@ var TelnyxSTT = class {
8668
9155
  }
8669
9156
  this.ws.send(audio);
8670
9157
  }
8671
- /** Register a transcript listener (max 10 concurrent listeners). */
9158
+ /** Register a transcript listener. */
8672
9159
  onTranscript(callback) {
8673
- if (this.callbacks.length >= 10) {
8674
- getLogger().warn("TelnyxSTT: maximum of 10 onTranscript callbacks reached; replacing the last callback.");
8675
- this.callbacks[this.callbacks.length - 1] = callback;
8676
- return;
8677
- }
8678
- this.callbacks.push(callback);
9160
+ this.callbacks.add(callback);
9161
+ }
9162
+ /** Unregister a previously-registered transcript listener. */
9163
+ offTranscript(callback) {
9164
+ this.callbacks.delete(callback);
8679
9165
  }
8680
9166
  /** Close the streaming WebSocket. */
8681
9167
  close() {
@@ -8686,6 +9172,7 @@ var TelnyxSTT = class {
8686
9172
  }
8687
9173
  this.ws = null;
8688
9174
  }
9175
+ this.headerSent = false;
8689
9176
  }
8690
9177
  };
8691
9178
 
@@ -8706,6 +9193,7 @@ var TelnyxTTSSampleRate = {
8706
9193
  HZ_24000: 24e3
8707
9194
  };
8708
9195
  var DEFAULT_VOICE = TelnyxTTSVoice.NATURAL_HD_ASTRA;
9196
+ var FRAME_TIMEOUT_MS2 = 3e4;
8709
9197
  var TelnyxTTS = class {
8710
9198
  constructor(apiKey, voice = DEFAULT_VOICE, baseUrl = TELNYX_TTS_WS_URL) {
8711
9199
  this.apiKey = apiKey;
@@ -8733,69 +9221,83 @@ var TelnyxTTS = class {
8733
9221
  */
8734
9222
  async *synthesizeStream(text) {
8735
9223
  const url = `${this.baseUrl}?voice=${encodeURIComponent(this.voice)}`;
8736
- const ws = new WebSocket8(url, {
8737
- headers: { Authorization: `Bearer ${this.apiKey}` }
8738
- });
8739
- await new Promise((resolve, reject) => {
8740
- const timer = setTimeout(() => reject(new Error("Telnyx TTS connect timeout")), 1e4);
8741
- ws.once("open", () => {
8742
- clearTimeout(timer);
8743
- resolve();
9224
+ let ws = null;
9225
+ try {
9226
+ let push2 = function(item) {
9227
+ const w = waiters.shift();
9228
+ if (w) {
9229
+ w(item);
9230
+ } else {
9231
+ queue.push(item);
9232
+ }
9233
+ };
9234
+ var push = push2;
9235
+ ws = new WebSocket8(url, {
9236
+ headers: { Authorization: `Bearer ${this.apiKey}` }
8744
9237
  });
8745
- ws.once("error", (err) => {
8746
- clearTimeout(timer);
8747
- reject(err);
9238
+ await new Promise((resolve, reject) => {
9239
+ const timer = setTimeout(() => reject(new Error("Telnyx TTS connect timeout")), 1e4);
9240
+ ws.once("open", () => {
9241
+ clearTimeout(timer);
9242
+ resolve();
9243
+ });
9244
+ ws.once("error", (err) => {
9245
+ clearTimeout(timer);
9246
+ reject(err);
9247
+ });
8748
9248
  });
8749
- });
8750
- const queue = [];
8751
- const waiters = [];
8752
- function push(item) {
8753
- const w = waiters.shift();
8754
- if (w) {
8755
- w(item);
8756
- } else {
8757
- queue.push(item);
8758
- }
8759
- }
8760
- ws.on("message", (raw) => {
8761
- let data;
8762
- try {
8763
- data = JSON.parse(raw.toString());
8764
- } catch {
8765
- getLogger().warn("TelnyxTTS: received invalid JSON");
8766
- return;
8767
- }
8768
- const audioB64 = data.audio;
8769
- if (!audioB64) return;
8770
- try {
8771
- const audioBytes = Buffer.from(audioB64, "base64");
8772
- if (audioBytes.length > 0) {
8773
- push(audioBytes);
9249
+ const queue = [];
9250
+ const waiters = [];
9251
+ ws.on("message", (raw) => {
9252
+ let data;
9253
+ try {
9254
+ data = JSON.parse(raw.toString());
9255
+ } catch {
9256
+ getLogger().warn("TelnyxTTS: received invalid JSON");
9257
+ return;
8774
9258
  }
8775
- } catch {
8776
- }
8777
- });
8778
- ws.on("close", () => {
8779
- push(null);
8780
- });
8781
- ws.on("error", (err) => {
8782
- push({ error: err instanceof Error ? err : new Error(String(err)) });
8783
- });
8784
- ws.send(JSON.stringify({ text: " " }));
8785
- ws.send(JSON.stringify({ text }));
8786
- ws.send(JSON.stringify({ text: "" }));
8787
- try {
9259
+ const audioB64 = data.audio;
9260
+ if (!audioB64) return;
9261
+ try {
9262
+ const audioBytes = Buffer.from(audioB64, "base64");
9263
+ if (audioBytes.length > 0) {
9264
+ push2(audioBytes);
9265
+ }
9266
+ } catch {
9267
+ }
9268
+ });
9269
+ ws.on("close", () => {
9270
+ push2(null);
9271
+ });
9272
+ ws.on("error", (err) => {
9273
+ push2({ error: err instanceof Error ? err : new Error(String(err)) });
9274
+ });
9275
+ ws.send(JSON.stringify({ text: " " }));
9276
+ ws.send(JSON.stringify({ text }));
9277
+ ws.send(JSON.stringify({ text: "" }));
8788
9278
  while (true) {
8789
- const item = queue.length > 0 ? queue.shift() : await new Promise((resolve) => waiters.push(resolve));
9279
+ let frameTimer;
9280
+ const item = queue.length > 0 ? queue.shift() : await Promise.race([
9281
+ new Promise((resolve) => waiters.push(resolve)),
9282
+ new Promise((_, reject) => {
9283
+ frameTimer = setTimeout(
9284
+ () => reject(new Error("Telnyx TTS frame timeout")),
9285
+ FRAME_TIMEOUT_MS2
9286
+ );
9287
+ })
9288
+ ]).finally(() => {
9289
+ if (frameTimer !== void 0) clearTimeout(frameTimer);
9290
+ });
8790
9291
  if (item === null) return;
8791
9292
  if (typeof item === "object" && "error" in item) throw item.error;
8792
9293
  yield item;
8793
9294
  }
8794
9295
  } finally {
8795
9296
  try {
8796
- ws.close();
9297
+ ws?.close();
8797
9298
  } catch {
8798
9299
  }
9300
+ ws?.removeAllListeners();
8799
9301
  }
8800
9302
  }
8801
9303
  };
@@ -8840,6 +9342,7 @@ export {
8840
9342
  LLM5 as GoogleLLM,
8841
9343
  LLM3 as GroqLLM,
8842
9344
  Guardrail,
9345
+ LLM7 as HermesLLM,
8843
9346
  IVRActivity,
8844
9347
  TTS7 as InworldTTS,
8845
9348
  KrispFrameDuration,
@@ -8850,6 +9353,8 @@ export {
8850
9353
  MetricsStore,
8851
9354
  MinWordsStrategy,
8852
9355
  Ngrok,
9356
+ LLM6 as OpenAICompatibleLLM,
9357
+ OpenAICompatibleLLMProvider,
8853
9358
  LLM as OpenAILLM,
8854
9359
  OpenAILLMProvider,
8855
9360
  Realtime as OpenAIRealtime,
@@ -8863,10 +9368,12 @@ export {
8863
9368
  STT3 as OpenAITranscribeSTT,
8864
9369
  OpenAITranscriptionModel,
8865
9370
  OpenAIVoice,
9371
+ LLM8 as OpenClawLLM,
8866
9372
  PRICING_LAST_UPDATED,
8867
9373
  PRICING_VERSION,
8868
9374
  PartialStreamError,
8869
9375
  Patter,
9376
+ PatterConfigError,
8870
9377
  PatterConnectionError,
8871
9378
  PatterError,
8872
9379
  PatterTool,
@@ -8954,6 +9461,8 @@ export {
8954
9461
  mulawToPcm16,
8955
9462
  notifyDashboard,
8956
9463
  openaiTts,
9464
+ openclawConsult,
9465
+ openclawPostCallNotifier,
8957
9466
  pcm16ToMulaw,
8958
9467
  resample16kTo8k,
8959
9468
  resample24kTo16k,