@psnext/slingcli 2.5.20260623-1 → 2.5.20260629-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.
Files changed (108) hide show
  1. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.js +5 -2
  2. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/session.js +2 -1
  3. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/shell-output.js +1 -1
  4. package/node_modules/@earendil-works/pi-agent-core/package.json +2 -2
  5. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js +51 -47
  6. package/node_modules/@earendil-works/pi-ai/dist/api/azure-openai-responses.js +9 -3
  7. package/node_modules/@earendil-works/pi-ai/dist/api/bedrock-converse-stream.js +9 -6
  8. package/node_modules/@earendil-works/pi-ai/dist/api/cloudflare.js +0 -18
  9. package/node_modules/@earendil-works/pi-ai/dist/api/google-generative-ai.js +6 -3
  10. package/node_modules/@earendil-works/pi-ai/dist/api/google-vertex.js +6 -3
  11. package/node_modules/@earendil-works/pi-ai/dist/api/mistral-conversations.js +1 -1
  12. package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js +15 -25
  13. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js +30 -26
  14. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.js +189 -204
  15. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.js +24 -21
  16. package/node_modules/@earendil-works/pi-ai/dist/api/openrouter-images.js +2 -5
  17. package/node_modules/@earendil-works/pi-ai/dist/api/simple-options.js +11 -2
  18. package/node_modules/@earendil-works/pi-ai/dist/auth/helpers.js +2 -2
  19. package/node_modules/@earendil-works/pi-ai/dist/auth/resolve.js +19 -4
  20. package/node_modules/@earendil-works/pi-ai/dist/compat.js +81 -7
  21. package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js +45 -0
  22. package/node_modules/@earendil-works/pi-ai/dist/images-models.js +7 -3
  23. package/node_modules/@earendil-works/pi-ai/dist/index.js +1 -0
  24. package/node_modules/@earendil-works/pi-ai/dist/legacy-api-aliases.js +49 -0
  25. package/node_modules/@earendil-works/pi-ai/dist/models.js +7 -3
  26. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.models.js +25 -8
  27. package/node_modules/@earendil-works/pi-ai/dist/providers/ant-ling.models.js +3 -3
  28. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.models.js +0 -34
  29. package/node_modules/@earendil-works/pi-ai/dist/providers/cerebras.models.js +2 -0
  30. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-ai-gateway.js +2 -2
  31. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-ai-gateway.models.js +21 -9
  32. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-auth.js +85 -0
  33. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-workers-ai.js +2 -2
  34. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-workers-ai.models.js +13 -13
  35. package/node_modules/@earendil-works/pi-ai/dist/providers/deepseek.models.js +2 -2
  36. package/node_modules/@earendil-works/pi-ai/dist/providers/faux.js +1 -20
  37. package/node_modules/@earendil-works/pi-ai/dist/providers/fireworks.models.js +19 -1
  38. package/node_modules/@earendil-works/pi-ai/dist/providers/huggingface.models.js +72 -0
  39. package/node_modules/@earendil-works/pi-ai/dist/providers/minimax-cn.models.js +4 -4
  40. package/node_modules/@earendil-works/pi-ai/dist/providers/minimax.models.js +4 -4
  41. package/node_modules/@earendil-works/pi-ai/dist/providers/nvidia.models.js +19 -0
  42. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode-go.models.js +11 -11
  43. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode.models.js +15 -15
  44. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.js +310 -39
  45. package/node_modules/@earendil-works/pi-ai/dist/providers/together.models.js +37 -18
  46. package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.js +349 -77
  47. package/node_modules/@earendil-works/pi-ai/dist/providers/xai.models.js +7 -0
  48. package/node_modules/@earendil-works/pi-ai/dist/providers/zai-coding-cn.models.js +6 -6
  49. package/node_modules/@earendil-works/pi-ai/dist/providers/zai.models.js +6 -6
  50. package/node_modules/@earendil-works/pi-ai/dist/utils/estimate.js +95 -0
  51. package/node_modules/@earendil-works/pi-ai/dist/utils/headers.js +10 -0
  52. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/openai-codex.js +2 -2
  53. package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js +86 -0
  54. package/node_modules/@earendil-works/pi-ai/package.json +1 -33
  55. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/file-processor.js +13 -25
  56. package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js +15 -30
  57. package/node_modules/@earendil-works/pi-coding-agent/dist/core/compaction/compaction.js +6 -3
  58. package/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/loader.js +5 -1
  59. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-registry.js +2 -4
  60. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-resolver.js +1 -1
  61. package/node_modules/@earendil-works/pi-coding-agent/dist/core/resource-loader.js +2 -0
  62. package/node_modules/@earendil-works/pi-coding-agent/dist/core/session-manager.js +9 -5
  63. package/node_modules/@earendil-works/pi-coding-agent/dist/core/settings-manager.js +11 -0
  64. package/node_modules/@earendil-works/pi-coding-agent/dist/core/timings.js +24 -14
  65. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/read.js +12 -25
  66. package/node_modules/@earendil-works/pi-coding-agent/dist/main.js +20 -4
  67. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +9 -9
  68. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/extension-editor.js +12 -3
  69. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/session-selector.js +14 -3
  70. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/user-message.js +1 -1
  71. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js +37 -25
  72. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-client.js +14 -0
  73. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-mode.js +16 -0
  74. package/node_modules/@earendil-works/pi-coding-agent/dist/rpc-entry.js +10 -0
  75. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/image-convert.js +21 -15
  76. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/image-process.js +83 -0
  77. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/mime.js +41 -0
  78. package/node_modules/@earendil-works/pi-coding-agent/npm-shrinkwrap.json +12 -12
  79. package/node_modules/@earendil-works/pi-coding-agent/package.json +8 -5
  80. package/node_modules/@earendil-works/pi-orchestrator/dist/cli.js +136 -0
  81. package/node_modules/@earendil-works/pi-orchestrator/dist/config.js +55 -0
  82. package/node_modules/@earendil-works/pi-orchestrator/dist/handler.js +105 -0
  83. package/node_modules/@earendil-works/pi-orchestrator/dist/index.js +11 -0
  84. package/node_modules/@earendil-works/pi-orchestrator/dist/ipc/client.js +56 -0
  85. package/node_modules/@earendil-works/pi-orchestrator/dist/ipc/protocol.js +12 -0
  86. package/node_modules/@earendil-works/pi-orchestrator/dist/ipc/server.js +145 -0
  87. package/node_modules/@earendil-works/pi-orchestrator/dist/radius.js +365 -0
  88. package/node_modules/@earendil-works/pi-orchestrator/dist/rpc-process.js +168 -0
  89. package/node_modules/@earendil-works/pi-orchestrator/dist/serve.js +70 -0
  90. package/node_modules/@earendil-works/pi-orchestrator/dist/storage.js +58 -0
  91. package/node_modules/@earendil-works/pi-orchestrator/dist/supervisor.js +290 -0
  92. package/node_modules/@earendil-works/pi-orchestrator/dist/types.js +2 -0
  93. package/node_modules/@earendil-works/pi-orchestrator/package.json +45 -0
  94. package/node_modules/@earendil-works/pi-tui/dist/components/markdown.js +3 -0
  95. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  96. package/node_modules/@types/node/README.md +1 -1
  97. package/node_modules/@types/node/diagnostics_channel.d.ts +17 -20
  98. package/node_modules/@types/node/package.json +2 -2
  99. package/node_modules/brace-expansion/dist/commonjs/index.js +24 -14
  100. package/node_modules/brace-expansion/dist/commonjs/index.js.map +1 -1
  101. package/node_modules/brace-expansion/dist/esm/index.js +24 -14
  102. package/node_modules/brace-expansion/dist/esm/index.js.map +1 -1
  103. package/node_modules/brace-expansion/package.json +2 -2
  104. package/node_modules/google-auth-library/build/src/auth/googleauth.js +1 -1
  105. package/node_modules/google-auth-library/package.json +4 -4
  106. package/package.json +7 -6
  107. package/slingshot/index.js +302 -304
  108. package/node_modules/@earendil-works/pi-ai/dist/api-registry.js +0 -44
@@ -65,13 +65,16 @@ export function calculateContextTokens(usage) {
65
65
  function getAssistantUsage(msg) {
66
66
  if (msg.role === "assistant" && "usage" in msg) {
67
67
  const assistantMsg = msg;
68
- if (assistantMsg.stopReason !== "aborted" && assistantMsg.stopReason !== "error" && assistantMsg.usage) {
68
+ if (assistantMsg.stopReason !== "aborted" &&
69
+ assistantMsg.stopReason !== "error" &&
70
+ assistantMsg.usage &&
71
+ calculateContextTokens(assistantMsg.usage) > 0) {
69
72
  return assistantMsg.usage;
70
73
  }
71
74
  }
72
75
  return undefined;
73
76
  }
74
- /** Return usage from the last successful assistant message in session entries. */
77
+ /** Return usage from the last valid assistant message in session entries. */
75
78
  export function getLastAssistantUsage(entries) {
76
79
  for (let i = entries.length - 1; i >= 0; i--) {
77
80
  const entry = entries[i];
@@ -180,12 +180,13 @@ export class Session {
180
180
  });
181
181
  }
182
182
  async appendSessionName(name) {
183
+ const sanitizedName = name.replace(/[\r\n]+/g, " ").trim();
183
184
  return this.appendTypedEntry({
184
185
  type: "session_info",
185
186
  id: await this.storage.createEntryId(),
186
187
  parentId: await this.storage.getLeafId(),
187
188
  timestamp: new Date().toISOString(),
188
- name: name.trim(),
189
+ name: sanitizedName,
189
190
  });
190
191
  }
191
192
  async moveTo(entryId, summary) {
@@ -1,4 +1,4 @@
1
- import { ExecutionError, err, ok, toError, } from "../types.js";
1
+ import { ExecutionError, err, ok, toError } from "../types.js";
2
2
  import { DEFAULT_MAX_BYTES, truncateTail } from "./truncate.js";
3
3
  function toExecutionError(error) {
4
4
  if (error instanceof ExecutionError)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@earendil-works/pi-agent-core",
3
- "version": "0.79.10",
3
+ "version": "0.80.2",
4
4
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -29,7 +29,7 @@
29
29
  "prepublishOnly": "npm run clean && npm run build"
30
30
  },
31
31
  "dependencies": {
32
- "@earendil-works/pi-ai": "^0.79.10",
32
+ "@earendil-works/pi-ai": "^0.80.2",
33
33
  "ignore": "7.0.5",
34
34
  "typebox": "1.1.38",
35
35
  "yaml": "2.9.0"
@@ -5,9 +5,8 @@ import { headersToRecord } from "../utils/headers.js";
5
5
  import { parseJsonWithRepair, parseStreamingJson } from "../utils/json-parse.js";
6
6
  import { getProviderEnvValue } from "../utils/provider-env.js";
7
7
  import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
8
- import { resolveCloudflareBaseUrl } from "./cloudflare.js";
9
8
  import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
10
- import { adjustMaxTokensForThinking, buildBaseOptions } from "./simple-options.js";
9
+ import { adjustMaxTokensForThinking, buildBaseOptions, clampMaxTokensToContext } from "./simple-options.js";
11
10
  import { transformMessages } from "./transform-messages.js";
12
11
  /**
13
12
  * Resolve cache retention preference.
@@ -108,14 +107,11 @@ function convertContentBlocks(content) {
108
107
  const FINE_GRAINED_TOOL_STREAMING_BETA = "fine-grained-tool-streaming-2025-05-14";
109
108
  const INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14";
110
109
  function getAnthropicCompat(model) {
111
- // Auto-detect session affinity and cache control support from provider
112
- const isFireworks = model.provider === "fireworks";
113
- const isCloudflareAiGatewayAnthropic = model.provider === "cloudflare-ai-gateway" && model.baseUrl.includes("anthropic");
114
110
  return {
115
- supportsEagerToolInputStreaming: model.compat?.supportsEagerToolInputStreaming ?? !isFireworks,
116
- supportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? !isFireworks,
117
- sendSessionAffinityHeaders: model.compat?.sendSessionAffinityHeaders ?? !!(isFireworks || isCloudflareAiGatewayAnthropic),
118
- supportsCacheControlOnTools: model.compat?.supportsCacheControlOnTools ?? !isFireworks,
111
+ supportsEagerToolInputStreaming: model.compat?.supportsEagerToolInputStreaming ?? true,
112
+ supportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? true,
113
+ sendSessionAffinityHeaders: model.compat?.sendSessionAffinityHeaders ?? false,
114
+ supportsCacheControlOnTools: model.compat?.supportsCacheControlOnTools ?? true,
119
115
  supportsTemperature: model.compat?.supportsTemperature ?? true,
120
116
  allowEmptySignature: model.compat?.allowEmptySignature ?? false,
121
117
  };
@@ -129,6 +125,26 @@ function mergeHeaders(...headerSources) {
129
125
  }
130
126
  return merged;
131
127
  }
128
+ function hasHeader(headers, name) {
129
+ if (!headers)
130
+ return false;
131
+ const expected = name.toLowerCase();
132
+ for (const [key, value] of Object.entries(headers)) {
133
+ if (key.toLowerCase() === expected && value !== null && value.trim().length > 0)
134
+ return true;
135
+ }
136
+ return false;
137
+ }
138
+ function assertRequestAuth(provider, apiKey, headers) {
139
+ if (apiKey)
140
+ return;
141
+ if (hasHeader(headers, "authorization") ||
142
+ hasHeader(headers, "x-api-key") ||
143
+ hasHeader(headers, "cf-aig-authorization")) {
144
+ return;
145
+ }
146
+ throw new Error(`No API key for provider: ${provider}`);
147
+ }
132
148
  const ANTHROPIC_MESSAGE_EVENTS = new Set([
133
149
  "message_start",
134
150
  "message_delta",
@@ -309,9 +325,7 @@ export const stream = (model, context, options) => {
309
325
  }
310
326
  else {
311
327
  const apiKey = options?.apiKey;
312
- if (!apiKey) {
313
- throw new Error(`No API key for provider: ${model.provider}`);
314
- }
328
+ assertRequestAuth(model.provider, apiKey, options?.headers);
315
329
  let copilotDynamicHeaders;
316
330
  if (model.provider === "github-copilot") {
317
331
  const hasImages = hasCopilotVisionInput(context.messages);
@@ -322,7 +336,7 @@ export const stream = (model, context, options) => {
322
336
  }
323
337
  const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env);
324
338
  const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
325
- const created = createClient(model, apiKey, options?.interleavedThinking ?? true, shouldUseFineGrainedToolStreamingBeta(model, context), options?.headers, copilotDynamicHeaders, cacheSessionId, options?.env);
339
+ const created = createClient(model, apiKey, options?.interleavedThinking ?? true, shouldUseFineGrainedToolStreamingBeta(model, context), options?.headers, copilotDynamicHeaders, cacheSessionId);
326
340
  client = created.client;
327
341
  isOAuth = created.isOAuthToken;
328
342
  }
@@ -508,6 +522,14 @@ export const stream = (model, context, options) => {
508
522
  if (event.usage.cache_creation_input_tokens != null) {
509
523
  output.usage.cacheWrite = event.usage.cache_creation_input_tokens;
510
524
  }
525
+ // Anthropic reports reasoning tokens in `output_tokens_details.thinking_tokens` on the
526
+ // final message_delta usage (a subset of output_tokens). SDK 0.91.1 omits the field from
527
+ // its Usage type, so read it through a narrow cast. Verified against the live API.
528
+ const thinkingTokens = event.usage
529
+ .output_tokens_details?.thinking_tokens;
530
+ if (thinkingTokens != null) {
531
+ output.usage.reasoning = thinkingTokens;
532
+ }
511
533
  // Anthropic doesn't provide total_tokens, compute from components
512
534
  output.usage.totalTokens =
513
535
  output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
@@ -558,11 +580,8 @@ function mapThinkingLevelToEffort(model, level) {
558
580
  }
559
581
  }
560
582
  export const streamSimple = (model, context, options) => {
561
- const apiKey = options?.apiKey;
562
- if (!apiKey) {
563
- throw new Error(`No API key for provider: ${model.provider}`);
564
- }
565
- const base = buildBaseOptions(model, options, apiKey);
583
+ assertRequestAuth(model.provider, options?.apiKey, options?.headers);
584
+ const base = buildBaseOptions(model, context, options, options?.apiKey);
566
585
  if (!options?.reasoning) {
567
586
  return stream(model, context, { ...base, thinkingEnabled: false });
568
587
  }
@@ -579,17 +598,18 @@ export const streamSimple = (model, context, options) => {
579
598
  // Undefined means the caller did not request an output cap; let the helper use the model cap.
580
599
  // Do not coerce to 0 here, or the thinking budget would become the entire max_tokens value.
581
600
  const adjusted = adjustMaxTokensForThinking(base.maxTokens, model.maxTokens, options.reasoning, options.thinkingBudgets);
601
+ const maxTokens = clampMaxTokensToContext(model, context, adjusted.maxTokens);
582
602
  return stream(model, context, {
583
603
  ...base,
584
- maxTokens: adjusted.maxTokens,
604
+ maxTokens,
585
605
  thinkingEnabled: true,
586
- thinkingBudgetTokens: adjusted.thinkingBudget,
606
+ thinkingBudgetTokens: Math.min(adjusted.thinkingBudget, Math.max(0, maxTokens - 1024)),
587
607
  });
588
608
  };
589
609
  function isOAuthToken(apiKey) {
590
610
  return apiKey.includes("sk-ant-oat");
591
611
  }
592
- function createClient(model, apiKey, interleavedThinking, useFineGrainedToolStreamingBeta, optionsHeaders, dynamicHeaders, sessionId, env) {
612
+ function createClient(model, apiKey, interleavedThinking, useFineGrainedToolStreamingBeta, optionsHeaders, dynamicHeaders, sessionId) {
593
613
  // Adaptive thinking models have interleaved thinking built in, so skip the beta header.
594
614
  const needsInterleavedBeta = interleavedThinking && model.compat?.forceAdaptiveThinking !== true;
595
615
  const betaFeatures = [];
@@ -599,28 +619,11 @@ function createClient(model, apiKey, interleavedThinking, useFineGrainedToolStre
599
619
  if (needsInterleavedBeta) {
600
620
  betaFeatures.push(INTERLEAVED_THINKING_BETA);
601
621
  }
602
- if (model.provider === "cloudflare-ai-gateway") {
603
- const client = new Anthropic({
604
- apiKey: null,
605
- authToken: null,
606
- baseURL: resolveCloudflareBaseUrl(model, env),
607
- dangerouslyAllowBrowser: true,
608
- defaultHeaders: mergeHeaders({
609
- accept: "application/json",
610
- "anthropic-dangerous-direct-browser-access": "true",
611
- "cf-aig-authorization": `Bearer ${apiKey}`,
612
- "x-api-key": null,
613
- Authorization: null,
614
- ...(betaFeatures.length > 0 ? { "anthropic-beta": betaFeatures.join(",") } : {}),
615
- }, model.headers, optionsHeaders),
616
- });
617
- return { client, isOAuthToken: false };
618
- }
619
622
  // Copilot: Bearer auth, selective betas.
620
623
  if (model.provider === "github-copilot") {
621
624
  const client = new Anthropic({
622
625
  apiKey: null,
623
- authToken: apiKey,
626
+ authToken: apiKey ?? null,
624
627
  baseURL: model.baseUrl,
625
628
  dangerouslyAllowBrowser: true,
626
629
  defaultHeaders: mergeHeaders({
@@ -632,7 +635,7 @@ function createClient(model, apiKey, interleavedThinking, useFineGrainedToolStre
632
635
  return { client, isOAuthToken: false };
633
636
  }
634
637
  // OAuth: Bearer auth, Claude Code identity headers
635
- if (isOAuthToken(apiKey)) {
638
+ if (apiKey && isOAuthToken(apiKey)) {
636
639
  const client = new Anthropic({
637
640
  apiKey: null,
638
641
  authToken: apiKey,
@@ -648,18 +651,19 @@ function createClient(model, apiKey, interleavedThinking, useFineGrainedToolStre
648
651
  });
649
652
  return { client, isOAuthToken: true };
650
653
  }
651
- // API key auth
654
+ // API key or header-owned auth.
652
655
  const sessionAffinityHeaders = sessionId && getAnthropicCompat(model).sendSessionAffinityHeaders ? { "x-session-affinity": sessionId } : {};
656
+ const defaultHeaders = mergeHeaders({
657
+ accept: "application/json",
658
+ "anthropic-dangerous-direct-browser-access": "true",
659
+ ...(betaFeatures.length > 0 ? { "anthropic-beta": betaFeatures.join(",") } : {}),
660
+ }, sessionAffinityHeaders, model.headers, optionsHeaders);
653
661
  const client = new Anthropic({
654
- apiKey,
662
+ apiKey: apiKey ?? null,
655
663
  authToken: null,
656
664
  baseURL: model.baseUrl,
657
665
  dangerouslyAllowBrowser: true,
658
- defaultHeaders: mergeHeaders({
659
- accept: "application/json",
660
- "anthropic-dangerous-direct-browser-access": "true",
661
- ...(betaFeatures.length > 0 ? { "anthropic-beta": betaFeatures.join(",") } : {}),
662
- }, sessionAffinityHeaders, model.headers, optionsHeaders),
666
+ defaultHeaders,
663
667
  });
664
668
  return { client, isOAuthToken: false };
665
669
  }
@@ -120,7 +120,7 @@ export const streamSimple = (model, context, options) => {
120
120
  if (!apiKey) {
121
121
  throw new Error(`No API key for provider: ${model.provider}`);
122
122
  }
123
- const base = buildBaseOptions(model, options, apiKey);
123
+ const base = buildBaseOptions(model, context, options, apiKey);
124
124
  const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
125
125
  const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning;
126
126
  return stream(model, context, {
@@ -137,11 +137,17 @@ function normalizeAzureBaseUrl(baseUrl) {
137
137
  catch {
138
138
  throw new Error(`Invalid Azure OpenAI base URL: ${baseUrl}`);
139
139
  }
140
- const isAzureHost = url.hostname.endsWith(".openai.azure.com") || url.hostname.endsWith(".cognitiveservices.azure.com");
140
+ const isAzureHost = url.hostname.endsWith(".openai.azure.com") ||
141
+ url.hostname.endsWith(".cognitiveservices.azure.com") ||
142
+ url.hostname.endsWith(".ai.azure.com");
141
143
  const normalizedPath = url.pathname.replace(/\/+$/, "");
142
144
  // Ensure Azure hosts have /openai/v1 as base path so the AzureOpenAI SDK
143
145
  // can append /deployments/<model>/... and ?api-version=v1 correctly.
144
- if (isAzureHost && (normalizedPath === "" || normalizedPath === "/" || normalizedPath === "/openai")) {
146
+ if (isAzureHost &&
147
+ (normalizedPath === "" ||
148
+ normalizedPath === "/" ||
149
+ normalizedPath === "/openai" ||
150
+ normalizedPath === "/openai/v1/responses")) {
145
151
  url.pathname = "/openai/v1";
146
152
  url.search = "";
147
153
  }
@@ -4,11 +4,12 @@ import { HttpProxyAgent } from "http-proxy-agent";
4
4
  import { HttpsProxyAgent } from "https-proxy-agent";
5
5
  import { calculateCost } from "../models.js";
6
6
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
7
+ import { providerHeadersToRecord } from "../utils/headers.js";
7
8
  import { parseStreamingJson } from "../utils/json-parse.js";
8
9
  import { resolveHttpProxyUrlForTarget } from "../utils/node-http-proxy.js";
9
10
  import { getProviderEnvValue } from "../utils/provider-env.js";
10
11
  import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
11
- import { adjustMaxTokensForThinking, buildBaseOptions, clampReasoning } from "./simple-options.js";
12
+ import { adjustMaxTokensForThinking, buildBaseOptions, clampMaxTokensToContext, clampReasoning, } from "./simple-options.js";
12
13
  import { transformMessages } from "./transform-messages.js";
13
14
  const EMPTY_TEXT_PLACEHOLDER = "<empty>";
14
15
  export const stream = (model, context, options = {}) => {
@@ -105,8 +106,9 @@ export const stream = (model, context, options = {}) => {
105
106
  }
106
107
  try {
107
108
  const client = new BedrockRuntimeClient(config);
108
- if (options.headers && Object.keys(options.headers).length > 0) {
109
- addCustomHeadersMiddleware(client, options.headers);
109
+ const customHeaders = providerHeadersToRecord(options.headers);
110
+ if (customHeaders) {
111
+ addCustomHeadersMiddleware(client, customHeaders);
110
112
  }
111
113
  const cacheRetention = resolveCacheRetention(options.cacheRetention, options.env);
112
114
  const inferenceMaxTokens = options.maxTokens ?? (isAnthropicClaudeModel(model) ? model.maxTokens : undefined);
@@ -267,7 +269,7 @@ function addCustomHeadersMiddleware(client, headers) {
267
269
  client.middlewareStack.add(middleware, { step: "build", name: "pi-ai-custom-headers", priority: "low" });
268
270
  }
269
271
  export const streamSimple = (model, context, options) => {
270
- const base = buildBaseOptions(model, options, undefined);
272
+ const base = buildBaseOptions(model, context, options, undefined);
271
273
  if (!options?.reasoning) {
272
274
  return stream(model, context, { ...base, reasoning: undefined });
273
275
  }
@@ -282,13 +284,14 @@ export const streamSimple = (model, context, options) => {
282
284
  // Undefined means the caller did not request an output cap; let the helper use the model cap.
283
285
  // Do not coerce to 0 here, or the thinking budget would become the entire maxTokens value.
284
286
  const adjusted = adjustMaxTokensForThinking(base.maxTokens, model.maxTokens, options.reasoning, options.thinkingBudgets);
287
+ const maxTokens = clampMaxTokensToContext(model, context, adjusted.maxTokens);
285
288
  return stream(model, context, {
286
289
  ...base,
287
- maxTokens: adjusted.maxTokens,
290
+ maxTokens,
288
291
  reasoning: options.reasoning,
289
292
  thinkingBudgets: {
290
293
  ...(options.thinkingBudgets || {}),
291
- [clampReasoning(options.reasoning)]: adjusted.thinkingBudget,
294
+ [clampReasoning(options.reasoning)]: Math.min(adjusted.thinkingBudget, Math.max(0, maxTokens - 1024)),
292
295
  },
293
296
  });
294
297
  }
@@ -1,4 +1,3 @@
1
- import { getProviderEnvValue } from "../utils/provider-env.js";
2
1
  /** Workers AI direct endpoint. */
3
2
  export const CLOUDFLARE_WORKERS_AI_BASE_URL = "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1";
4
3
  /** AI Gateway Unified API. https://developers.cloudflare.com/ai-gateway/usage/unified-api/ */
@@ -7,21 +6,4 @@ export const CLOUDFLARE_AI_GATEWAY_COMPAT_BASE_URL = "https://gateway.ai.cloudfl
7
6
  export const CLOUDFLARE_AI_GATEWAY_OPENAI_BASE_URL = "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai";
8
7
  /** AI Gateway → Anthropic passthrough. */
9
8
  export const CLOUDFLARE_AI_GATEWAY_ANTHROPIC_BASE_URL = "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic";
10
- export function isCloudflareProvider(provider) {
11
- return provider === "cloudflare-workers-ai" || provider === "cloudflare-ai-gateway";
12
- }
13
- /** Substitute `{VAR}` placeholders in a Cloudflare baseUrl from provider env or process.env. */
14
- export function resolveCloudflareBaseUrl(model, env) {
15
- const url = model.baseUrl;
16
- if (!url.includes("{"))
17
- return url;
18
- const baseUrl = url.replace(/\{([A-Z_][A-Z0-9_]*)\}/g, (_match, name) => {
19
- const value = getProviderEnvValue(name, env);
20
- if (!value) {
21
- throw new Error(`${name} is required for provider ${model.provider} but is not set.`);
22
- }
23
- return value;
24
- });
25
- return baseUrl;
26
- }
27
9
  //# sourceMappingURL=cloudflare.js.map
@@ -1,6 +1,7 @@
1
1
  import { GoogleGenAI, } from "@google/genai";
2
2
  import { calculateCost, clampThinkingLevel } from "../models.js";
3
3
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
4
+ import { providerHeadersToRecord } from "../utils/headers.js";
4
5
  import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
5
6
  import { convertMessages, convertTools, isThinkingPart, mapStopReason, mapToolChoice, retainThoughtSignature, } from "./google-shared.js";
6
7
  import { buildBaseOptions } from "./simple-options.js";
@@ -161,6 +162,7 @@ export const stream = (model, context, options) => {
161
162
  output: (chunk.usageMetadata.candidatesTokenCount || 0) + (chunk.usageMetadata.thoughtsTokenCount || 0),
162
163
  cacheRead: chunk.usageMetadata.cachedContentTokenCount || 0,
163
164
  cacheWrite: 0,
165
+ reasoning: chunk.usageMetadata.thoughtsTokenCount || 0,
164
166
  totalTokens: chunk.usageMetadata.totalTokenCount || 0,
165
167
  cost: {
166
168
  input: 0,
@@ -220,7 +222,7 @@ export const streamSimple = (model, context, options) => {
220
222
  if (!apiKey) {
221
223
  throw new Error(`No API key for provider: ${model.provider}`);
222
224
  }
223
- const base = buildBaseOptions(model, options, apiKey);
225
+ const base = buildBaseOptions(model, context, options, apiKey);
224
226
  if (!options?.reasoning) {
225
227
  return stream(model, context, { ...base, thinking: { enabled: false } });
226
228
  }
@@ -250,8 +252,9 @@ function createClient(model, apiKey, optionsHeaders) {
250
252
  httpOptions.baseUrl = model.baseUrl;
251
253
  httpOptions.apiVersion = ""; // baseUrl already includes version path, don't append
252
254
  }
253
- if (model.headers || optionsHeaders) {
254
- httpOptions.headers = { ...model.headers, ...optionsHeaders };
255
+ const headers = providerHeadersToRecord({ ...model.headers, ...optionsHeaders });
256
+ if (headers) {
257
+ httpOptions.headers = headers;
255
258
  }
256
259
  return new GoogleGenAI({
257
260
  apiKey,
@@ -1,6 +1,7 @@
1
1
  import { GoogleGenAI, ResourceScope, ThinkingLevel, } from "@google/genai";
2
2
  import { calculateCost, clampThinkingLevel } from "../models.js";
3
3
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
4
+ import { providerHeadersToRecord } from "../utils/headers.js";
4
5
  import { getProviderEnvValue } from "../utils/provider-env.js";
5
6
  import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
6
7
  import { convertMessages, convertTools, isThinkingPart, mapStopReason, mapToolChoice, retainThoughtSignature, } from "./google-shared.js";
@@ -170,6 +171,7 @@ export const stream = (model, context, options) => {
170
171
  output: (chunk.usageMetadata.candidatesTokenCount || 0) + (chunk.usageMetadata.thoughtsTokenCount || 0),
171
172
  cacheRead: chunk.usageMetadata.cachedContentTokenCount || 0,
172
173
  cacheWrite: 0,
174
+ reasoning: chunk.usageMetadata.thoughtsTokenCount || 0,
173
175
  totalTokens: chunk.usageMetadata.totalTokenCount || 0,
174
176
  cost: {
175
177
  input: 0,
@@ -225,7 +227,7 @@ export const stream = (model, context, options) => {
225
227
  return stream;
226
228
  };
227
229
  export const streamSimple = (model, context, options) => {
228
- const base = buildBaseOptions(model, options, undefined);
230
+ const base = buildBaseOptions(model, context, options, undefined);
229
231
  if (!options?.reasoning) {
230
232
  return stream(model, context, {
231
233
  ...base,
@@ -281,8 +283,9 @@ function buildHttpOptions(model, optionsHeaders) {
281
283
  httpOptions.apiVersion = "";
282
284
  }
283
285
  }
284
- if (model.headers || optionsHeaders) {
285
- httpOptions.headers = { ...model.headers, ...optionsHeaders };
286
+ const headers = providerHeadersToRecord({ ...model.headers, ...optionsHeaders });
287
+ if (headers) {
288
+ httpOptions.headers = headers;
286
289
  }
287
290
  return Object.keys(httpOptions).length > 0 ? httpOptions : undefined;
288
291
  }
@@ -65,7 +65,7 @@ export const streamSimple = (model, context, options) => {
65
65
  if (!apiKey) {
66
66
  throw new Error(`No API key for provider: ${model.provider}`);
67
67
  }
68
- const base = buildBaseOptions(model, options, apiKey);
68
+ const base = buildBaseOptions(model, context, options, apiKey);
69
69
  const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
70
70
  const reasoning = clampedReasoning === "off" ? undefined : clampedReasoning;
71
71
  const shouldUseReasoning = model.reasoning && reasoning !== undefined;
@@ -33,9 +33,6 @@ const JWT_CLAIM_PATH = "https://api.openai.com/auth";
33
33
  const DEFAULT_MAX_RETRIES = 0;
34
34
  const BASE_DELAY_MS = 1000;
35
35
  const DEFAULT_MAX_RETRY_DELAY_MS = 60_000;
36
- // Keep a bounded pre-header timeout so zero-event Codex SSE stalls fail instead of
37
- // leaving callers stuck on "Working..." indefinitely. See #4945.
38
- const DEFAULT_SSE_HEADER_TIMEOUT_MS = 20_000;
39
36
  const DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS = 15_000;
40
37
  const CODEX_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
41
38
  const WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE = 1009;
@@ -110,19 +107,6 @@ function normalizeTimeoutMs(value) {
110
107
  }
111
108
  return Math.floor(value);
112
109
  }
113
- function createSSEHeaderTimeout() {
114
- const controller = new AbortController();
115
- let error;
116
- const timeout = setTimeout(() => {
117
- error = new Error(`Codex SSE response headers timed out after ${DEFAULT_SSE_HEADER_TIMEOUT_MS}ms`);
118
- controller.abort(error);
119
- }, DEFAULT_SSE_HEADER_TIMEOUT_MS);
120
- return {
121
- signal: controller.signal,
122
- clear: () => clearTimeout(timeout),
123
- error: () => error,
124
- };
125
- }
126
110
  // ============================================================================
127
111
  // Main Stream Function
128
112
  // ============================================================================
@@ -161,7 +145,7 @@ export const stream = (model, context, options) => {
161
145
  const sseHeaders = buildSSEHeaders(model.headers, options?.headers, accountId, apiKey, options?.sessionId);
162
146
  const websocketHeaders = buildWebSocketHeaders(model.headers, options?.headers, accountId, apiKey, websocketRequestId);
163
147
  const bodyJson = JSON.stringify(body);
164
- const idleTimeoutMs = normalizeTimeoutMs(options?.timeoutMs);
148
+ const httpTimeoutMs = normalizeTimeoutMs(options?.timeoutMs);
165
149
  const websocketConnectTimeoutMs = normalizeTimeoutMs(options?.websocketConnectTimeoutMs);
166
150
  const transport = options?.transport || "auto";
167
151
  const websocketDisabledForSession = transport !== "sse" && isWebSocketSseFallbackActive(options?.sessionId);
@@ -176,7 +160,7 @@ export const stream = (model, context, options) => {
176
160
  try {
177
161
  await processWebSocketStream(resolveCodexWebSocketUrl(model.baseUrl), body, websocketHeaders, output, stream, model, () => {
178
162
  websocketStarted = true;
179
- }, idleTimeoutMs, websocketConnectTimeoutMs, options);
163
+ }, httpTimeoutMs, websocketConnectTimeoutMs, options);
180
164
  if (options?.signal?.aborted) {
181
165
  throw new Error("Request was aborted");
182
166
  }
@@ -223,8 +207,8 @@ export const stream = (model, context, options) => {
223
207
  throw new Error("Request was aborted");
224
208
  }
225
209
  try {
226
- const headerTimeout = createSSEHeaderTimeout();
227
- const combinedSignal = combineAbortSignals([options?.signal, headerTimeout.signal]);
210
+ const headerTimeoutSignal = httpTimeoutMs !== undefined && httpTimeoutMs > 0 ? AbortSignal.timeout(httpTimeoutMs) : undefined;
211
+ const combinedSignal = combineAbortSignals([options?.signal, headerTimeoutSignal]);
228
212
  try {
229
213
  response = await fetch(resolveCodexUrl(model.baseUrl), {
230
214
  method: "POST",
@@ -234,12 +218,13 @@ export const stream = (model, context, options) => {
234
218
  });
235
219
  }
236
220
  catch (error) {
237
- const timeoutError = headerTimeout.error();
238
- throw timeoutError && !options?.signal?.aborted ? timeoutError : error;
221
+ if (headerTimeoutSignal?.aborted && !options?.signal?.aborted) {
222
+ throw new Error(`Codex SSE response headers timed out after ${httpTimeoutMs}ms`);
223
+ }
224
+ throw error;
239
225
  }
240
226
  finally {
241
227
  combinedSignal.cleanup();
242
- headerTimeout.clear();
243
228
  }
244
229
  await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
245
230
  if (response.ok) {
@@ -312,7 +297,7 @@ export const streamSimple = (model, context, options) => {
312
297
  if (!apiKey) {
313
298
  throw new Error(`No API key for provider: ${model.provider}`);
314
299
  }
315
- const base = buildBaseOptions(model, options, apiKey);
300
+ const base = buildBaseOptions(model, context, options, apiKey);
316
301
  const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
317
302
  const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning;
318
303
  return stream(model, context, {
@@ -1164,7 +1149,12 @@ function createCodexRequestId() {
1164
1149
  function buildBaseCodexHeaders(initHeaders, additionalHeaders, accountId, token) {
1165
1150
  const headers = new Headers(initHeaders);
1166
1151
  for (const [key, value] of Object.entries(additionalHeaders || {})) {
1167
- headers.set(key, value);
1152
+ if (value === null) {
1153
+ headers.delete(key);
1154
+ }
1155
+ else {
1156
+ headers.set(key, value);
1157
+ }
1168
1158
  }
1169
1159
  headers.set("Authorization", `Bearer ${token}`);
1170
1160
  headers.set("chatgpt-account-id", accountId);