@cydm/pie 2.7.0 → 2.7.1

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.
@@ -475,6 +475,7 @@ function calculateCost(model, usage) {
475
475
  const cacheRead = model.cost.cacheRead / 1e6 * usage.cacheRead;
476
476
  const cacheWrite = model.cost.cacheWrite / 1e6 * usage.cacheWrite;
477
477
  const cacheWriteLongTokens = usage.cacheWriteLong ?? 0;
478
+ const hasExplicitLongRate = model.cost.cacheWriteLong !== void 0;
478
479
  const cacheWriteLongRate = model.cost.cacheWriteLong ?? model.cost.input * 2;
479
480
  const cacheWriteLong = cacheWriteLongTokens > 0 ? cacheWriteLongRate / 1e6 * cacheWriteLongTokens : 0;
480
481
  return {
@@ -482,7 +483,10 @@ function calculateCost(model, usage) {
482
483
  output,
483
484
  cacheRead,
484
485
  cacheWrite,
485
- ...cacheWriteLongTokens > 0 ? { cacheWriteLong } : {},
486
+ ...cacheWriteLongTokens > 0 ? {
487
+ cacheWriteLong,
488
+ ...hasExplicitLongRate ? {} : { cacheWriteLongEstimated: true }
489
+ } : {},
486
490
  total: input + output + cacheRead + cacheWrite + cacheWriteLong
487
491
  };
488
492
  }
@@ -1872,6 +1876,8 @@ function getHttpAgent() {
1872
1876
  return globalHttpAgent;
1873
1877
  }
1874
1878
  var NodeHttpClient = class {
1879
+ /** The Node client never re-sends a request: one `request()` call is one submission. */
1880
+ transportCapabilities = { singleAttempt: true };
1875
1881
  async request(url, options = {}) {
1876
1882
  const requestStartTime = Date.now();
1877
1883
  const requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
@@ -2194,6 +2200,8 @@ var NodeHttpClient = class {
2194
2200
  }
2195
2201
  };
2196
2202
  var PuerTSHttpClient = class {
2203
+ /** No internal re-send: each call starts exactly one bridge request. */
2204
+ transportCapabilities = { singleAttempt: true };
2197
2205
  async request(url, options = {}) {
2198
2206
  logHttp("DEBUG", `PuerTS Request started`, {
2199
2207
  url: url.slice(0, 100),
@@ -3316,6 +3324,9 @@ function getHttpClient() {
3316
3324
  }
3317
3325
 
3318
3326
  // ../../packages/ai/src/utils/http.ts
3327
+ function getHttpTransport() {
3328
+ return getHttpClient();
3329
+ }
3319
3330
  function httpRequest(url, options = {}) {
3320
3331
  const client = getHttpClient();
3321
3332
  return client.request(url, options);
@@ -3359,6 +3370,7 @@ function buildBaseOptions(model, options, apiKey) {
3359
3370
  timeoutMs: options?.timeoutMs,
3360
3371
  onPayload: options?.onPayload,
3361
3372
  onTransportDiagnostic: options?.onTransportDiagnostic,
3373
+ onTransportLifecycle: options?.onTransportLifecycle,
3362
3374
  maxRetryDelayMs: options?.maxRetryDelayMs,
3363
3375
  metadata: options?.metadata
3364
3376
  };
@@ -4411,6 +4423,35 @@ import Anthropic from "@anthropic-ai/sdk";
4411
4423
  function sanitizeSurrogates(text) {
4412
4424
  return text.replace(/[\uD800-\uDFFF]/g, "");
4413
4425
  }
4426
+ function applyAnthropicUsageUpdate(model, usage, payload) {
4427
+ if (payload.input_tokens != null) usage.input = Math.max(0, payload.input_tokens);
4428
+ if (payload.output_tokens != null) usage.output = Math.max(0, payload.output_tokens);
4429
+ if (payload.cache_read_input_tokens != null) usage.cacheRead = Math.max(0, payload.cache_read_input_tokens);
4430
+ const reportedWriteTotal = payload.cache_creation_input_tokens;
4431
+ const breakdown = payload.cache_creation ?? void 0;
4432
+ const breakdownTotal = (breakdown?.ephemeral_5m_input_tokens ?? 0) + (breakdown?.ephemeral_1h_input_tokens ?? 0);
4433
+ if (reportedWriteTotal == null && breakdown !== void 0 && breakdownTotal === 0) {
4434
+ usage.cacheWrite = Math.max(0, usage.cacheWrite);
4435
+ } else if (reportedWriteTotal != null || breakdown !== void 0) {
4436
+ const previousLong = usage.cacheWriteLong ?? 0;
4437
+ const previousTotal = usage.cacheWrite + previousLong;
4438
+ const writeTotal = Math.max(
4439
+ 0,
4440
+ reportedWriteTotal != null ? reportedWriteTotal : breakdownTotal > 0 ? breakdownTotal : previousTotal
4441
+ );
4442
+ const longTokens = breakdown?.ephemeral_1h_input_tokens ?? 0;
4443
+ if (longTokens > 0) {
4444
+ const long = Math.min(longTokens, writeTotal);
4445
+ usage.cacheWriteLong = long;
4446
+ usage.cacheWrite = writeTotal - long;
4447
+ } else {
4448
+ delete usage.cacheWriteLong;
4449
+ usage.cacheWrite = writeTotal;
4450
+ }
4451
+ }
4452
+ usage.totalTokens = usage.input + usage.output + usage.cacheRead + usage.cacheWrite + (usage.cacheWriteLong ?? 0);
4453
+ usage.cost = calculateCost(model, usage);
4454
+ }
4414
4455
  var ANTHROPIC_PROVIDER_HEADERS = {
4415
4456
  accept: "application/json",
4416
4457
  "anthropic-dangerous-direct-browser-access": "true",
@@ -4696,17 +4737,7 @@ var streamAnthropic = (model, context, options) => {
4696
4737
  const blocks = output.content;
4697
4738
  for await (const event of anthropicStream) {
4698
4739
  if (event.type === "message_start") {
4699
- output.usage.input = event.message.usage.input_tokens || 0;
4700
- output.usage.output = event.message.usage.output_tokens || 0;
4701
- output.usage.cacheRead = event.message.usage.cache_read_input_tokens || 0;
4702
- output.usage.cacheWrite = event.message.usage.cache_creation_input_tokens || 0;
4703
- const creation = event.message.usage.cache_creation;
4704
- if (creation?.ephemeral_1h_input_tokens) {
4705
- output.usage.cacheWriteLong = creation.ephemeral_1h_input_tokens;
4706
- output.usage.cacheWrite = Math.max(0, output.usage.cacheWrite - creation.ephemeral_1h_input_tokens);
4707
- }
4708
- output.usage.totalTokens = output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite + (output.usage.cacheWriteLong ?? 0);
4709
- calculateCost(model, output.usage);
4740
+ applyAnthropicUsageUpdate(model, output.usage, event.message.usage);
4710
4741
  } else if (event.type === "content_block_start") {
4711
4742
  if (event.content_block.type === "text") {
4712
4743
  const block = { type: "text", text: "", index: event.index };
@@ -4810,19 +4841,7 @@ var streamAnthropic = (model, context, options) => {
4810
4841
  if (event.delta.stop_reason) {
4811
4842
  output.stopReason = mapStopReason(event.delta.stop_reason);
4812
4843
  }
4813
- if (event.usage.input_tokens != null) output.usage.input = event.usage.input_tokens;
4814
- if (event.usage.output_tokens != null) output.usage.output = event.usage.output_tokens;
4815
- if (event.usage.cache_read_input_tokens != null) output.usage.cacheRead = event.usage.cache_read_input_tokens;
4816
- if (event.usage.cache_creation_input_tokens != null) {
4817
- output.usage.cacheWrite = event.usage.cache_creation_input_tokens;
4818
- const creationDelta = event.usage.cache_creation;
4819
- if (creationDelta?.ephemeral_1h_input_tokens) {
4820
- output.usage.cacheWriteLong = creationDelta.ephemeral_1h_input_tokens;
4821
- output.usage.cacheWrite = Math.max(0, output.usage.cacheWrite - creationDelta.ephemeral_1h_input_tokens);
4822
- }
4823
- }
4824
- output.usage.totalTokens = output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite + (output.usage.cacheWriteLong ?? 0);
4825
- calculateCost(model, output.usage);
4844
+ applyAnthropicUsageUpdate(model, output.usage, event.usage);
4826
4845
  }
4827
4846
  }
4828
4847
  if (options?.signal?.aborted) {
@@ -5567,6 +5586,22 @@ function reportSubmission(options, body, details) {
5567
5586
  } catch {
5568
5587
  }
5569
5588
  }
5589
+ function reportLifecycle(options, event) {
5590
+ if (!options?.onTransportLifecycle) return;
5591
+ try {
5592
+ options.onTransportLifecycle({ ...event });
5593
+ } catch {
5594
+ }
5595
+ }
5596
+ var localRequestCounter = 0;
5597
+ function nextLocalRequestId(prefix = "req") {
5598
+ localRequestCounter += 1;
5599
+ return `${prefix}-${localRequestCounter}-${Math.random().toString(36).slice(2, 8)}`;
5600
+ }
5601
+ function monotonicNow() {
5602
+ const perf = globalThis.performance;
5603
+ return typeof perf?.now === "function" ? perf.now() : Date.now();
5604
+ }
5570
5605
 
5571
5606
  // ../../packages/ai/src/providers/responses/responses-sse.ts
5572
5607
  var DEFAULT_RESPONSES_STREAM_IDLE_TIMEOUT_MS = 2 * 60 * 1e3;
@@ -5578,15 +5613,134 @@ function parseSSEDataLine2(line) {
5578
5613
  const data = trimmed.slice(5);
5579
5614
  return data.startsWith(" ") ? data.slice(1) : data;
5580
5615
  }
5616
+ function singleAttemptSupport() {
5617
+ let capabilities;
5618
+ try {
5619
+ capabilities = getHttpTransport().transportCapabilities;
5620
+ } catch {
5621
+ capabilities = void 0;
5622
+ }
5623
+ return capabilities?.singleAttempt === true ? "verified" : "unverified";
5624
+ }
5625
+ function assertSingleAttempt() {
5626
+ if (singleAttemptSupport() === "verified") return;
5627
+ throw new ResponsesProtocolError(
5628
+ "Responses SSE requires the platform HTTP adapter to declare transportCapabilities.singleAttempt; refusing to submit because a submitted generation must not be re-sent by the transport."
5629
+ );
5630
+ }
5631
+ function explicitBudget(value, field) {
5632
+ if (value === void 0) return void 0;
5633
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
5634
+ throw new ResponsesProtocolError(`Invalid ${field}: expected a finite positive number of milliseconds.`);
5635
+ }
5636
+ return value;
5637
+ }
5638
+ function resolveSSEBudgets(options) {
5639
+ const firstResponse = explicitBudget(options?.firstResponseTimeoutMs, "firstResponseTimeoutMs");
5640
+ const streamIdle = explicitBudget(options?.streamIdleTimeoutMs, "streamIdleTimeoutMs");
5641
+ const requestTimeoutMs = explicitBudget(options?.requestTimeoutMs, "requestTimeoutMs");
5642
+ explicitBudget(options?.connectionIdleTimeoutMs, "connectionIdleTimeoutMs");
5643
+ if (firstResponse !== void 0 || streamIdle !== void 0) {
5644
+ return {
5645
+ http: {
5646
+ firstByteTimeoutMs: firstResponse ?? DEFAULT_RESPONSES_FIRST_BYTE_TIMEOUT_MS,
5647
+ streamIdleTimeoutMs: streamIdle ?? DEFAULT_RESPONSES_STREAM_IDLE_TIMEOUT_MS
5648
+ },
5649
+ requestTimeoutMs
5650
+ };
5651
+ }
5652
+ if (options?.timeoutMs !== void 0) return { http: { timeoutMs: options.timeoutMs }, requestTimeoutMs };
5653
+ return {
5654
+ http: {
5655
+ firstByteTimeoutMs: DEFAULT_RESPONSES_FIRST_BYTE_TIMEOUT_MS,
5656
+ streamIdleTimeoutMs: DEFAULT_RESPONSES_STREAM_IDLE_TIMEOUT_MS
5657
+ },
5658
+ requestTimeoutMs
5659
+ };
5660
+ }
5581
5661
  function streamResponsesSSE(model, context, options) {
5582
5662
  const stream = new AssistantMessageEventStream();
5583
5663
  void (async () => {
5584
5664
  const reducer = new ResponsesEventReducer(model);
5665
+ const localRequestId = nextLocalRequestId("sse");
5666
+ const startedAt = monotonicNow();
5667
+ let lastReceiveAt = startedAt;
5668
+ let receivedMessageCount = 0;
5669
+ let sawModelContent = false;
5670
+ let submitted = false;
5671
+ let terminalAccepted = false;
5672
+ let phase = "request_start";
5673
+ let budgets = {};
5674
+ let totalTimer;
5675
+ let internalTimeout;
5676
+ let localFailure = false;
5677
+ const requestAbort = new AbortController();
5678
+ const onUserAbort = () => requestAbort.abort();
5679
+ const clearTotalTimer = () => {
5680
+ if (totalTimer !== void 0) {
5681
+ clearTimeout(totalTimer);
5682
+ totalTimer = void 0;
5683
+ }
5684
+ };
5685
+ const lifecycle = (extra = {}) => reportLifecycle(options, {
5686
+ ...extra,
5687
+ transport: "sse",
5688
+ localRequestId,
5689
+ requestGeneration: 1,
5690
+ reusedConnection: false,
5691
+ phase,
5692
+ requestAgeMs: Math.round(monotonicNow() - startedAt),
5693
+ lastReceiveAgeMs: Math.round(monotonicNow() - lastReceiveAt),
5694
+ budgets,
5695
+ receivedMessageCount,
5696
+ sawModelContent,
5697
+ submissionAttempted: submitted,
5698
+ terminalAccepted,
5699
+ singleAttempt: singleAttemptSupport()
5700
+ });
5585
5701
  try {
5702
+ let resolved;
5703
+ try {
5704
+ resolved = resolveSSEBudgets(options);
5705
+ } catch (error) {
5706
+ localFailure = true;
5707
+ throw error;
5708
+ }
5709
+ budgets = {
5710
+ firstResponseTimeoutMs: resolved.http.firstByteTimeoutMs ?? resolved.http.timeoutMs,
5711
+ streamIdleTimeoutMs: resolved.http.streamIdleTimeoutMs ?? resolved.http.timeoutMs,
5712
+ requestTimeoutMs: resolved.requestTimeoutMs
5713
+ };
5714
+ options?.signal?.addEventListener("abort", onUserAbort, { once: true });
5715
+ if (options?.signal?.aborted) requestAbort.abort();
5716
+ if (resolved.requestTimeoutMs !== void 0) {
5717
+ totalTimer = setTimeout(() => {
5718
+ totalTimer = void 0;
5719
+ if (terminalAccepted || requestAbort.signal.aborted) return;
5720
+ internalTimeout = "request_total";
5721
+ phase = "timeout";
5722
+ lifecycle({
5723
+ timeoutKind: "request_total",
5724
+ terminalAccepted: false,
5725
+ cancelSource: "internal_timeout",
5726
+ retryAllowed: !submitted,
5727
+ retryDecision: submitted ? "not_retryable_after_submission" : "not_submitted"
5728
+ });
5729
+ requestAbort.abort();
5730
+ }, resolved.requestTimeoutMs);
5731
+ totalTimer.unref?.();
5732
+ }
5733
+ lifecycle();
5586
5734
  const apiKey = options?.apiKey || getEnvApiKey(model.provider, model.apiKeyEnv) || "";
5587
5735
  if (!apiKey) {
5588
5736
  throw new Error(`No API key for provider: ${model.provider}`);
5589
5737
  }
5738
+ try {
5739
+ assertSingleAttempt();
5740
+ } catch (error) {
5741
+ localFailure = true;
5742
+ throw error;
5743
+ }
5590
5744
  const payload = buildResponsesSSEPayload(model, context, options);
5591
5745
  options?.onPayload?.(payload);
5592
5746
  const headers = {
@@ -5598,15 +5752,17 @@ function streamResponsesSSE(model, context, options) {
5598
5752
  };
5599
5753
  const body = JSON.stringify(payload);
5600
5754
  reportSubmission(options, body, { transport: "sse", reusedConnection: false, continuation: false, attempt: 1 });
5755
+ submitted = true;
5756
+ phase = "submission_attempted";
5757
+ lifecycle();
5601
5758
  const response = await httpRequest(`${model.baseUrl}/responses`, {
5602
5759
  method: "POST",
5603
5760
  headers,
5604
5761
  body,
5605
- signal: options?.signal,
5606
- ...options?.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {
5607
- firstByteTimeoutMs: DEFAULT_RESPONSES_FIRST_BYTE_TIMEOUT_MS,
5608
- streamIdleTimeoutMs: DEFAULT_RESPONSES_STREAM_IDLE_TIMEOUT_MS
5609
- },
5762
+ signal: requestAbort.signal,
5763
+ // The transport must not re-send a submitted generation.
5764
+ retryPolicy: "never",
5765
+ ...resolved.http,
5610
5766
  label: `${model.provider}/${model.id}`
5611
5767
  });
5612
5768
  if (!response.ok) {
@@ -5616,9 +5772,15 @@ function streamResponsesSSE(model, context, options) {
5616
5772
  if (!response.readSSELines) {
5617
5773
  throw new Error("SSE streaming not supported on this platform");
5618
5774
  }
5619
- for await (const line of response.readSSELines(options?.signal)) {
5775
+ for await (const line of response.readSSELines(requestAbort.signal)) {
5776
+ receivedMessageCount += 1;
5777
+ lastReceiveAt = monotonicNow();
5620
5778
  const data = parseSSEDataLine2(line);
5621
5779
  if (data === null) continue;
5780
+ if (phase === "submission_attempted") {
5781
+ phase = "first_application_message";
5782
+ lifecycle();
5783
+ }
5622
5784
  if (data === "[DONE]") {
5623
5785
  continue;
5624
5786
  }
@@ -5632,35 +5794,51 @@ function streamResponsesSSE(model, context, options) {
5632
5794
  if (!isResponsesEventLike(event)) {
5633
5795
  throw new ResponsesProtocolError("Responses SSE payload is not an event object with a string type.");
5634
5796
  }
5797
+ if (typeof event.type === "string" && (event.type.startsWith("response.output") || event.type.startsWith("response.content") || event.type.startsWith("response.reasoning"))) {
5798
+ sawModelContent = true;
5799
+ }
5635
5800
  for (const assistantEvent of reducer.feed(event)) {
5636
5801
  stream.push(assistantEvent);
5637
5802
  }
5638
- if (reducer.isTerminal) break;
5803
+ if (reducer.isTerminal) {
5804
+ terminalAccepted = true;
5805
+ phase = "terminal_accepted";
5806
+ clearTotalTimer();
5807
+ lifecycle({ retryAllowed: reducer.message.retryable !== false, retryDecision: "terminal" });
5808
+ break;
5809
+ }
5639
5810
  }
5640
5811
  if (reducer.isTerminal) {
5641
5812
  stream.end();
5642
5813
  return;
5643
5814
  }
5644
- if (options?.signal?.aborted) {
5645
- throw new Error("Request was aborted");
5646
- }
5647
- if (!reducer.isTerminal) {
5648
- throw reducer.finishWithoutTerminal();
5815
+ if (requestAbort.signal.aborted) {
5816
+ throw new Error(internalTimeout ? `Responses request timeout (${internalTimeout})` : "Request was aborted");
5649
5817
  }
5650
- if (reducer.message.stopReason === "error" || reducer.message.stopReason === "aborted") {
5651
- throw new Error(reducer.message.errorMessage ?? "An unknown error occurred");
5652
- }
5653
- stream.end();
5818
+ throw reducer.finishWithoutTerminal();
5654
5819
  } catch (error) {
5655
5820
  if (reducer.isTerminal) {
5656
5821
  stream.end();
5657
5822
  return;
5658
5823
  }
5659
5824
  const message = reducer.message;
5660
- message.stopReason = options?.signal?.aborted ? "aborted" : "error";
5661
- message.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
5825
+ const userAborted = options?.signal?.aborted === true;
5826
+ message.stopReason = userAborted ? "aborted" : "error";
5827
+ message.errorMessage = internalTimeout ? `Responses request timeout (${internalTimeout}) after ${Math.round(monotonicNow() - startedAt)}ms` : error instanceof Error ? error.message : JSON.stringify(error);
5828
+ if (submitted || localFailure) message.retryable = false;
5829
+ phase = "cleanup";
5830
+ lifecycle({
5831
+ timeoutKind: internalTimeout,
5832
+ retryAllowed: !submitted && !userAborted && !localFailure,
5833
+ retryDecision: submitted ? "not_retryable_after_submission" : localFailure ? "local_configuration_error" : "not_submitted",
5834
+ cancelSource: userAborted ? "user" : internalTimeout ? "internal_timeout" : "none",
5835
+ cleanupOutcome: "request_abandoned"
5836
+ });
5662
5837
  stream.push({ type: "error", reason: message.stopReason, error: message });
5663
5838
  stream.end();
5839
+ } finally {
5840
+ clearTotalTimer();
5841
+ options?.signal?.removeEventListener("abort", onUserAbort);
5664
5842
  }
5665
5843
  })();
5666
5844
  return stream;
@@ -5669,26 +5847,77 @@ function streamResponsesSSE(model, context, options) {
5669
5847
  // ../../packages/ai/src/providers/responses/responses-ws.ts
5670
5848
  var owners = /* @__PURE__ */ new WeakMap();
5671
5849
  var MAX_ANCHOR_BYTES = 1024 * 1024;
5850
+ var DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
5851
+ var DEFAULT_FIRST_RESPONSE_TIMEOUT_MS = 2 * 60 * 1e3;
5852
+ var DEFAULT_STREAM_IDLE_TIMEOUT_MS = 2 * 60 * 1e3;
5853
+ var DEFAULT_CONNECTION_IDLE_TIMEOUT_MS = 2 * 60 * 1e3;
5854
+ var ITERATOR_CLEANUP_TIMEOUT_MS = 1e3;
5855
+ function explicitBudget2(value, field) {
5856
+ if (value === void 0) return void 0;
5857
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
5858
+ throw new ResponsesProtocolError(`Invalid ${field}: expected a finite positive number of milliseconds.`);
5859
+ }
5860
+ return value;
5861
+ }
5862
+ function resolveBudgets(options) {
5863
+ const firstResponse = explicitBudget2(options?.firstResponseTimeoutMs, "firstResponseTimeoutMs");
5864
+ const streamIdle = explicitBudget2(options?.streamIdleTimeoutMs, "streamIdleTimeoutMs");
5865
+ const requestTimeout = explicitBudget2(options?.requestTimeoutMs, "requestTimeoutMs");
5866
+ const connectionIdle = explicitBudget2(options?.connectionIdleTimeoutMs, "connectionIdleTimeoutMs");
5867
+ const legacy = options?.timeoutMs !== void 0 && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : void 0;
5868
+ return {
5869
+ connectTimeoutMs: legacy ?? DEFAULT_CONNECT_TIMEOUT_MS,
5870
+ firstResponseTimeoutMs: firstResponse ?? legacy ?? DEFAULT_FIRST_RESPONSE_TIMEOUT_MS,
5871
+ streamIdleTimeoutMs: streamIdle ?? legacy ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS,
5872
+ requestTimeoutMs: requestTimeout,
5873
+ connectionIdleTimeoutMs: connectionIdle ?? DEFAULT_CONNECTION_IDLE_TIMEOUT_MS
5874
+ };
5875
+ }
5672
5876
  function disposeModelTransports(owner) {
5673
5877
  const state = owners.get(owner);
5674
5878
  if (!state) return;
5675
5879
  disposeState(owner, state);
5676
5880
  }
5881
+ function clearConnectionIdleTimer(state) {
5882
+ if (state.idleTimer !== void 0) {
5883
+ clearTimeout(state.idleTimer);
5884
+ state.idleTimer = void 0;
5885
+ }
5886
+ }
5677
5887
  function disposeState(owner, state) {
5678
5888
  if (owners.get(owner) === state) owners.delete(owner);
5889
+ clearConnectionIdleTimer(state);
5679
5890
  state.anchor = void 0;
5680
5891
  state.abort.abort();
5681
- void state.connection?.close().catch(() => {
5892
+ const connection = state.connection;
5893
+ state.connection = void 0;
5894
+ void connection?.close().catch(() => {
5682
5895
  });
5683
5896
  }
5684
5897
  function stateFor(owner) {
5685
5898
  let state = owners.get(owner);
5686
5899
  if (!state) {
5687
- state = { busy: false, abort: new AbortController() };
5900
+ state = { busy: false, abort: new AbortController(), requestGeneration: 0, connectionGeneration: 0 };
5688
5901
  owners.set(owner, state);
5689
5902
  }
5690
5903
  return state;
5691
5904
  }
5905
+ function armConnectionIdleRecycle(state, connection, generation, timeoutMs) {
5906
+ clearConnectionIdleTimer(state);
5907
+ if (!(timeoutMs > 0)) return;
5908
+ const timer = setTimeout(() => {
5909
+ if (state.idleTimer !== timer) return;
5910
+ state.idleTimer = void 0;
5911
+ if (state.busy || state.connectionGeneration !== generation || state.connection !== connection) return;
5912
+ state.connection = void 0;
5913
+ state.anchor = void 0;
5914
+ state.identity = void 0;
5915
+ void connection.close().catch(() => {
5916
+ });
5917
+ }, timeoutMs);
5918
+ timer.unref?.();
5919
+ state.idleTimer = timer;
5920
+ }
5692
5921
  function websocketUrl(base) {
5693
5922
  const url = new URL(`${base.replace(/\/$/, "")}/responses`);
5694
5923
  if (url.protocol !== "https:" && url.protocol !== "http:") throw new ResponsesProtocolError("Responses requires an HTTP(S) API root.");
@@ -5706,19 +5935,190 @@ function streamResponsesWS(model, context, options) {
5706
5935
  stream.end();
5707
5936
  return stream;
5708
5937
  }
5938
+ let budgets;
5939
+ try {
5940
+ budgets = resolveBudgets(options);
5941
+ } catch (error) {
5942
+ reducer.message.stopReason = "error";
5943
+ reducer.message.errorMessage = error instanceof Error ? error.message : String(error);
5944
+ reducer.message.retryable = false;
5945
+ stream.push({ type: "error", reason: "error", error: reducer.message });
5946
+ stream.end();
5947
+ return stream;
5948
+ }
5709
5949
  state.busy = true;
5950
+ const localRequestId = nextLocalRequestId("ws");
5951
+ const requestGeneration = ++state.requestGeneration;
5952
+ const startedAt = monotonicNow();
5953
+ let lastReceiveAt = startedAt;
5954
+ let receivedMessageCount = 0;
5955
+ let sawResponseCreated = false;
5956
+ let sawModelContent = false;
5957
+ let reusedConnection = false;
5958
+ let connectionGeneration = state.connectionGeneration;
5710
5959
  void (async () => {
5711
5960
  let sent = false;
5712
5961
  let finished = false;
5713
5962
  let terminalEvents = [];
5714
5963
  let fallback = false;
5964
+ let timedOut;
5965
+ let retryDecision = "none";
5966
+ let requestTimer;
5967
+ let totalTimer;
5968
+ let receiveCleanupFailed = false;
5969
+ let cleanupCancelSource;
5970
+ const requestAbort = new AbortController();
5715
5971
  const publish = (event) => {
5716
5972
  if (event.type === "done" || event.type === "error") terminalEvents.push(event);
5717
5973
  else stream.push(event);
5718
5974
  };
5975
+ const ageMs = () => Math.round(monotonicNow() - startedAt);
5976
+ const lastReceiveAgeMs = () => Math.round(monotonicNow() - lastReceiveAt);
5977
+ const lifecycle = (phase, extra = {}) => reportLifecycle(options, {
5978
+ ...extra,
5979
+ transport: "websocket",
5980
+ localRequestId,
5981
+ requestGeneration,
5982
+ connectionGeneration,
5983
+ reusedConnection,
5984
+ phase,
5985
+ requestAgeMs: ageMs(),
5986
+ lastReceiveAgeMs: lastReceiveAgeMs(),
5987
+ budgets,
5988
+ receivedMessageCount,
5989
+ sawResponseCreated,
5990
+ sawModelContent,
5991
+ // The WS transport never re-sends a submitted frame (no hidden retry).
5992
+ singleAttempt: "verified"
5993
+ });
5994
+ const clearRequestTimers = () => {
5995
+ if (requestTimer !== void 0) {
5996
+ clearTimeout(requestTimer);
5997
+ requestTimer = void 0;
5998
+ }
5999
+ if (totalTimer !== void 0) {
6000
+ clearTimeout(totalTimer);
6001
+ totalTimer = void 0;
6002
+ }
6003
+ };
6004
+ const armTotalTimer = () => {
6005
+ if (budgets.requestTimeoutMs === void 0 || totalTimer !== void 0) return;
6006
+ const generation = requestGeneration;
6007
+ totalTimer = setTimeout(() => {
6008
+ totalTimer = void 0;
6009
+ if (generation !== requestGeneration || finished || requestAbort.signal.aborted) return;
6010
+ timedOut = "request_total";
6011
+ retryDecision = sent ? "not_retryable_after_submission" : "not_submitted";
6012
+ lifecycle("timeout", {
6013
+ timeoutKind: "request_total",
6014
+ terminalAccepted: false,
6015
+ cancelSource: "internal_timeout",
6016
+ retryAllowed: !sent,
6017
+ retryDecision,
6018
+ submissionAttempted: sent
6019
+ });
6020
+ requestAbort.abort();
6021
+ }, budgets.requestTimeoutMs);
6022
+ totalTimer.unref?.();
6023
+ };
6024
+ const remainingRequestBudget = () => {
6025
+ if (budgets.requestTimeoutMs === void 0) return void 0;
6026
+ return Math.max(0, Math.floor(budgets.requestTimeoutMs - ageMs()));
6027
+ };
6028
+ let fallbackAbort;
6029
+ let detachFallbackAbort;
6030
+ const fallbackSignal = () => {
6031
+ if (!fallbackAbort) {
6032
+ const controller = new AbortController();
6033
+ const onAbort = () => controller.abort();
6034
+ options?.signal?.addEventListener("abort", onAbort, { once: true });
6035
+ state.abort.signal.addEventListener("abort", onAbort, { once: true });
6036
+ if (options?.signal?.aborted || state.abort.signal.aborted) controller.abort();
6037
+ fallbackAbort = controller;
6038
+ detachFallbackAbort = () => {
6039
+ options?.signal?.removeEventListener("abort", onAbort);
6040
+ state.abort.signal.removeEventListener("abort", onAbort);
6041
+ };
6042
+ }
6043
+ return fallbackAbort.signal;
6044
+ };
6045
+ const closeReceiveIterator = async (iterator) => {
6046
+ const returnFn = iterator.return;
6047
+ if (typeof returnFn !== "function") return;
6048
+ let timer;
6049
+ try {
6050
+ await Promise.race([
6051
+ Promise.resolve().then(() => returnFn.call(iterator)),
6052
+ new Promise((_, reject) => {
6053
+ timer = setTimeout(() => reject(new Error("bounded iterator cleanup")), ITERATOR_CLEANUP_TIMEOUT_MS);
6054
+ timer.unref?.();
6055
+ })
6056
+ ]);
6057
+ } catch {
6058
+ receiveCleanupFailed = true;
6059
+ state.connection = void 0;
6060
+ state.anchor = void 0;
6061
+ requestAbort.abort();
6062
+ } finally {
6063
+ if (timer !== void 0) clearTimeout(timer);
6064
+ }
6065
+ };
6066
+ const armPhaseTimer = (kind, timeoutMs) => {
6067
+ if (requestTimer !== void 0) {
6068
+ clearTimeout(requestTimer);
6069
+ requestTimer = void 0;
6070
+ }
6071
+ if (timeoutMs === void 0 || timeoutMs <= 0 || finished) return;
6072
+ const generation = requestGeneration;
6073
+ requestTimer = setTimeout(() => {
6074
+ requestTimer = void 0;
6075
+ if (generation !== requestGeneration || finished || requestAbort.signal.aborted) return;
6076
+ timedOut = kind;
6077
+ retryDecision = sent ? "not_retryable_after_submission" : "not_submitted";
6078
+ lifecycle("timeout", {
6079
+ timeoutKind: kind,
6080
+ terminalAccepted: false,
6081
+ cancelSource: "internal_timeout",
6082
+ retryAllowed: !sent,
6083
+ retryDecision,
6084
+ submissionAttempted: sent
6085
+ });
6086
+ requestAbort.abort();
6087
+ }, timeoutMs);
6088
+ requestTimer.unref?.();
6089
+ };
6090
+ const onOwnerAbort = () => requestAbort.abort();
6091
+ if (state.abort.signal.aborted) requestAbort.abort();
6092
+ else state.abort.signal.addEventListener("abort", onOwnerAbort, { once: true });
5719
6093
  const abort = () => disposeState(owner, state);
5720
6094
  options?.signal?.addEventListener("abort", abort, { once: true });
6095
+ const runSSEFallback = async () => {
6096
+ const remaining = remainingRequestBudget();
6097
+ if (remaining !== void 0 && remaining <= 0) {
6098
+ timedOut = "request_total";
6099
+ retryDecision = sent ? "not_retryable_after_submission" : "not_submitted";
6100
+ lifecycle("timeout", {
6101
+ timeoutKind: "request_total",
6102
+ terminalAccepted: false,
6103
+ cancelSource: "internal_timeout",
6104
+ retryAllowed: !sent,
6105
+ retryDecision,
6106
+ submissionAttempted: sent
6107
+ });
6108
+ throw new Error("Responses request timeout (request_total)");
6109
+ }
6110
+ clearRequestTimers();
6111
+ fallback = true;
6112
+ const fallbackOptions = {
6113
+ ...options,
6114
+ signal: fallbackSignal(),
6115
+ ...remaining !== void 0 ? { requestTimeoutMs: remaining } : {}
6116
+ };
6117
+ for await (const event of streamResponsesSSE(model, context, fallbackOptions)) publish(event);
6118
+ finished = true;
6119
+ };
5721
6120
  try {
6121
+ armTotalTimer();
5722
6122
  if (options?.signal?.aborted) {
5723
6123
  abort();
5724
6124
  throw new Error("Request aborted");
@@ -5729,42 +6129,52 @@ function streamResponsesWS(model, context, options) {
5729
6129
  const headers = { Authorization: `Bearer ${key}`, ...model.headers, ...options?.headers };
5730
6130
  const { input, ...settings } = payload;
5731
6131
  const identity = JSON.stringify([model.provider, model.api, model.id, model.baseUrl, headers, settings]);
6132
+ clearConnectionIdleTimer(state);
5732
6133
  if (state.identity !== identity || state.connection?.state() !== "open") {
5733
- await state.connection?.close();
6134
+ const stale = state.connection;
5734
6135
  state.connection = void 0;
5735
6136
  state.anchor = void 0;
5736
6137
  state.identity = identity;
6138
+ await stale?.close().catch(() => {
6139
+ });
5737
6140
  }
6141
+ reusedConnection = !!state.connection;
5738
6142
  if (state.abort.signal.aborted) throw new Error("Request aborted");
6143
+ lifecycle("request_start");
5739
6144
  const client = getWebSocketClient();
5740
6145
  if (!client) {
5741
6146
  if (options?.transport === "auto") {
5742
- fallback = true;
5743
- for await (const event of streamResponsesSSE(model, context, { ...options, signal: state.abort.signal })) publish(event);
5744
- finished = true;
6147
+ await runSSEFallback();
5745
6148
  return;
5746
6149
  }
5747
6150
  throw new Error("WebSocket transport is not available on this host.");
5748
6151
  }
5749
- const reusedConnection = !!state.connection;
5750
- if (!state.connection) {
6152
+ let connection = state.connection;
6153
+ if (!connection) {
5751
6154
  try {
5752
- state.connection = await client.connect(websocketUrl(model.baseUrl), {
6155
+ connection = await client.connect(websocketUrl(model.baseUrl), {
5753
6156
  headers,
5754
- signal: state.abort.signal,
5755
- connectTimeoutMs: options?.timeoutMs ?? 3e4,
5756
- idleTimeoutMs: options?.timeoutMs ?? 12e4
6157
+ signal: requestAbort.signal,
6158
+ connectTimeoutMs: budgets.connectTimeoutMs
5757
6159
  });
5758
6160
  } catch (error) {
6161
+ if (error instanceof WebSocketTransportError && error.kind === "timeout") timedOut = "connect";
5759
6162
  if (options?.transport === "auto" && error instanceof WebSocketTransportError && error.kind === "handshake" && [404, 405, 426, 501].includes(error.status ?? 0) && !options.signal?.aborted && !state.abort.signal.aborted) {
5760
- fallback = true;
5761
- for await (const event of streamResponsesSSE(model, context, { ...options, signal: state.abort.signal })) publish(event);
5762
- finished = true;
6163
+ await runSSEFallback();
5763
6164
  return;
5764
6165
  }
5765
6166
  throw error;
5766
6167
  }
6168
+ state.connection = connection;
6169
+ connectionGeneration = ++state.connectionGeneration;
6170
+ if (state.abort.signal.aborted || requestAbort.signal.aborted) {
6171
+ state.connection = void 0;
6172
+ void connection.close().catch(() => {
6173
+ });
6174
+ throw new Error("Request aborted");
6175
+ }
5767
6176
  }
6177
+ const activeConnection = connection;
5768
6178
  const fullInput = input;
5769
6179
  const anchor = state.anchor;
5770
6180
  if (!options?.onPayload && model.compat?.responsesContinuation && anchor && fullInput.length > anchor.count && JSON.stringify(fullInput.slice(0, anchor.count)) === anchor.prefix) {
@@ -5775,65 +6185,123 @@ function streamResponsesWS(model, context, options) {
5775
6185
  for (let attempt = 0; attempt < 2; attempt++) {
5776
6186
  let rebuild = false;
5777
6187
  options?.onPayload?.(payload);
5778
- if (state.abort.signal.aborted) throw new Error("Request aborted");
6188
+ if (state.abort.signal.aborted || requestAbort.signal.aborted) throw new Error("Request aborted");
5779
6189
  sent = true;
5780
6190
  const body = JSON.stringify(payload);
5781
6191
  reportSubmission(options, body, { transport: "websocket", reusedConnection: reusedConnection || attempt > 0, continuation: !!payload.previous_response_id, attempt: attempt + 1 });
5782
- await state.connection.send(body, { signal: state.abort.signal });
6192
+ lifecycle("submission_attempted", { submissionAttempted: true });
6193
+ armPhaseTimer("first_response", budgets.firstResponseTimeoutMs);
6194
+ await activeConnection.send(body, { signal: requestAbort.signal });
5783
6195
  let started = false;
5784
6196
  let responseId;
5785
- for await (const message of state.connection.receive(state.abort.signal)) {
5786
- if (state.abort.signal.aborted) throw new Error("Request aborted");
5787
- if (message.isBinary) throw new ResponsesProtocolError("Responses requires JSON text messages.");
5788
- const event = JSON.parse(message.data);
5789
- if (!isRecord2(event)) throw new ResponsesProtocolError("Invalid Responses WebSocket event.");
5790
- if (!started && attempt === 0 && payload.previous_response_id && event.type === "error" && event.status === 400 && isRecord2(event.error) && event.error.code === "previous_response_not_found" && event.error.param === "previous_response_id") {
5791
- delete payload.previous_response_id;
5792
- payload.input = fullInput;
5793
- rebuild = true;
5794
- break;
5795
- }
5796
- if (!started && typeof event.type === "string" && event.type !== "error" && !event.type.startsWith("response.")) continue;
5797
- if (!started && event.type !== "error") {
5798
- if (event.type !== "response.created" || !isRecord2(event.response) || typeof event.response.id !== "string" || event.response.id === state.lastResponseId) {
5799
- throw new ResponsesProtocolError("Expected a new response.created before output.");
6197
+ const receiveIterator = activeConnection.receive(requestAbort.signal)[Symbol.asyncIterator]();
6198
+ let receiveDone = false;
6199
+ try {
6200
+ while (true) {
6201
+ const step = await receiveIterator.next();
6202
+ if (step.done) {
6203
+ receiveDone = true;
6204
+ break;
5800
6205
  }
5801
- started = true;
5802
- responseId = event.response.id;
5803
- }
5804
- if (responseId && isRecord2(event.response) && event.response.id !== responseId) throw new ResponsesProtocolError("Response identity changed within a request.");
5805
- const events = reducer.feed(event);
5806
- if (reducer.isTerminal) {
5807
- if (reducer.message.stopReason === "error") reducer.message.retryable = false;
5808
- const response = reducer.terminalResponse;
5809
- state.lastResponseId = response?.id;
5810
- if (!options?.onPayload && response?.status === "completed" && reducer.message.stopReason !== "error") {
5811
- const prefixItems = [...fullInput, ...buildResponsesOutput(reducer.message)];
5812
- const prefix = JSON.stringify(prefixItems);
5813
- if (prefix.length * 2 <= MAX_ANCHOR_BYTES) state.anchor = { id: response.id, prefix, count: prefixItems.length };
6206
+ const message = step.value;
6207
+ receivedMessageCount += 1;
6208
+ lastReceiveAt = monotonicNow();
6209
+ armPhaseTimer("stream_idle", budgets.streamIdleTimeoutMs);
6210
+ if (receivedMessageCount === 1) lifecycle("first_application_message");
6211
+ if (state.abort.signal.aborted || requestAbort.signal.aborted) {
6212
+ throw new Error(timedOut ? `Responses request timeout (${timedOut})` : "Request aborted");
6213
+ }
6214
+ if (message.isBinary) throw new ResponsesProtocolError("Responses requires JSON text messages.");
6215
+ const event = JSON.parse(message.data);
6216
+ if (!isRecord2(event)) throw new ResponsesProtocolError("Invalid Responses WebSocket event.");
6217
+ if (typeof event.type === "string") {
6218
+ if (event.type === "response.created") sawResponseCreated = true;
6219
+ else if (event.type.startsWith("response.output") || event.type.startsWith("response.content") || event.type.startsWith("response.reasoning")) sawModelContent = true;
6220
+ }
6221
+ if (!started && attempt === 0 && payload.previous_response_id && event.type === "error" && event.status === 400 && isRecord2(event.error) && event.error.code === "previous_response_not_found" && event.error.param === "previous_response_id") {
6222
+ delete payload.previous_response_id;
6223
+ payload.input = fullInput;
6224
+ rebuild = true;
6225
+ break;
6226
+ }
6227
+ if (!started && typeof event.type === "string" && event.type !== "error" && !event.type.startsWith("response.")) continue;
6228
+ if (!started && event.type !== "error") {
6229
+ if (event.type !== "response.created" || !isRecord2(event.response) || typeof event.response.id !== "string" || event.response.id === state.lastResponseId) {
6230
+ throw new ResponsesProtocolError("Expected a new response.created before output.");
6231
+ }
6232
+ started = true;
6233
+ responseId = event.response.id;
6234
+ }
6235
+ if (responseId && isRecord2(event.response) && event.response.id !== responseId) throw new ResponsesProtocolError("Response identity changed within a request.");
6236
+ const events = reducer.feed(event);
6237
+ if (reducer.isTerminal) {
6238
+ if (reducer.message.stopReason === "error") reducer.message.retryable = false;
6239
+ const response = reducer.terminalResponse;
6240
+ state.lastResponseId = response?.id;
6241
+ if (!options?.onPayload && response?.status === "completed" && reducer.message.stopReason !== "error") {
6242
+ const prefixItems = [...fullInput, ...buildResponsesOutput(reducer.message)];
6243
+ const prefix = JSON.stringify(prefixItems);
6244
+ if (prefix.length * 2 <= MAX_ANCHOR_BYTES) state.anchor = { id: response.id, prefix, count: prefixItems.length };
6245
+ }
6246
+ finished = true;
6247
+ clearRequestTimers();
6248
+ lifecycle("terminal_accepted", {
6249
+ terminalAccepted: true,
6250
+ retryAllowed: reducer.message.retryable !== false,
6251
+ retryDecision: "terminal"
6252
+ });
6253
+ for (const e of events) publish(e);
6254
+ break;
5814
6255
  }
5815
- finished = true;
5816
6256
  for (const e of events) publish(e);
5817
- break;
5818
6257
  }
5819
- for (const e of events) publish(e);
6258
+ } finally {
6259
+ if (!receiveDone) await closeReceiveIterator(receiveIterator);
6260
+ }
6261
+ if (rebuild) {
6262
+ retryDecision = "rebuild_previous_response";
6263
+ continue;
5820
6264
  }
5821
- if (rebuild) continue;
5822
6265
  if (!reducer.isTerminal) throw reducer.finishWithoutTerminal();
5823
6266
  break;
5824
6267
  }
5825
6268
  } catch (error) {
5826
6269
  if (!finished) {
5827
6270
  const message = reducer.message;
5828
- message.stopReason = options?.signal?.aborted || state.abort.signal.aborted ? "aborted" : "error";
5829
- message.errorMessage = error instanceof Error ? error.message : String(error);
5830
- if (sent) message.retryable = false;
6271
+ const userAborted = options?.signal?.aborted === true;
6272
+ const ownerDisposed = !userAborted && !timedOut && state.abort.signal.aborted;
6273
+ message.stopReason = userAborted || ownerDisposed ? "aborted" : "error";
6274
+ const cause = timedOut ? `Responses request timeout (${timedOut}) after ${ageMs()}ms` : error instanceof Error ? error.message : String(error);
6275
+ message.errorMessage = cause;
6276
+ if (sent) {
6277
+ message.retryable = false;
6278
+ retryDecision = "not_retryable_after_submission";
6279
+ }
6280
+ cleanupCancelSource = userAborted ? "user" : timedOut ? "internal_timeout" : ownerDisposed ? "owner_disposal" : "none";
5831
6281
  stream.push({ type: "error", reason: message.stopReason, error: message });
5832
6282
  }
5833
6283
  } finally {
6284
+ clearRequestTimers();
6285
+ detachFallbackAbort?.();
6286
+ state.abort.signal.removeEventListener("abort", onOwnerAbort);
5834
6287
  options?.signal?.removeEventListener("abort", abort);
5835
6288
  state.busy = false;
5836
- if (fallback || !finished || !options?.transportOwner || reducer.message.stopReason === "error") disposeState(owner, state);
6289
+ const cancelSource = cleanupCancelSource ?? (options?.signal?.aborted ? "user" : timedOut ? "internal_timeout" : state.abort.signal.aborted ? "owner_disposal" : "none");
6290
+ const retain = finished && !fallback && !receiveCleanupFailed && !!options?.transportOwner && reducer.message.stopReason !== "error" && !timedOut && state.connection !== void 0 && !state.abort.signal.aborted;
6291
+ if (!retain) disposeState(owner, state);
6292
+ else if (state.connection) {
6293
+ armConnectionIdleRecycle(state, state.connection, connectionGeneration, budgets.connectionIdleTimeoutMs);
6294
+ }
6295
+ const cleanupOutcome = receiveCleanupFailed ? "cleanup_failed" : retain ? "connection_reused" : finished ? "connection_closed" : "request_abandoned";
6296
+ lifecycle("cleanup", {
6297
+ terminalAccepted: finished,
6298
+ timeoutKind: timedOut,
6299
+ submissionAttempted: sent,
6300
+ retryAllowed: !finished && !sent && reducer.message.stopReason !== "aborted" && reducer.message.retryable !== false,
6301
+ cancelSource,
6302
+ retryDecision,
6303
+ cleanupOutcome
6304
+ });
5837
6305
  for (const event of terminalEvents) stream.push(event);
5838
6306
  stream.end();
5839
6307
  }