@oh-my-pi/pi-ai 16.5.2 → 17.0.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.
@@ -1167,6 +1167,25 @@ function resolveAnthropicBaseUrl(model: Model<"anthropic-messages">, apiKey?: st
1167
1167
  return normalizeAnthropicBaseUrl(model.baseUrl);
1168
1168
  }
1169
1169
 
1170
+ function resolveEagerToolInputStreamingSupport(
1171
+ model: Model<"anthropic-messages">,
1172
+ effectiveBaseUrl: string | undefined,
1173
+ ): boolean {
1174
+ if (!model.compat.supportsEagerToolInputStreaming) return false;
1175
+ // First-party Anthropic endpoints accept the per-tool flag.
1176
+ if (isOfficialAnthropicApiUrl(effectiveBaseUrl)) return true;
1177
+ // Non-official effective endpoint. `supportsEagerToolInputStreaming` may be
1178
+ // stale-true here because compat is materialized once at build time and is
1179
+ // never rebuilt for a baseUrl-only reroute — either a runtime provider
1180
+ // override (`pi.registerProvider("anthropic", { baseUrl })`) or Foundry
1181
+ // (`CLAUDE_CODE_USE_FOUNDRY`). Both leave the canonical model's resolved
1182
+ // compat in place. `officialEndpoint` records whether compat was built for
1183
+ // the canonical Anthropic URL, so only endpoints whose compat was authored
1184
+ // for a non-official host (an explicit `compat.supportsEagerToolInputStreaming`
1185
+ // opt-in on a custom `baseUrl`) still send the field.
1186
+ return !model.compat.officialEndpoint;
1187
+ }
1188
+
1170
1189
  function parseAnthropicCustomHeaders(rawHeaders: string | undefined): Record<string, string> | undefined {
1171
1190
  const source = rawHeaders?.trim();
1172
1191
  if (!source) return undefined;
@@ -1741,6 +1760,7 @@ const streamAnthropicOnce = (
1741
1760
  }
1742
1761
  const apiKey = options?.apiKey ?? getEnvApiKey(model.provider) ?? "";
1743
1762
  const baseUrl = resolveAnthropicBaseUrl(model, apiKey) ?? "https://api.anthropic.com";
1763
+ const supportsEagerToolInputStreaming = resolveEagerToolInputStreamingSupport(model, baseUrl);
1744
1764
  const providerSessionState = getAnthropicProviderSessionState(
1745
1765
  options?.providerSessionState,
1746
1766
  baseUrl,
@@ -1752,6 +1772,23 @@ const streamAnthropicOnce = (
1752
1772
  let forceDemoteUnsignedThinking = providerSessionState?.replayUnsignedThinkingDisabled ?? false;
1753
1773
  const mergedCallerHeaders = mergeHeaders(model.headers, options?.headers);
1754
1774
  const umansGatewayWebSearchHeader = getUmansWebSearchHeader(model, mergedCallerHeaders);
1775
+ // Keep fallback payloads aligned with the top-level Vertex effort gate:
1776
+ // no nested effort field means the fallback scan cannot re-add its beta.
1777
+ let fallbacks = options?.fallbacks;
1778
+ if (
1779
+ model.provider === "google-vertex" &&
1780
+ fallbacks?.some(entry => entry.output_config?.effort !== undefined)
1781
+ ) {
1782
+ fallbacks = fallbacks.map(entry => {
1783
+ const outputConfig = entry.output_config;
1784
+ if (outputConfig?.effort === undefined) return entry;
1785
+ return {
1786
+ ...entry,
1787
+ output_config:
1788
+ outputConfig.task_budget === undefined ? undefined : { task_budget: outputConfig.task_budget },
1789
+ };
1790
+ });
1791
+ }
1755
1792
 
1756
1793
  let client: AnthropicMessagesClientLike;
1757
1794
  let isOAuthToken: boolean;
@@ -1776,6 +1813,10 @@ const streamAnthropicOnce = (
1776
1813
  // the toggle cannot 400); the beta must accompany the field in both.
1777
1814
  // MiniMax uses `thinking.type:"adaptive"` itself as the control surface,
1778
1815
  // so the sentinel "adaptive" value intentionally sends no output_config.
1816
+ // Skip Vertex rawPredict: that adapter needs betas in the body
1817
+ // (`anthropic_beta`), not as an `anthropic-beta` HTTP header, so the
1818
+ // effort field is dropped from the body there too (see buildParams) and
1819
+ // advertising the beta would only earn a 400 (#5614).
1779
1820
  const sendsAdaptiveEffortPin =
1780
1821
  options?.thinkingEnabled === false &&
1781
1822
  model.thinking?.mode === "anthropic-adaptive" &&
@@ -1783,6 +1824,7 @@ const streamAnthropicOnce = (
1783
1824
  !usesAdaptiveThinkingTagOnly(model);
1784
1825
  if (
1785
1826
  model.reasoning &&
1827
+ model.provider !== "google-vertex" &&
1786
1828
  ((options?.thinkingEnabled && options.effort !== "adaptive") || sendsAdaptiveEffortPin) &&
1787
1829
  !extraBetas.includes(effortBeta)
1788
1830
  ) {
@@ -1816,11 +1858,11 @@ const streamAnthropicOnce = (
1816
1858
  // `output_config.task_budget`) reuse the same top-level betas
1817
1859
  // Anthropic requires for the primary request, so scan the chain
1818
1860
  // and add every companion beta the fallback entries touch.
1819
- if (options?.fallbacks?.length) {
1861
+ if (fallbacks?.length) {
1820
1862
  if (!extraBetas.includes(serverSideFallbackBeta)) {
1821
1863
  extraBetas.push(serverSideFallbackBeta);
1822
1864
  }
1823
- for (const entry of options.fallbacks) {
1865
+ for (const entry of fallbacks) {
1824
1866
  if (entry.speed === "fast" && !extraBetas.includes(fastModeBeta)) {
1825
1867
  extraBetas.push(fastModeBeta);
1826
1868
  }
@@ -1854,15 +1896,13 @@ const streamAnthropicOnce = (
1854
1896
  }
1855
1897
  const preparedContext = await prepareAnthropicManyImageContext(context, model.input.includes("image"));
1856
1898
  const prepareParams = async (): Promise<MessageCreateParamsStreaming> => {
1857
- let nextParams = buildParams(
1858
- model,
1859
- preparedContext,
1860
- isOAuthToken,
1861
- options,
1899
+ let nextParams = buildParams(model, preparedContext, isOAuthToken, options, {
1862
1900
  disableStrictTools,
1863
- umansGatewayWebSearchHeader !== undefined,
1901
+ useUmansGatewayWebSearch: umansGatewayWebSearchHeader !== undefined,
1864
1902
  forceDemoteUnsignedThinking,
1865
- );
1903
+ supportsEagerToolInputStreaming,
1904
+ fallbacks,
1905
+ });
1866
1906
  if (disableStrictTools) {
1867
1907
  dropAnthropicStrictTools(nextParams);
1868
1908
  }
@@ -1888,9 +1928,9 @@ const streamAnthropicOnce = (
1888
1928
 
1889
1929
  // Opt-in flag: the response parser only honors `fallback` content
1890
1930
  // blocks and `usage.iterations` when the current request opted into
1891
- // the server-side-fallback beta chain. Leaving `options.fallbacks`
1892
- // unset preserves the pre-fallback stream shape on every event.
1893
- const serverSideFallback = !!options?.fallbacks?.length;
1931
+ // server-side-fallback beta chain. Leaving `fallbacks` unset preserves
1932
+ // the pre-fallback stream shape on every event.
1933
+ const serverSideFallback = !!fallbacks?.length;
1894
1934
  type Block = (
1895
1935
  | ThinkingContent
1896
1936
  | RedactedThinkingContent
@@ -2682,9 +2722,11 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
2682
2722
  const compat = model.compat;
2683
2723
  const disableStrictTools = disableStrictToolsOverride ?? compat.disableStrictTools;
2684
2724
  const needsInterleavedBeta = interleavedThinking && !model.thinking?.supportsDisplay;
2685
- const needsFineGrainedToolStreamingBeta = hasTools && !compat.supportsEagerToolInputStreaming;
2686
2725
  const oauthToken = isOAuth ?? isAnthropicOAuthToken(apiKey);
2687
2726
  const baseUrl = resolveAnthropicBaseUrl(model, apiKey);
2727
+ const supportsEagerToolInputStreaming = resolveEagerToolInputStreamingSupport(model, baseUrl);
2728
+ const needsFineGrainedToolStreamingBeta =
2729
+ hasTools && isOfficialAnthropicApiUrl(baseUrl) && !supportsEagerToolInputStreaming;
2688
2730
  const foundryCustomHeaders = resolveAnthropicCustomHeaders(model);
2689
2731
  const tlsFetchOptions = buildClaudeCodeTlsFetchOptions(model, baseUrl);
2690
2732
  // Disable Bun's native ~300s pre-response fetch timeout (issue #2422).
@@ -2699,9 +2741,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
2699
2741
  if (model.provider === "github-copilot") {
2700
2742
  const copilotApiKey = parseGitHubCopilotApiKey(apiKey).accessToken;
2701
2743
  // The GitHub Copilot Anthropic proxy doesn't accept Anthropic beta
2702
- // features (and the catalog already forces `supportsEagerToolInputStreaming
2703
- // = false` for this host, so `needsFineGrainedToolStreamingBeta` is true
2704
- // whenever tools are present). Forward only caller-supplied betas.
2744
+ // features. Forward only caller-supplied betas.
2705
2745
  const betaFeatures = [...extraBetas];
2706
2746
  const defaultHeaders = mergeHeaders(
2707
2747
  {
@@ -3129,15 +3169,29 @@ function extractClaudeCodeFirstUserMessageText(messages: readonly Message[]): st
3129
3169
  return "";
3130
3170
  }
3131
3171
 
3172
+ type AnthropicParamBuildOptions = {
3173
+ disableStrictTools: boolean;
3174
+ useUmansGatewayWebSearch: boolean;
3175
+ forceDemoteUnsignedThinking: boolean;
3176
+ supportsEagerToolInputStreaming: boolean;
3177
+ /** Sanitized server-side fallback entries; defaults to `options?.fallbacks` when omitted. */
3178
+ fallbacks?: AnthropicOptions["fallbacks"];
3179
+ };
3180
+
3132
3181
  function buildParams(
3133
3182
  model: Model<"anthropic-messages">,
3134
3183
  context: Context,
3135
3184
  isOAuthToken: boolean,
3136
- options?: AnthropicOptions,
3137
- disableStrictTools = false,
3138
- useUmansGatewayWebSearch = false,
3139
- forceDemoteUnsignedThinking = false,
3185
+ options: AnthropicOptions | undefined,
3186
+ buildOptions: AnthropicParamBuildOptions,
3140
3187
  ): MessageCreateParamsStreaming {
3188
+ const {
3189
+ disableStrictTools,
3190
+ useUmansGatewayWebSearch,
3191
+ forceDemoteUnsignedThinking,
3192
+ supportsEagerToolInputStreaming,
3193
+ fallbacks = options?.fallbacks,
3194
+ } = buildOptions;
3141
3195
  // A session-scoped auto-demote (learned from a live signing 400) clones the
3142
3196
  // resolved compat with `replayUnsignedThinking: false` so every subsequent
3143
3197
  // downstream read (convertAnthropicMessages, transformMessages) sees the
@@ -3165,7 +3219,7 @@ function buildParams(
3165
3219
  context.tools,
3166
3220
  isOAuthToken,
3167
3221
  disableStrictTools || model.provider === "github-copilot",
3168
- model.compat.supportsEagerToolInputStreaming,
3222
+ supportsEagerToolInputStreaming,
3169
3223
  model.compat.escapeBuiltinToolNames,
3170
3224
  useUmansGatewayWebSearch,
3171
3225
  );
@@ -3255,9 +3309,12 @@ function buildParams(
3255
3309
  ? { edits: [{ type: "clear_thinking_20251015" as const, keep: "all" as const }] }
3256
3310
  : undefined;
3257
3311
 
3258
- // Pre-compute output_config.
3312
+ // Pre-compute output_config. Skip `effort` on Vertex rawPredict: it requires
3313
+ // the `effort-2025-11-24` beta, which that adapter can only accept in the body
3314
+ // (`anthropic_beta`), never as the `anthropic-beta` HTTP header this path sets
3315
+ // — so the field is dropped alongside the beta to avoid a 400 (#5614).
3259
3316
  const outputConfigEntries: AnthropicOutputConfig = {};
3260
- if (outputConfigEffort) outputConfigEntries.effort = outputConfigEffort;
3317
+ if (outputConfigEffort && model.provider !== "google-vertex") outputConfigEntries.effort = outputConfigEffort;
3261
3318
  if (options?.taskBudget) outputConfigEntries.task_budget = options.taskBudget;
3262
3319
  const outputConfig = Object.keys(outputConfigEntries).length ? outputConfigEntries : undefined;
3263
3320
 
@@ -3272,7 +3329,7 @@ function buildParams(
3272
3329
  const params: MessageCreateParamsStreaming = {
3273
3330
  model: options?.requestModelId ?? model.requestModelId ?? model.id,
3274
3331
  messages: convertAnthropicMessages(context.messages, effectiveModel, isOAuthToken, {
3275
- serverSideFallbackEnabled: !!options?.fallbacks?.length,
3332
+ serverSideFallbackEnabled: !!fallbacks?.length,
3276
3333
  }),
3277
3334
  ...(systemBlocks && { system: systemBlocks }),
3278
3335
  ...(tools !== undefined && { tools }),
@@ -3281,7 +3338,7 @@ function buildParams(
3281
3338
  ...(thinking && { thinking }),
3282
3339
  ...(contextManagement && { context_management: contextManagement }),
3283
3340
  ...(outputConfig && { output_config: outputConfig }),
3284
- ...(options?.fallbacks?.length ? { fallbacks: options.fallbacks } : {}),
3341
+ ...(fallbacks?.length ? { fallbacks } : {}),
3285
3342
  stream: true,
3286
3343
  };
3287
3344
 
@@ -351,6 +351,8 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
351
351
  let h2Request: http2.ClientHttp2Stream | null = null;
352
352
  let heartbeatTimer: NodeJS.Timeout | null = null;
353
353
  let debugResponseLogPromise: Promise<RequestDebugResponseLog | undefined> | undefined;
354
+ const h2Completion = Promise.withResolvers<void>();
355
+ let resolveH2: (() => void) | undefined = h2Completion.resolve;
354
356
 
355
357
  try {
356
358
  const apiKey = options?.apiKey;
@@ -406,6 +408,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
406
408
  } else {
407
409
  h2Client = http2.connect(baseUrl);
408
410
  }
411
+ h2Client.on("error", h2Completion.reject);
409
412
 
410
413
  h2Request = h2Client.request(requestHeaders);
411
414
 
@@ -449,8 +452,6 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
449
452
  conversationStateCache.set(conversationId, checkpoint);
450
453
  };
451
454
 
452
- let resolveH2: (() => void) | undefined;
453
-
454
455
  h2Request.on("response", headers => {
455
456
  debugResponseLogPromise = debugSession?.openResponseLog(
456
457
  `HTTP/2 ${headers[":status"] ?? ""}`.trim(),
@@ -517,8 +518,6 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
517
518
  }
518
519
  });
519
520
 
520
- h2Request.write(frameConnectMessage(requestBytes));
521
-
522
521
  const sendHeartbeat = () => {
523
522
  if (!h2Request || h2Request.closed) {
524
523
  return;
@@ -530,57 +529,55 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
530
529
  h2Request.write(frameConnectMessage(heartbeatBytes));
531
530
  };
532
531
 
533
- heartbeatTimer = setInterval(sendHeartbeat, 5000);
534
-
535
- await new Promise<void>((resolve, reject) => {
536
- resolveH2 = resolve;
532
+ const closeDebugLog = async (): Promise<void> => {
533
+ const log = await debugResponseLogPromise;
534
+ await log?.close();
535
+ };
537
536
 
538
- const closeDebugLog = async (): Promise<void> => {
539
- const log = await debugResponseLogPromise;
540
- await log?.close();
541
- };
537
+ h2Request.on("trailers", trailers => {
538
+ const status = trailers["grpc-status"];
539
+ const msg = trailers["grpc-message"];
540
+ if (status && status !== "0") {
541
+ void closeDebugLog().finally(() => {
542
+ h2Completion.reject(
543
+ new AIError.ProviderResponseError(
544
+ `gRPC error ${status}: ${decodeURIComponent(String(msg || ""))}`,
545
+ { kind: "envelope" },
546
+ ),
547
+ );
548
+ });
549
+ }
550
+ });
542
551
 
543
- h2Request!.on("trailers", trailers => {
544
- const status = trailers["grpc-status"];
545
- const msg = trailers["grpc-message"];
546
- if (status && status !== "0") {
547
- void closeDebugLog().finally(() => {
548
- reject(
549
- new AIError.ProviderResponseError(
550
- `gRPC error ${status}: ${decodeURIComponent(String(msg || ""))}`,
551
- { kind: "envelope" },
552
- ),
553
- );
554
- });
555
- }
556
- });
552
+ h2Request.on("end", () => {
553
+ resolveH2 = undefined;
554
+ void closeDebugLog()
555
+ .then(() => {
556
+ if (endStreamError) {
557
+ h2Completion.reject(endStreamError);
558
+ return;
559
+ }
560
+ h2Completion.resolve();
561
+ })
562
+ .catch(h2Completion.reject);
563
+ });
557
564
 
558
- h2Request!.on("end", () => {
559
- resolveH2 = undefined;
560
- void closeDebugLog()
561
- .then(() => {
562
- if (endStreamError) {
563
- reject(endStreamError);
564
- return;
565
- }
566
- resolve();
567
- })
568
- .catch(reject);
569
- });
565
+ h2Request.on("error", error => {
566
+ void closeDebugLog().finally(() => h2Completion.reject(error));
567
+ });
570
568
 
571
- h2Request!.on("error", error => {
572
- void closeDebugLog().finally(() => reject(error));
569
+ if (options?.signal) {
570
+ options.signal.addEventListener("abort", () => {
571
+ h2Request?.close();
572
+ void closeDebugLog().finally(() => {
573
+ h2Completion.reject(new AIError.AbortError());
574
+ });
573
575
  });
576
+ }
574
577
 
575
- if (options?.signal) {
576
- options.signal.addEventListener("abort", () => {
577
- h2Request?.close();
578
- void closeDebugLog().finally(() => {
579
- reject(new AIError.AbortError());
580
- });
581
- });
582
- }
583
- });
578
+ h2Request.write(frameConnectMessage(requestBytes));
579
+ heartbeatTimer = setInterval(sendHeartbeat, 5000);
580
+ await h2Completion.promise;
584
581
 
585
582
  endCurrentTextBlock(output, stream, state);
586
583
  endCurrentThinkingBlock(output, stream, state);
@@ -1,4 +1,4 @@
1
- import { fetchWithRetry, parseStreamingJson } from "@oh-my-pi/pi-utils";
1
+ import { fetchWithRetry, parseStreamingJson, readJsonl } from "@oh-my-pi/pi-utils";
2
2
  import * as AIError from "../error";
3
3
  import { getEnvApiKey } from "../stream";
4
4
  import type {
@@ -347,36 +347,6 @@ async function captureHttpErrorResponse(response: Response): Promise<CapturedHtt
347
347
  };
348
348
  }
349
349
 
350
- async function* iterateNdjson(stream: ReadableStream<Uint8Array>): AsyncGenerator<OllamaChatChunk> {
351
- const reader = stream.getReader();
352
- const decoder = new TextDecoder();
353
- let buffer = "";
354
- while (true) {
355
- const { done, value } = await reader.read();
356
- if (done) {
357
- break;
358
- }
359
- buffer += decoder.decode(value, { stream: true });
360
- while (true) {
361
- const newlineIndex = buffer.indexOf("\n");
362
- if (newlineIndex < 0) {
363
- break;
364
- }
365
- const line = buffer.slice(0, newlineIndex).trim();
366
- buffer = buffer.slice(newlineIndex + 1);
367
- if (!line) {
368
- continue;
369
- }
370
- yield JSON.parse(line) as OllamaChatChunk;
371
- }
372
- }
373
- buffer += decoder.decode();
374
- const tail = buffer.trim();
375
- if (tail) {
376
- yield JSON.parse(tail) as OllamaChatChunk;
377
- }
378
- }
379
-
380
350
  function createEmptyOutput(model: Model<"ollama-chat">): AssistantMessage {
381
351
  return {
382
352
  role: "assistant",
@@ -622,7 +592,7 @@ const streamOllamaOnce = (
622
592
  });
623
593
  }
624
594
  stream.push({ type: "start", partial: output });
625
- for await (const chunk of iterateNdjson(response.body)) {
595
+ for await (const chunk of readJsonl<OllamaChatChunk>(response.body)) {
626
596
  if (chunk.message?.thinking) {
627
597
  suppressHealedThinking = true;
628
598
  endActiveTextBlock();
@@ -60,6 +60,7 @@ import {
60
60
  getOpenAIStreamIdleTimeoutMs,
61
61
  iterateWithIdleTimeout,
62
62
  } from "../utils/idle-iterator";
63
+ import { getProxyForProvider, shouldBypassProxy } from "../utils/proxy";
63
64
  import { createRequestDebugSession, isRequestDebugEnabled, type RequestDebugResponseLog } from "../utils/request-debug";
64
65
  import { adaptSchemaForStrict, NO_STRICT, sanitizeSchemaForOpenAIResponses, toolWireSchema } from "../utils/schema";
65
66
  import { notifyRawSseEvent } from "../utils/sse-debug";
@@ -1481,6 +1482,7 @@ async function openCodexWebSocketTransport(
1481
1482
  websocketState,
1482
1483
  toWebSocketUrl(requestContext.url),
1483
1484
  websocketHeaders,
1485
+ model.provider,
1484
1486
  requestSetup.requestSignal,
1485
1487
  );
1486
1488
  const eventStream = websocketConnection.streamRequest(
@@ -2614,6 +2616,7 @@ export async function prewarmOpenAICodexResponses(
2614
2616
  state,
2615
2617
  toWebSocketUrl(url),
2616
2618
  headers,
2619
+ model.provider,
2617
2620
  options?.signal,
2618
2621
  );
2619
2622
  state.prewarmed = true;
@@ -3076,11 +3079,13 @@ interface CodexWebSocketRequestTimeouts {
3076
3079
 
3077
3080
  interface CodexWebSocketConnectionOptions {
3078
3081
  onHandshakeHeaders?: (headers: Headers) => void;
3082
+ proxy?: string;
3079
3083
  }
3080
3084
 
3081
3085
  class CodexWebSocketConnection {
3082
3086
  #url: string;
3083
3087
  #headers: Record<string, string>;
3088
+ #proxy?: string;
3084
3089
  #onHandshakeHeaders?: (headers: Headers) => void;
3085
3090
  #socket: Bun.WebSocket | null = null;
3086
3091
  #queue: Array<Record<string, unknown> | Error | null> = [];
@@ -3111,6 +3116,7 @@ class CodexWebSocketConnection {
3111
3116
  constructor(url: string, headers: Record<string, string>, options: CodexWebSocketConnectionOptions) {
3112
3117
  this.#url = url;
3113
3118
  this.#headers = headers;
3119
+ this.#proxy = options.proxy;
3114
3120
  this.#onHandshakeHeaders = options.onHandshakeHeaders;
3115
3121
  }
3116
3122
 
@@ -3172,7 +3178,7 @@ class CodexWebSocketConnection {
3172
3178
  this.#connectPromise = promise;
3173
3179
  const socket = new (WebSocket as unknown as new (url: string, opts: Bun.WebSocketOptions) => Bun.WebSocket)(
3174
3180
  this.#url,
3175
- { headers: this.#headers },
3181
+ { headers: this.#headers, proxy: this.#proxy },
3176
3182
  );
3177
3183
  socket.binaryType = "nodebuffer";
3178
3184
  this.#socket = socket;
@@ -3663,8 +3669,18 @@ async function getOrCreateCodexWebSocketConnection(
3663
3669
  state: CodexWebSocketSessionState,
3664
3670
  url: string,
3665
3671
  headers: Headers,
3672
+ provider: string,
3666
3673
  signal?: AbortSignal,
3667
3674
  ): Promise<CodexWebSocketConnection> {
3675
+ const targetUrl = new URL(url);
3676
+ const proxy = shouldBypassProxy(targetUrl)
3677
+ ? undefined
3678
+ : (getProxyForProvider(provider) ??
3679
+ (targetUrl.protocol === "wss:"
3680
+ ? Bun.env.HTTPS_PROXY || Bun.env.https_proxy
3681
+ : Bun.env.HTTP_PROXY || Bun.env.http_proxy) ??
3682
+ Bun.env.ALL_PROXY ??
3683
+ Bun.env.all_proxy);
3668
3684
  const headerRecord = headersToRecord(headers);
3669
3685
  // Join an in-flight handshake instead of tearing it down: closing a
3670
3686
  // CONNECTING socket rejects the concurrent caller (prewarm racing the first
@@ -3707,6 +3723,7 @@ async function getOrCreateCodexWebSocketConnection(
3707
3723
  onHandshakeHeaders: handshakeHeaders => {
3708
3724
  updateCodexSessionMetadataFromHeaders(state, handshakeHeaders);
3709
3725
  },
3726
+ proxy,
3710
3727
  });
3711
3728
  await state.connection.connect(signal);
3712
3729
  return state.connection;
@@ -76,6 +76,7 @@ import {
76
76
  applyOpenAIExtraBody,
77
77
  applyOpenAIGatewayRouting,
78
78
  applyOpenAIServiceTier,
79
+ applyOpenRouterReportedCost,
79
80
  applyWireModelIdTransform,
80
81
  calculateOpenAIUsageAccounting,
81
82
  clearOpenAIStrictToolsState,
@@ -1460,31 +1461,35 @@ function buildParams(
1460
1461
  params.store = false;
1461
1462
  }
1462
1463
 
1463
- if (options?.temperature !== undefined) {
1464
- params.temperature = options.temperature;
1465
- }
1466
- if (options?.topP !== undefined) {
1467
- params.top_p = options.topP;
1468
- }
1469
- if (options?.topK !== undefined) {
1470
- params.top_k = options.topK;
1471
- }
1472
- if (options?.minP !== undefined) {
1473
- params.min_p = options.minP;
1474
- }
1475
- if (options?.presencePenalty !== undefined) {
1476
- params.presence_penalty = options.presencePenalty;
1477
- }
1478
- if (options?.repetitionPenalty !== undefined) {
1479
- params.repetition_penalty = options.repetitionPenalty;
1464
+ // OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit
1465
+ // sampling params with a 400 on every serving host (#5606).
1466
+ if (initialCompat.supportsSamplingParams) {
1467
+ if (options?.temperature !== undefined) {
1468
+ params.temperature = options.temperature;
1469
+ }
1470
+ if (options?.topP !== undefined) {
1471
+ params.top_p = options.topP;
1472
+ }
1473
+ if (options?.topK !== undefined) {
1474
+ params.top_k = options.topK;
1475
+ }
1476
+ if (options?.minP !== undefined) {
1477
+ params.min_p = options.minP;
1478
+ }
1479
+ if (options?.presencePenalty !== undefined) {
1480
+ params.presence_penalty = options.presencePenalty;
1481
+ }
1482
+ if (options?.repetitionPenalty !== undefined) {
1483
+ params.repetition_penalty = options.repetitionPenalty;
1484
+ }
1485
+ if (options?.frequencyPenalty !== undefined) {
1486
+ params.frequency_penalty = options.frequencyPenalty;
1487
+ }
1480
1488
  }
1481
1489
  if (options?.stopSequences?.length) {
1482
1490
  const seqs = options.stopSequences;
1483
1491
  params.stop = seqs.length === 1 ? seqs[0] : seqs.slice(0, 4);
1484
1492
  }
1485
- if (options?.frequencyPenalty !== undefined) {
1486
- params.frequency_penalty = options.frequencyPenalty;
1487
- }
1488
1493
  applyOpenAIServiceTier(params, options?.serviceTier, model);
1489
1494
 
1490
1495
  if (context.tools?.length) {
@@ -1629,6 +1634,7 @@ export function parseChunkUsage(
1629
1634
  ...(premiumRequests !== undefined ? { premiumRequests } : {}),
1630
1635
  };
1631
1636
  calculateCost(model, usage);
1637
+ applyOpenRouterReportedCost(model, usage, rawUsage);
1632
1638
  return usage;
1633
1639
  }
1634
1640
 
@@ -25,6 +25,7 @@ import {
25
25
  classifyJsonPrefix,
26
26
  extractHttpStatusFromError,
27
27
  logger,
28
+ parseImageMetadata,
28
29
  parseStreamingJson,
29
30
  parseStreamingJsonThrottled,
30
31
  stringifyJson,
@@ -338,6 +339,29 @@ export function applyOpenAIResponsesServiceTierCost(
338
339
  usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
339
340
  }
340
341
 
342
+ /** Reconcile token-price estimates with OpenRouter's authoritative account charge. */
343
+ export function applyOpenRouterReportedCost(model: Pick<Model, "provider">, usage: Usage, rawUsage: unknown): void {
344
+ if (model.provider !== "openrouter" || typeof rawUsage !== "object" || rawUsage === null) return;
345
+ const reportedCost = Reflect.get(rawUsage, "cost");
346
+ if (typeof reportedCost !== "number" || !Number.isFinite(reportedCost) || reportedCost < 0) return;
347
+
348
+ const estimatedCost = usage.cost.total;
349
+ if (Number.isFinite(estimatedCost) && estimatedCost > 0) {
350
+ const scale = reportedCost / estimatedCost;
351
+ usage.cost.input *= scale;
352
+ usage.cost.output *= scale;
353
+ usage.cost.cacheRead *= scale;
354
+ usage.cost.cacheWrite *= scale;
355
+ } else {
356
+ // Keep legacy component-only aggregators additive when catalog pricing is unavailable.
357
+ usage.cost.input = reportedCost;
358
+ usage.cost.output = 0;
359
+ usage.cost.cacheRead = 0;
360
+ usage.cost.cacheWrite = 0;
361
+ }
362
+ usage.cost.total = reportedCost;
363
+ }
364
+
341
365
  export interface OpenAIUsageAccountingInput {
342
366
  promptTokens: number;
343
367
  outputTokens: number;
@@ -2532,6 +2556,19 @@ export async function processResponsesStream<TApi extends Api>(
2532
2556
  }
2533
2557
  closeOpenItem(event.output_index, item.id, entry, item.call_id, prefixedFunctionCallItemKey(item.call_id));
2534
2558
  stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
2559
+ } else if (item.type === "image_generation_call" && item.status === "completed" && item.result) {
2560
+ const image: ImageContent = {
2561
+ type: "image",
2562
+ data: item.result,
2563
+ mimeType: parseImageMetadata(Buffer.from(item.result, "base64"))?.mimeType ?? "image/png",
2564
+ };
2565
+ output.content.push(image);
2566
+ stream.push({
2567
+ type: "image_end",
2568
+ contentIndex: output.content.length - 1,
2569
+ content: image,
2570
+ partial: output,
2571
+ });
2535
2572
  }
2536
2573
  } else if (terminalEvent) {
2537
2574
  const response = terminalEvent.response;
@@ -2541,6 +2578,7 @@ export async function processResponsesStream<TApi extends Api>(
2541
2578
  }
2542
2579
  populateResponsesUsageFromResponse(output, response?.usage);
2543
2580
  calculateCost(model, output.usage);
2581
+ applyOpenRouterReportedCost(model, output.usage, response?.usage);
2544
2582
  applyOpenAIResponsesServiceTierCost(
2545
2583
  model,
2546
2584
  output.usage,
@@ -2717,7 +2755,9 @@ type CommonSamplingOptions = Pick<
2717
2755
  export function applyCommonResponsesSamplingParams<P extends CommonResponsesParams>(
2718
2756
  params: P,
2719
2757
  options: CommonSamplingOptions | undefined,
2720
- model: Pick<Model, "provider" | "api" | "id" | "omitMaxOutputTokens" | "maxTokens">,
2758
+ model: Pick<Model, "provider" | "api" | "id" | "omitMaxOutputTokens" | "maxTokens"> & {
2759
+ compat: Pick<ResolvedOpenAISharedCompat, "supportsSamplingParams">;
2760
+ },
2721
2761
  ): void {
2722
2762
  if (options?.maxTokens && !model.omitMaxOutputTokens) {
2723
2763
  params.max_output_tokens = Math.min(
@@ -2726,12 +2766,16 @@ export function applyCommonResponsesSamplingParams<P extends CommonResponsesPara
2726
2766
  OPENAI_MAX_OUTPUT_TOKENS,
2727
2767
  );
2728
2768
  }
2729
- if (options?.temperature !== undefined) params.temperature = options.temperature;
2730
- if (options?.topP !== undefined) params.top_p = options.topP;
2731
- if (options?.topK !== undefined) params.top_k = options.topK;
2732
- if (options?.minP !== undefined) params.min_p = options.minP;
2733
- if (options?.presencePenalty !== undefined) params.presence_penalty = options.presencePenalty;
2734
- if (options?.repetitionPenalty !== undefined) params.repetition_penalty = options.repetitionPenalty;
2769
+ // OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit
2770
+ // sampling params with a 400 on every serving host (#5606).
2771
+ if (model.compat.supportsSamplingParams) {
2772
+ if (options?.temperature !== undefined) params.temperature = options.temperature;
2773
+ if (options?.topP !== undefined) params.top_p = options.topP;
2774
+ if (options?.topK !== undefined) params.top_k = options.topK;
2775
+ if (options?.minP !== undefined) params.min_p = options.minP;
2776
+ if (options?.presencePenalty !== undefined) params.presence_penalty = options.presencePenalty;
2777
+ if (options?.repetitionPenalty !== undefined) params.repetition_penalty = options.repetitionPenalty;
2778
+ }
2735
2779
  applyOpenAIServiceTier(params, options?.serviceTier, model);
2736
2780
  }
2737
2781
 
@@ -554,6 +554,13 @@ export function transformMessages<TApi extends Api>(
554
554
  return [];
555
555
  }
556
556
 
557
+ if (block.type === "image") {
558
+ // Assistant images are display artifacts. No provider accepts them
559
+ // in an assistant replay turn; the native Responses result remains
560
+ // in providerPayload for OpenAI replay.
561
+ return [];
562
+ }
563
+
557
564
  if (block.type === "text") {
558
565
  if (isSameModel) return block;
559
566
  return {