@bitkyc08/opencodex 2.87.0 → 2.89.0

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 (246) hide show
  1. package/README.md +11 -1
  2. package/bin/ocx.mjs +15 -4
  3. package/gui/dist/assets/App-BnRSujOq.js +58 -0
  4. package/gui/dist/assets/App-dnkJ85te.css +1 -0
  5. package/gui/dist/assets/{Tray-DCd-A1az.js → Tray-7z1COnmz.js} +1 -1
  6. package/gui/dist/assets/index-uchD-ngf.js +94 -0
  7. package/gui/dist/assets/{quota-summary-ChvmKGOk.js → quota-summary-BY_10IEz.js} +1 -1
  8. package/gui/dist/index.html +1 -1
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/cache-retention.ts +117 -0
  11. package/src/adapters/anthropic/passthrough.ts +7 -4
  12. package/src/adapters/anthropic-rejection-memo.ts +71 -0
  13. package/src/adapters/anthropic-replay.ts +135 -0
  14. package/src/adapters/anthropic-tool-catalog.ts +11 -0
  15. package/src/adapters/anthropic.ts +86 -48
  16. package/src/adapters/coding-agent/protocol.ts +6 -0
  17. package/src/adapters/google-http.ts +3 -2
  18. package/src/adapters/google-progress-history.ts +53 -0
  19. package/src/adapters/google-vertex-endpoint.ts +67 -0
  20. package/src/adapters/google.ts +52 -30
  21. package/src/adapters/openai-responses/compaction-search-history.ts +27 -0
  22. package/src/adapters/openai-responses/passthrough.ts +17 -12
  23. package/src/adapters/openai-responses/reasoning.ts +68 -1
  24. package/src/adapters/registry.ts +1 -1
  25. package/src/adapters/upstream-http-error.ts +3 -2
  26. package/src/adapters/zed.ts +7 -4
  27. package/src/bridge/internal.ts +17 -3
  28. package/src/bridge/response-json.ts +4 -4
  29. package/src/bridge/sse.ts +17 -10
  30. package/src/claude/inbound-content-options.ts +66 -0
  31. package/src/claude/inbound.ts +12 -26
  32. package/src/claude/outbound.ts +18 -5
  33. package/src/cli/account-api.ts +13 -1
  34. package/src/cli/account-extended.ts +17 -5
  35. package/src/cli/account-key-quota.ts +4 -1
  36. package/src/cli/account.ts +1 -1
  37. package/src/cli/capabilities-agents-routing.ts +45 -8
  38. package/src/cli/capabilities-base.ts +5 -5
  39. package/src/cli/capabilities-provider-models.ts +5 -4
  40. package/src/cli/combo-result.ts +8 -2
  41. package/src/cli/combo.ts +17 -0
  42. package/src/cli/cross-home-owner.ts +16 -2
  43. package/src/cli/help.ts +1 -1
  44. package/src/cli/loopback-family.ts +31 -0
  45. package/src/cli/message-command.ts +4 -1
  46. package/src/cli/message-remote-args.ts +69 -0
  47. package/src/cli/message-remote-command.ts +77 -0
  48. package/src/cli/provider-runtime.ts +69 -1
  49. package/src/cli/registry.ts +3 -3
  50. package/src/codex/account-pause.ts +6 -1
  51. package/src/codex/account-runtime-state.ts +5 -0
  52. package/src/codex/account-store.ts +30 -3
  53. package/src/codex/auth-api/account-list.ts +3 -0
  54. package/src/codex/auth-api/main-account-probe.ts +3 -2
  55. package/src/codex/auth-context.ts +9 -4
  56. package/src/codex/catalog/aggregation.ts +15 -0
  57. package/src/codex/catalog/auto-review.ts +2 -1
  58. package/src/codex/catalog/build-entries.ts +17 -7
  59. package/src/codex/catalog/derive-entry.ts +3 -1
  60. package/src/codex/catalog/metadata.ts +2 -0
  61. package/src/codex/catalog/model-visibility.ts +0 -2
  62. package/src/codex/catalog/ownership.ts +71 -0
  63. package/src/codex/catalog/parsing.ts +12 -3
  64. package/src/codex/catalog/retained-sync.ts +2 -1
  65. package/src/codex/catalog/routed-gather.ts +3 -1
  66. package/src/codex/catalog/routed-removal.ts +4 -4
  67. package/src/codex/convergence.ts +2 -1
  68. package/src/codex/low-quota-desktop-notifier.ts +125 -0
  69. package/src/codex/low-quota-protection.ts +57 -31
  70. package/src/codex/main-account-cache.ts +77 -3
  71. package/src/codex/main-account.ts +6 -2
  72. package/src/codex/native-residue.ts +2 -3
  73. package/src/codex/quota.ts +26 -2
  74. package/src/codex/websocket-activity.ts +161 -0
  75. package/src/codex/websocket-registry.ts +25 -1
  76. package/src/combos/failover.ts +2 -1
  77. package/src/combos/jev-model-backend.ts +30 -7
  78. package/src/combos/jev-model-request.ts +20 -0
  79. package/src/combos/jev-quota-activation.ts +153 -0
  80. package/src/combos/jev-quota-config.ts +62 -0
  81. package/src/combos/jev-quota-route.ts +26 -0
  82. package/src/combos/jev-quota.ts +88 -0
  83. package/src/combos/jev.ts +25 -12
  84. package/src/combos/types.ts +27 -0
  85. package/src/compatibility/index.ts +3 -0
  86. package/src/compatibility/openai-responses.ts +8 -1
  87. package/src/compatibility/opengateway-responses.ts +61 -0
  88. package/src/config/account-auto-pause-revisions.ts +136 -0
  89. package/src/config/diagnostics.ts +13 -0
  90. package/src/config/live-reconcile.ts +9 -0
  91. package/src/config/opencode-zen-upgrade.ts +9 -8
  92. package/src/config/persist-unlocked.ts +10 -1
  93. package/src/config/removed-opencode-free.ts +135 -0
  94. package/src/config/schema/account-auto-pause.ts +14 -0
  95. package/src/config/schema/config-schema.ts +15 -0
  96. package/src/config/schema/leaf-validators.ts +7 -0
  97. package/src/config.ts +10 -3
  98. package/src/generated/compatibility-version.json +411 -175
  99. package/src/images/loop.ts +101 -27
  100. package/src/lib/account-auto-pause-response.ts +16 -0
  101. package/src/lib/bun-runtime.ts +18 -1
  102. package/src/lib/bun-transpiler-cache-scope.d.mts +8 -0
  103. package/src/lib/bun-transpiler-cache-scope.mjs +68 -0
  104. package/src/lib/decision-quota-sink.ts +76 -0
  105. package/src/lib/request-failure-model.ts +2 -0
  106. package/src/lib/state-store-registrations.ts +25 -0
  107. package/src/link/ssh-argv.ts +3 -0
  108. package/src/messaging/envelope.ts +6 -3
  109. package/src/messaging/remote-auth.ts +83 -0
  110. package/src/messaging/remote-bridge.ts +270 -0
  111. package/src/messaging/remote-contract.ts +108 -0
  112. package/src/messaging/remote-enrollment-recovery.ts +59 -0
  113. package/src/messaging/remote-enrollment.ts +244 -0
  114. package/src/messaging/remote-envelope.ts +40 -0
  115. package/src/messaging/remote-files.ts +117 -0
  116. package/src/messaging/remote-owner.ts +169 -0
  117. package/src/messaging/remote-ports.ts +56 -0
  118. package/src/messaging/remote-process.ts +131 -0
  119. package/src/messaging/remote-replies.ts +98 -0
  120. package/src/messaging/remote-rpc-admission.ts +32 -0
  121. package/src/messaging/remote-send.ts +72 -0
  122. package/src/messaging/remote-store.ts +111 -0
  123. package/src/messaging/remote-tunnels.ts +65 -0
  124. package/src/messaging/rpc.ts +13 -1
  125. package/src/oauth/account-auto-pause-policy.ts +66 -0
  126. package/src/oauth/account-auto-pause.ts +171 -0
  127. package/src/oauth/account-quota-rank.ts +2 -1
  128. package/src/oauth/anthropic-account-refusal.ts +6 -0
  129. package/src/oauth/anthropic-model-family.ts +8 -0
  130. package/src/oauth/anthropic-model-quota.ts +2 -6
  131. package/src/oauth/api-key-pool-preserve.ts +37 -0
  132. package/src/oauth/generic-account-failover.ts +7 -4
  133. package/src/oauth/index.ts +5 -35
  134. package/src/oauth/store.ts +35 -8
  135. package/src/oauth/types.ts +2 -0
  136. package/src/plugins/upstream-hooks.ts +14 -1
  137. package/src/protocols/encoders/adapter-events.ts +5 -5
  138. package/src/protocols/encoders/messages.ts +2 -2
  139. package/src/providers/api-key-pause.ts +32 -0
  140. package/src/providers/api-key-resolve.ts +10 -4
  141. package/src/providers/api-key-selection.ts +6 -1
  142. package/src/providers/api-keys.ts +8 -4
  143. package/src/providers/claude-cli-usage.ts +5 -0
  144. package/src/providers/key-failover.ts +11 -11
  145. package/src/providers/model-rename-fields.ts +1 -0
  146. package/src/providers/opencode-zen-rate-limit.ts +12 -73
  147. package/src/providers/quota/account-cache.ts +15 -1
  148. package/src/providers/quota/anthropic-account-quota.ts +2 -1
  149. package/src/providers/quota/anthropic-cooldown-recovery.ts +22 -4
  150. package/src/providers/quota/anthropic-decision-windows.ts +47 -0
  151. package/src/providers/quota/antigravity.ts +39 -6
  152. package/src/providers/quota/report-cache.ts +14 -0
  153. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  154. package/src/providers/quota-decision-publication.ts +77 -0
  155. package/src/providers/quota-decision-snapshot.ts +145 -0
  156. package/src/providers/quota-routing-cache.ts +3 -0
  157. package/src/providers/quota-types.ts +6 -1
  158. package/src/providers/quota.ts +3 -2
  159. package/src/providers/registry/entries-extended.ts +8 -52
  160. package/src/providers/registry/model-seeds.ts +1 -1
  161. package/src/providers/registry.ts +1 -2
  162. package/src/providers/stale-vision-classification-migration.ts +1 -1
  163. package/src/responses/parser.ts +18 -10
  164. package/src/responses/reasoning-replay-cache.ts +50 -4
  165. package/src/responses/web-search-replay.ts +138 -0
  166. package/src/routing/analytics.ts +7 -15
  167. package/src/routing/compatibility/behavior.ts +4 -1
  168. package/src/server/auth-cors.ts +7 -0
  169. package/src/server/background-lifecycle.ts +2 -1
  170. package/src/server/chat-native.ts +28 -13
  171. package/src/server/claude-messages.ts +4 -2
  172. package/src/server/index/live-sideband.ts +3 -2
  173. package/src/server/index/optional-listeners.ts +6 -0
  174. package/src/server/index/serve-options.ts +11 -5
  175. package/src/server/index/websocket-handler.ts +18 -4
  176. package/src/server/index.ts +7 -7
  177. package/src/server/lifecycle.ts +13 -1
  178. package/src/server/management/combo-routes.ts +17 -2
  179. package/src/server/management/oauth-account-routes.ts +10 -1
  180. package/src/server/management/provider-completion-test.ts +289 -0
  181. package/src/server/management/provider-routes.ts +9 -1
  182. package/src/server/management/route-registry.ts +2 -0
  183. package/src/server/messages-native.ts +34 -7
  184. package/src/server/messages-shared.ts +4 -1
  185. package/src/server/request-log.ts +7 -2
  186. package/src/server/responses/account-auto-pause-recovery.ts +26 -0
  187. package/src/server/responses/adapter-continuation.ts +34 -18
  188. package/src/server/responses/adapter-delivery.ts +53 -6
  189. package/src/server/responses/adapter-dispatch.ts +104 -29
  190. package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
  191. package/src/server/responses/combo-session-recall.ts +25 -0
  192. package/src/server/responses/combo-stream-preflight.ts +6 -3
  193. package/src/server/responses/combo-tool-loop-pin.ts +70 -0
  194. package/src/server/responses/compact.ts +8 -5
  195. package/src/server/responses/compaction-recovery.ts +2 -0
  196. package/src/server/responses/context-overflow.ts +141 -2
  197. package/src/server/responses/core-codex-account.ts +9 -3
  198. package/src/server/responses/core-combo.ts +34 -7
  199. package/src/server/responses/core-lifetime.ts +3 -2
  200. package/src/server/responses/core-options.ts +2 -0
  201. package/src/server/responses/core-replay.ts +5 -1
  202. package/src/server/responses/core.ts +12 -9
  203. package/src/server/responses/empty-completion-guard.ts +2 -0
  204. package/src/server/responses/fetch-helpers.ts +35 -4
  205. package/src/server/responses/jev-model-invoke.ts +13 -14
  206. package/src/server/responses/native-steering-log.ts +2 -2
  207. package/src/server/responses/passthrough-delivery.ts +3 -2
  208. package/src/server/responses/passthrough-dispatch.ts +30 -33
  209. package/src/server/responses/passthrough-execution.ts +3 -2
  210. package/src/server/responses/passthrough-observations.ts +38 -0
  211. package/src/server/responses/request-compression.ts +98 -0
  212. package/src/server/responses/request-prepare.ts +1 -0
  213. package/src/server/responses/request-transport.ts +32 -3
  214. package/src/server/responses/response-effects.ts +1 -0
  215. package/src/server/responses/response-tool-ownership.ts +21 -0
  216. package/src/server/responses/run-turn-execution.ts +2 -0
  217. package/src/server/responses/sidecar-execution.ts +59 -10
  218. package/src/server/responses/sidecar-send-budget.ts +2 -2
  219. package/src/server/responses/terminal-guard.ts +2 -0
  220. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  221. package/src/server/startup-action-control.ts +2 -2
  222. package/src/server/startup-health-cache.ts +2 -2
  223. package/src/server/windows-tray-control.ts +2 -2
  224. package/src/server/ws-bridge.ts +2 -0
  225. package/src/service/definition.ts +9 -1
  226. package/src/types/account-auto-pause.ts +15 -0
  227. package/src/types/config.ts +23 -1
  228. package/src/types/opencode-zen.ts +9 -3
  229. package/src/types/provider.ts +9 -5
  230. package/src/types/request.ts +30 -4
  231. package/src/types/wire.ts +4 -2
  232. package/src/types.ts +2 -0
  233. package/src/usage/cost.ts +17 -1
  234. package/src/usage/expected-prices.ts +3 -1
  235. package/src/usage/jev-stats.ts +4 -0
  236. package/src/usage/log.ts +2 -0
  237. package/src/usage/summary.ts +16 -3
  238. package/src/usage/telemetry-contract.ts +1 -0
  239. package/src/web-search/anthropic-history.ts +59 -0
  240. package/src/web-search/loop.ts +212 -73
  241. package/src/web-search/progress-stream.ts +14 -1
  242. package/src/web-search/run-turn-loop.ts +9 -3
  243. package/src/web-search/sidecar-search-replay.ts +96 -0
  244. package/gui/dist/assets/App-ZfYbLHkB.js +0 -58
  245. package/gui/dist/assets/App-wT3mXqFz.css +0 -1
  246. package/gui/dist/assets/index-CfYDMj34.js +0 -94
@@ -1,3 +1,6 @@
1
+ import { captureAnthropicRejectionPayloads, keepAnthropicReplayBlock } from "./anthropic-rejection-memo";
2
+ import { anthropicDeclaredTools } from "./anthropic-tool-catalog";
3
+ import { anthropicReplayContent, createAnthropicReplayCapture } from "./anthropic-replay";
1
4
  import { withAnthropicProgressUpdates } from "./anthropic-progress";
2
5
  import type { IncomingMeta, ProviderAdapter } from "./base";
3
6
  import { createAdapterTierMetadata, type AdapterTierMetadata } from "../providers/fastwire";
@@ -17,7 +20,7 @@ import type {
17
20
  OcxToolResultMessage,
18
21
  OcxUsage,
19
22
  } from "../types";
20
- import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolChoiceToolPredicate } from "../types";
23
+ import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName } from "../types";
21
24
  import { ANTHROPIC_OAUTH_BETA, CLAUDE_CODE_SYSTEM_INSTRUCTION, applyClaudeToolPrefix, stripClaudeToolPrefix } from "../oauth/anthropic";
22
25
  import { parseDataUrl } from "./image";
23
26
  import { enforceAnthropicImageLimits } from "./anthropic-image-guard";
@@ -32,6 +35,7 @@ import { decodeServerSentEvents } from "../lib/sse-decoder";
32
35
  import { isTranslatorBudgetExceededError, retainTranslatedEventBatch, type TranslatorBudget } from "../lib/translator-budget";
33
36
  import { isReasoningEffortOmitted, modelRecordValue } from "../reasoning-effort";
34
37
  import { applyAgentRouterLanguageFraming, isAgentRouterEndpoint } from "./agentrouter";
38
+ import { anthropicCacheWrite1hTokens, resolveAnthropicCacheRetention, type AnthropicCacheRetention } from "./anthropic/cache-retention";
35
39
  import { claudeFamilyVersion, rejectsCombinedSampling, rejectsForcedToolChoice, rejectsSamplingParameters, supportsExplicitThinkingDisable, usesAdaptiveThinking, usesBetweenToolsFloor } from "./anthropic-model-contract";
36
40
 
37
41
  /** Map a user content part to an Anthropic content block (text or image source). */
@@ -73,8 +77,12 @@ const COMPAT_TOOL_PREFIX = "cx_";
73
77
  type CacheControl = { type: "ephemeral"; ttl?: "1h" | "5m" };
74
78
  const MAX_CACHE_BREAKPOINTS = 4;
75
79
 
76
- function resolveCacheControl(retention: "none" | "short" | "long" | undefined): CacheControl | undefined {
77
- const r = retention ?? "short";
80
+ /** Omitted retention follows the destination default (1h on first-party Anthropic, else 5m). */
81
+ function resolveCacheControl(
82
+ retention: AnthropicCacheRetention | undefined,
83
+ provider: OcxProviderConfig,
84
+ ): CacheControl | undefined {
85
+ const r = resolveAnthropicCacheRetention(retention, provider);
78
86
  if (r === "none") return undefined;
79
87
  return r === "long" ? { type: "ephemeral", ttl: "1h" } : { type: "ephemeral" };
80
88
  }
@@ -283,7 +291,7 @@ function normalizeTtlOrdering(body: Record<string, unknown>): void {
283
291
  }
284
292
  }
285
293
 
286
- function isLikelyRealAnthropicThinkingSignature(signature: string | undefined): signature is string {
294
+ export function isLikelyRealAnthropicThinkingSignature(signature: string | undefined): signature is string {
287
295
  if (typeof signature !== "string" || signature.length < 16) return false;
288
296
  if (/^(fc|call|msg|rs|resp|reasoning|item|ws|tool|func|function)[-_]/i.test(signature)) return false;
289
297
  return /^[A-Za-z0-9+/_=-]+$/.test(signature);
@@ -522,13 +530,13 @@ function streamedToolArgumentsParse(assembled: string): boolean {
522
530
  }
523
531
  }
524
532
 
525
- /** A string field of a server tool's streamed input (web_search `query`, web_fetch `url`); "" when it did not parse. */
526
- function serverToolInputField(json: string, key: "query" | "url"): string {
533
+ /** A server tool's complete streamed input; undefined when it cannot be recovered. */
534
+ function serverToolInput(json: string): Record<string, unknown> | undefined {
527
535
  try {
528
- const value = (JSON.parse(json) as Record<string, unknown> | null)?.[key];
529
- return typeof value === "string" ? value : "";
536
+ const input = JSON.parse(json) as unknown;
537
+ return isAnthropicRecord(input) ? input : undefined;
530
538
  } catch {
531
- return "";
539
+ return undefined;
532
540
  }
533
541
  }
534
542
 
@@ -640,6 +648,7 @@ function usageFromAnthropic(usage: unknown): OcxUsage | undefined {
640
648
  // estimate from visible summaries. Invalid optional detail leaves totals intact.
641
649
  const thinking = isAnthropicRecord(usage.output_tokens_details) ? usage.output_tokens_details.thinking_tokens : undefined;
642
650
  const reasoningOutputTokens = typeof thinking === "number" && Number.isFinite(thinking) && thinking >= 0 && thinking <= output ? thinking : undefined;
651
+ const write1h = anthropicCacheWrite1hTokens(usage, write);
643
652
  return {
644
653
  inputTokens,
645
654
  outputTokens: output,
@@ -648,6 +657,7 @@ function usageFromAnthropic(usage: unknown): OcxUsage | undefined {
648
657
  cachedInputTokens: read,
649
658
  cacheReadInputTokens: read,
650
659
  cacheCreationInputTokens: write,
660
+ ...(write1h !== undefined ? { cacheCreation1hInputTokens: write1h } : {}),
651
661
  } : {}),
652
662
  };
653
663
  }
@@ -735,7 +745,16 @@ function orphanToolResultContent(msg: OcxToolResultMessage): string | unknown[]
735
745
  function messagesToAnthropicFormat(
736
746
  parsed: OcxParsedRequest,
737
747
  toolNames: { toWire: (name: string) => string },
748
+ serverReplay?: ReadonlyMap<string, readonly Record<string, unknown>[]>,
738
749
  ): { system: string | undefined; messages: unknown[] } {
750
+ // The client's server-tool exchanges go back ahead of the block they preceded. Their anchors are
751
+ // unique in the caller's history (replayedAnthropicServerBlocks drops repeated ones); each is used once.
752
+ const pendingReplay = new Map(serverReplay);
753
+ const replayed = (anchor: string) => {
754
+ const blocks = pendingReplay.get(anchor) ?? [];
755
+ pendingReplay.delete(anchor);
756
+ return blocks;
757
+ };
739
758
  // One allocator for the whole request: a tool_result must resolve to the SAME wire id its
740
759
  // call got, and two distinct raw ids must never collapse into one. Conforming ids are claimed
741
760
  // first so a rewritten id can never squat on an id another call legitimately owns.
@@ -750,8 +769,8 @@ function messagesToAnthropicFormat(
750
769
  }
751
770
  }
752
771
  const toolCatalogNudge = buildNonOpenAIToolCatalogNudgeForTools(
753
- parsed.context.tools,
754
- parsed.options.toolChoice,
772
+ anthropicDeclaredTools(parsed),
773
+ undefined,
755
774
  tool => toolNames.toWire(namespacedToolName(tool.namespace, tool.name)),
756
775
  );
757
776
  const systemParts = [...(parsed.context.systemPrompt ?? []), ...(toolCatalogNudge ? [toolCatalogNudge] : [])];
@@ -781,6 +800,7 @@ function messagesToAnthropicFormat(
781
800
  case "assistant": {
782
801
  const aMsg = msg as OcxAssistantMessage;
783
802
  const preface: unknown[] = [];
803
+
784
804
  const toolUses: unknown[] = [];
785
805
  const originalOrder: unknown[] = [];
786
806
  const appendPreface = (block: unknown) => { preface.push(block); originalOrder.push(block); };
@@ -789,9 +809,13 @@ function messagesToAnthropicFormat(
789
809
  for (const part of aMsg.content) {
790
810
  if (part.type === "text") {
791
811
  const text = (part as OcxTextContent).text;
812
+ for (const b of replayed(`text:${text}`)) appendPreface(b);
792
813
  if (text) appendPreface({ type: "text", text });
793
814
  } else if (part.type === "thinking") {
794
815
  const t = part as OcxThinkingContent;
816
+ // Server blocks that preceded this reasoning keep their place, even when it is dropped below.
817
+ for (const data of t.redacted ?? []) for (const b of replayed(`redacted:${data}`)) appendPreface(b);
818
+ for (const b of replayed(`thinking:${t.signature}`)) appendPreface(b);
795
819
  // History minted under another serving identity (or already rejected as opaque) is not
796
820
  // this destination's to verify: drop its opaque blocks, as the Responses passthrough does.
797
821
  if (parsed._stripReasoningEncryptedContent === true) continue;
@@ -806,6 +830,7 @@ function messagesToAnthropicFormat(
806
830
  }
807
831
  } else if (part.type === "toolCall") {
808
832
  const tc = part as OcxToolCall;
833
+ for (const b of replayed(`call:${tc.id}`)) appendPreface(b);
809
834
  const flatName = namespacedToolName(tc.namespace, tc.name);
810
835
  // Normalized here, and identically for the matching tool_result above, so a history
811
836
  // replayed from another provider path keeps its call/result pairing (#1767).
@@ -824,9 +849,15 @@ function messagesToAnthropicFormat(
824
849
  }
825
850
  // Anthropic treats text/thinking after tool_use as ending the tool turn, which makes
826
851
  // earlier tool_use ids look unpaired (#620 / common multi-step history shape).
827
- // Signed native blocks bind the source ordering, including updates between tool calls.
828
- // Keep the legacy repair only for unsigned/cross-provider history.
829
- const content = signed ? originalOrder : [...preface, ...toolUses];
852
+ const captured = anthropicReplayContent(aMsg);
853
+ if (captured) {
854
+ const expected = toolUses as Array<{ id: string; name: string }>;
855
+ const actual = captured.filter(block => block.type === "tool_use");
856
+ if (actual.length !== expected.length || actual.some((block, index) => block.id !== expected[index]?.id || block.name !== expected[index]?.name)) {
857
+ throw new Error("Anthropic search replay tool identity changed");
858
+ }
859
+ }
860
+ const content = (captured ?? (signed ? originalOrder : [...preface, ...toolUses])).filter(block => keepAnthropicReplayBlock(parsed, block as Record<string, unknown>));
830
861
  if (content.length === 0) break;
831
862
  messages.push({ role: "assistant", content });
832
863
  if (toolUseIds.length > 0) {
@@ -889,9 +920,7 @@ function messagesToAnthropicFormat(
889
920
 
890
921
  function toolsToAnthropicFormat(parsed: OcxParsedRequest, toolNames: { toWire: (name: string) => string }): unknown[] | undefined {
891
922
  if (!parsed.context.tools || parsed.context.tools.length === 0) return undefined;
892
- const tools = isAllowedToolChoice(parsed.options.toolChoice)
893
- ? parsed.context.tools.filter(toolChoiceToolPredicate(parsed.options.toolChoice, parsed.context.tools))
894
- : parsed.context.tools;
923
+ const tools = anthropicDeclaredTools(parsed);
895
924
  if (tools.length === 0) return undefined;
896
925
  const converted = tools.map(t => ({
897
926
  name: toolNames.toWire(namespacedToolName(t.namespace, t.name)),
@@ -998,7 +1027,9 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
998
1027
  throw new Error("anthropic provider requires a non-empty apiKey (authMode: key)");
999
1028
  }
1000
1029
 
1001
- const { system, messages } = messagesToAnthropicFormat(parsed, toolNames);
1030
+ // Only api.anthropic.com runs Anthropic's server tools, so only it gets their history back.
1031
+ const { system, messages } = messagesToAnthropicFormat(parsed, toolNames,
1032
+ usesNativeAnthropicEndpoint(provider) ? parsed._anthropicServerReplay : undefined);
1002
1033
  // Before image normalization, so the framing block is present for every downstream pass.
1003
1034
  if (isAgentRouterEndpoint(provider.baseUrl)) applyAgentRouterLanguageFraming(messages);
1004
1035
  // Primary image layer: resize/re-encode to fit Anthropic limits without dropping
@@ -1199,7 +1230,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
1199
1230
 
1200
1231
  // Prompt caching: native Anthropic supports top-level automatic caching, which
1201
1232
  // follows the moving final block across turns. Keep one breakpoint slot free for it.
1202
- const cc = resolveCacheControl(cacheRetention);
1233
+ const cc = resolveCacheControl(cacheRetention, provider);
1203
1234
  const automaticPromptCaching = cc && usesNativeAnthropicEndpoint(provider);
1204
1235
  if (automaticPromptCaching) body.cache_control = cc;
1205
1236
  const explicitLimit = automaticPromptCaching ? MAX_CACHE_BREAKPOINTS - 1 : MAX_CACHE_BREAKPOINTS;
@@ -1210,7 +1241,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
1210
1241
  enforceCacheControlLimit(body, explicitLimit);
1211
1242
  normalizeTtlOrdering(body);
1212
1243
 
1213
- return {
1244
+ const request = {
1214
1245
  url,
1215
1246
  method: "POST",
1216
1247
  headers,
@@ -1224,6 +1255,8 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
1224
1255
  fastSpeed?.value ?? null,
1225
1256
  ),
1226
1257
  };
1258
+ captureAnthropicRejectionPayloads(request, body, incoming?.translatorBudget);
1259
+ return request;
1227
1260
  },
1228
1261
 
1229
1262
  async *parseStream(
@@ -1236,6 +1269,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
1236
1269
  return;
1237
1270
  }
1238
1271
 
1272
+ const replayCapture = createAnthropicReplayCapture(response, budget);
1239
1273
  const budgetEncoder = new TextEncoder();
1240
1274
  let currentBlockType = "";
1241
1275
  let currentToolCallId = "";
@@ -1270,11 +1304,13 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
1270
1304
  yield anthropicContentFilterIncomplete(pendingStopReason, usageFromAnthropic(pendingUsage));
1271
1305
  return;
1272
1306
  }
1273
- yield {
1307
+ const done: AdapterEvent = {
1274
1308
  type: "done",
1275
1309
  usage: usageFromAnthropic(pendingUsage),
1276
1310
  ...(pendingStopReason ? { stopReason: pendingStopReason } : {}),
1277
1311
  };
1312
+ replayCapture?.finish(done);
1313
+ yield done;
1278
1314
  };
1279
1315
 
1280
1316
  try {
@@ -1294,6 +1330,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
1294
1330
  try {
1295
1331
  parsed = JSON.parse(payload);
1296
1332
  } catch {
1333
+ replayCapture?.invalidate();
1297
1334
  debugDroppedFrame("anthropic", payload);
1298
1335
  continue;
1299
1336
  }
@@ -1301,10 +1338,12 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
1301
1338
  // it and the `data.type` read below crashed the stream. Drop a non-record frame the same
1302
1339
  // way an unparseable one is dropped, so the message_stop check still governs the outcome.
1303
1340
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
1341
+ replayCapture?.invalidate();
1304
1342
  debugDroppedFrame("anthropic", payload);
1305
1343
  continue;
1306
1344
  }
1307
1345
  const data = parsed as Record<string, unknown>;
1346
+ replayCapture?.accept({ ...data, type: record.event || data.type });
1308
1347
 
1309
1348
  switch (record.event || data.type) {
1310
1349
  case "message_start": {
@@ -1317,6 +1356,9 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
1317
1356
  case "content_block_start": {
1318
1357
  const block = data.content_block as { type: string; id?: string; name?: unknown; data?: string; thinking?: string; tool_use_id?: unknown; content?: unknown } | undefined;
1319
1358
  if (!block) break;
1359
+ // Even an unmodeled server tool or empty block is semantic progress.
1360
+ // Fence replays before a later refusal; unknown blocks must not look like pings.
1361
+ yield { type: "heartbeat", replayUnsafe: true };
1320
1362
  currentBlockType = block.type;
1321
1363
  if (block.type === "thinking") {
1322
1364
  // Preserve even a display:omitted block boundary. The bridge can then
@@ -1336,36 +1378,26 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
1336
1378
  // no result in this response and must not surface as a completed search.
1337
1379
  serverSearch = { id: block.id, json: "", streaming: true };
1338
1380
  }
1339
- if (block.type === "web_search_tool_result" && typeof block.tool_use_id === "string") {
1381
+ if ((block.type === "web_search_tool_result" || block.type === "web_fetch_tool_result") && typeof block.tool_use_id === "string") {
1382
+ // The begin/end pair the sidecar emits, plus both blocks verbatim so the Claude client holds a
1383
+ // replayable copy. Without this response's matching call and complete input, drop the
1384
+ // exchange instead of inventing a call that could poison the client's next request.
1385
+ const fetch = block.type === "web_fetch_tool_result";
1386
+ const input = serverSearch?.id === block.tool_use_id ? serverToolInput(serverSearch.json) : undefined;
1387
+ if (!input) break;
1340
1388
  const hits = Array.isArray(block.content) ? block.content as Array<{ url?: unknown; title?: unknown }> : undefined;
1341
- // The begin/end pair the sidecar emits. A result whose call came in an earlier response has no query here.
1342
- const query = serverSearch?.id === block.tool_use_id ? serverToolInputField(serverSearch.json, "query") : "";
1343
1389
  yield { type: "web_search_call_begin", id: block.tool_use_id };
1344
1390
  yield {
1345
1391
  type: "web_search_call_end",
1346
1392
  id: block.tool_use_id,
1347
- queries: query ? [query] : [],
1348
- // A failed search carries one error object instead of the hit list.
1349
- status: hits ? "completed" : "failed",
1393
+ queries: typeof input.query === "string" && input.query.length > 0 ? [input.query] : [],
1394
+ // A failed call carries one error object instead of the hit list / fetched page.
1395
+ status: (fetch ? (block.content as { type?: unknown } | undefined)?.type === "web_fetch_result" : hits) ? "completed" : "failed",
1350
1396
  sources: (hits ?? []).flatMap(h => typeof h.url === "string"
1351
1397
  ? [{ url: h.url, ...(typeof h.title === "string" ? { title: h.title } : {}) }] : []),
1352
- };
1353
- serverSearch = undefined;
1354
- }
1355
- if (block.type === "web_fetch_tool_result" && typeof block.tool_use_id === "string") {
1356
- // Relayed as the same cell, opened at the result for the same reason; the result is
1357
- // carried verbatim (a web_fetch_result document or a web_fetch_tool_error).
1358
- const result = block.content ?? { type: "web_fetch_tool_error", error_code: "unavailable" };
1359
- const url = (result as { url?: unknown }).url;
1360
- yield { type: "web_search_call_begin", id: block.tool_use_id };
1361
- yield {
1362
- type: "web_search_call_end",
1363
- id: block.tool_use_id,
1364
- queries: [],
1365
- status: (result as { type?: unknown }).type === "web_fetch_result" ? "completed" : "failed",
1366
- fetch: {
1367
- url: typeof url === "string" ? url : serverSearch?.id === block.tool_use_id ? serverToolInputField(serverSearch.json, "url") : "",
1368
- result,
1398
+ anthropic: {
1399
+ use: { type: "server_tool_use", id: block.tool_use_id, name: fetch ? "web_fetch" : "web_search", input },
1400
+ result: block as Record<string, unknown>,
1369
1401
  },
1370
1402
  };
1371
1403
  serverSearch = undefined;
@@ -1415,8 +1447,8 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
1415
1447
  yield { type: "tool_call_delta", arguments: delta.partial_json };
1416
1448
  } else if (delta.type === "input_json_delta" && typeof delta.partial_json === "string" && serverSearch?.streaming
1417
1449
  && serverSearch.json.length < 4096) {
1418
- // Capped: an input past 4 KiB loses its label (query / url), never the call.
1419
- serverSearch.json += delta.partial_json;
1450
+ // Capped: input that cannot be recovered drops its exchange instead of inventing a call.
1451
+ serverSearch.json += delta.partial_json.slice(0, 4096 - serverSearch.json.length);
1420
1452
  }
1421
1453
  break;
1422
1454
  }
@@ -1463,8 +1495,13 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
1463
1495
  break;
1464
1496
  }
1465
1497
  case "error": {
1466
- const err = data.error as { message?: string } | undefined;
1467
- yield { type: "error", message: err?.message ?? "Anthropic error" };
1498
+ const err = data.error as { message?: unknown; type?: unknown; code?: unknown; status?: unknown } | undefined;
1499
+ yield { type: "error", message: typeof err?.message === "string" ? err.message : "Anthropic error",
1500
+ ...(typeof err?.type === "string" ? { errorType: err.type } : {}),
1501
+ ...(typeof err?.code === "string" ? { code: err.code } : {}),
1502
+ ...(typeof err?.status === "number" && Number.isInteger(err.status) && err.status >= 400 && err.status <= 599
1503
+ ? { status: err.status } : {}),
1504
+ };
1468
1505
  return;
1469
1506
  }
1470
1507
  }
@@ -1483,6 +1520,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
1483
1520
  // one-terminal-event contract for consumers that keep draining the generator.
1484
1521
  return;
1485
1522
  } finally {
1523
+ replayCapture?.discard();
1486
1524
  if (currentToolCallId) budget.closeCall(currentToolCallId);
1487
1525
  }
1488
1526
  if (!emittedDone) {
@@ -1,6 +1,7 @@
1
1
  import { namespacedToolName, type AdapterEvent, type OcxMessage, type OcxParsedRequest, type OcxUsage } from "../../types";
2
2
  import type { TranslatorBudget } from "../../lib/translator-budget";
3
3
  import { isValidCodingAgentToolNamePart } from "./tool-catalog";
4
+ import { anthropicCacheWrite1hTokens } from "../anthropic/cache-retention";
4
5
 
5
6
  /**
6
7
  * Shared stream-json protocol for official coding-agent CLIs (CodeBuddy Code and Qoder CLI).
@@ -165,6 +166,7 @@ function usageFromAnthropicShape(usage: Record<string, unknown>): OcxUsage | und
165
166
  // that could carry those tokens instead, so the turn under-reported usage and cost.
166
167
  const cacheReadTotal = count(usage.cache_read_input_tokens);
167
168
  const cacheCreationTotal = count(usage.cache_creation_input_tokens);
169
+ const cacheCreation1h = anthropicCacheWrite1hTokens(usage, cacheCreationTotal);
168
170
  const inputTokens = freshInputTokens + cacheReadTotal + cacheCreationTotal;
169
171
  if (inputTokens === 0 && outputTokens === 0 && cacheReadTotal === 0 && cacheCreationTotal === 0) {
170
172
  return undefined;
@@ -175,6 +177,7 @@ function usageFromAnthropicShape(usage: Record<string, unknown>): OcxUsage | und
175
177
  totalTokens: inputTokens + outputTokens,
176
178
  ...(cacheReadTotal > 0 ? { cachedInputTokens: cacheReadTotal, cacheReadInputTokens: cacheReadTotal } : {}),
177
179
  ...(cacheCreationTotal > 0 ? { cacheCreationInputTokens: cacheCreationTotal } : {}),
180
+ ...(cacheCreationTotal > 0 && cacheCreation1h !== undefined ? { cacheCreation1hInputTokens: cacheCreation1h } : {}),
178
181
  };
179
182
  }
180
183
 
@@ -204,6 +207,8 @@ function mergePartialUsage(previous: OcxUsage | undefined, next: OcxUsage): OcxU
204
207
  next.cacheReadInputTokens ?? next.cachedInputTokens ?? 0,
205
208
  );
206
209
  const cacheCreation = Math.max(previous.cacheCreationInputTokens ?? 0, next.cacheCreationInputTokens ?? 0);
210
+ const cacheCreation1h = Math.min(cacheCreation,
211
+ Math.max(previous.cacheCreation1hInputTokens ?? 0, next.cacheCreation1hInputTokens ?? 0));
207
212
  // Inclusive inputs: fold each snapshot's fresh part, then add the folded cache counters once.
208
213
  const freshInput = (usage: OcxUsage) => Math.max(0, usage.inputTokens
209
214
  - (usage.cacheReadInputTokens ?? usage.cachedInputTokens ?? 0) - (usage.cacheCreationInputTokens ?? 0));
@@ -214,6 +219,7 @@ function mergePartialUsage(previous: OcxUsage | undefined, next: OcxUsage): OcxU
214
219
  totalTokens: inputTokens + outputTokens,
215
220
  ...(cacheRead > 0 ? { cachedInputTokens: cacheRead, cacheReadInputTokens: cacheRead } : {}),
216
221
  ...(cacheCreation > 0 ? { cacheCreationInputTokens: cacheCreation } : {}),
222
+ ...(cacheCreation1h > 0 ? { cacheCreation1hInputTokens: cacheCreation1h } : {}),
217
223
  };
218
224
  }
219
225
 
@@ -1,3 +1,4 @@
1
+ import { inheritAccountAutoPauseResponse } from "../lib/account-auto-pause-response";
1
2
  import type { AdapterFetchContext, AdapterRequest } from "./base";
2
3
  import { createAdapterPhysicalSend } from "./physical-send";
3
4
  import type { SendClass } from "../lib/request-execution-budget";
@@ -38,10 +39,10 @@ async function normalizeFinalGoogleError(label: string, res: Response, signal?:
38
39
  const headers = new Headers(res.headers);
39
40
  headers.delete("content-encoding");
40
41
  headers.delete("content-length");
41
- return new Response(safeGoogleHttpErrorMessage(label, res.status,
42
+ return inheritAccountAutoPauseResponse(res, new Response(safeGoogleHttpErrorMessage(label, res.status,
42
43
  body.displaySafe && !body.truncated ? body.text : ""), {
43
44
  status: res.status, statusText: res.statusText, headers,
44
- });
45
+ }));
45
46
  }
46
47
  return normalizeUpstreamHttpErrorResponse(res, {
47
48
  signal,
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Gemini progress-text history trimming.
3
+ *
4
+ * Gemini models, observed on Gemini 4 Argon over Cloud Code Assist, sometimes skip their thinking
5
+ * channel on a tool-calling turn and plan in the visible text instead. The provider sends that
6
+ * planning as an ordinary text part without a thought marker, for example a progress line in the
7
+ * user's language followed by "Let's read google.ts 580-1700 first." Once one turn like that is
8
+ * replayed as history, the model copies the pattern on the turns that follow, so the leak grows
9
+ * through the rest of the session. Replaying the same 300K-token request with the leaked tails
10
+ * removed from the history stopped the leak, while stronger system-prompt wording did not.
11
+ *
12
+ * The fix is on the history side only. Text that precedes the tool call of a replayed model turn is
13
+ * intermediate progress, and the Google brevity instruction already asks for a single short line
14
+ * there, so it is replayed as its lead sentence. A turn Codex marks `final_answer` and text after a
15
+ * tool call are replayed whole. Visible output is never rewritten, final
16
+ * answers (turns without tool calls) are replayed in full, and the split is punctuation-based so
17
+ * it works the same in every language without matching any wording.
18
+ */
19
+
20
+ /**
21
+ * Sentence end: ASCII terminal punctuation followed by whitespace or the end (so "google.ts" and
22
+ * "3.5" are not boundaries), a script's own sentence terminator (CJK 。!?, Devanagari
23
+ * ।॥, Arabic ۔؟, Armenian ։, Ethiopic ።, Myanmar ။), or a line break. Closing quotes, brackets and
24
+ * Markdown emphasis right after the punctuation belong to the sentence.
25
+ */
26
+ const SENTENCE_END = /[.!?]["'\u201D\u2019)\]*_`]*(?=\s|$)|[\u3002\uFF01\uFF1F\u0964\u0965\u06D4\u061F\u0589\u1362\u104B]["'\u201D\u2019\u300D\u300F)\]*_`]*|\n/g;
27
+
28
+ /** A boundary that only closes a leading list marker ("1.", "a.") or a common abbreviation ("e.g."). */
29
+ const NOT_A_SENTENCE = /^(?:\d{1,3}|[a-z])\.$|(?:^|\s)(?:e\.g|i\.e|etc|vs)\.$/i;
30
+
31
+ /** Whether replayed progress text for this model is trimmed to its lead sentence. */
32
+ export function trimsGeminiProgressHistory(modelId: string): boolean {
33
+ return /gemini/i.test(modelId);
34
+ }
35
+
36
+ /**
37
+ * The lead sentence of a replayed progress text, or undefined when nothing usable remains.
38
+ * Text without a sentence boundary is already a single sentence and is returned whole.
39
+ */
40
+ export function geminiProgressLead(text: string): string | undefined {
41
+ const trimmed = text.trim();
42
+ if (trimmed.length === 0) return undefined;
43
+ SENTENCE_END.lastIndex = 0;
44
+ let end = trimmed.length;
45
+ for (let match = SENTENCE_END.exec(trimmed); match; match = SENTENCE_END.exec(trimmed)) {
46
+ const boundary = match.index + match[0].length;
47
+ if (!NOT_A_SENTENCE.test(trimmed.slice(0, boundary).trim())) { end = boundary; break; }
48
+ }
49
+ const lead = trimmed.slice(0, end).trim();
50
+ // A code-fence opener is not a sentence; replaying it alone would leave an unclosed fence.
51
+ if (lead.length === 0 || lead.startsWith("```")) return undefined;
52
+ return lead;
53
+ }
@@ -0,0 +1,67 @@
1
+ import { googleVertexLocationConfigError } from "../providers/google-vertex-location";
2
+
3
+ /**
4
+ * Vertex endpoint selection, shared by inference and replay identity.
5
+ *
6
+ * API keys follow explicit provider configuration only: `provider.project` selects the project
7
+ * endpoint (location defaults to `global`), otherwise the projectless express endpoint is used.
8
+ * Ambient GOOGLE_CLOUD_PROJECT / GCLOUD_PROJECT / GOOGLE_CLOUD_LOCATION belong to ADC, so an
9
+ * unrelated gcloud default in the shell cannot move an express-mode key onto another project.
10
+ */
11
+ export type VertexEndpointSelection =
12
+ | { kind: "express" }
13
+ | { kind: "project"; project: string; location: string };
14
+
15
+ type VertexEndpointConfig = { project?: string; location?: string };
16
+
17
+ export function selectVertexEndpoint(
18
+ provider: VertexEndpointConfig,
19
+ hasApiKey: boolean,
20
+ env: NodeJS.ProcessEnv = process.env,
21
+ ): VertexEndpointSelection {
22
+ // A blank or whitespace-only project is unset: it must not select a /projects/%20/ endpoint.
23
+ const configuredProject = provider.project?.trim() || undefined;
24
+ if (hasApiKey) {
25
+ if (!configuredProject) return { kind: "express" };
26
+ return { kind: "project", project: configuredProject, location: provider.location || "global" };
27
+ }
28
+ const project = configuredProject || env.GOOGLE_CLOUD_PROJECT || env.GCLOUD_PROJECT;
29
+ if (!project) throw new Error("Vertex AI requires a project id (provider.project or GOOGLE_CLOUD_PROJECT/GCLOUD_PROJECT).");
30
+ const location = provider.location || env.GOOGLE_CLOUD_LOCATION;
31
+ if (!location) throw new Error("Vertex AI requires a location (provider.location or GOOGLE_CLOUD_LOCATION).");
32
+ return { kind: "project", project, location };
33
+ }
34
+
35
+ /**
36
+ * Replay namespace for thought-signature continuation; mirrors the endpoint actually used.
37
+ * The endpoint kind is part of the key and the project is encoded, so no configured project
38
+ * (for example one literally named "api-key" or containing ":") can share express mode's space.
39
+ */
40
+ export function vertexReplayModelKey(selection: VertexEndpointSelection, modelId: string): string {
41
+ return selection.kind === "express"
42
+ ? `vertex:express:${modelId}`
43
+ : `vertex:project:${encodeURIComponent(selection.project)}:${selection.location}:${modelId}`;
44
+ }
45
+
46
+ const VERTEX_MODEL_PATH_REJECT = /[/?#\\%]/;
47
+
48
+ /** The model id is interpolated as one URL path segment after a fixed Google authority. */
49
+ export function vertexModelPathError(modelId: string): string | undefined {
50
+ if (!modelId || VERTEX_MODEL_PATH_REJECT.test(modelId) || modelId.includes("..")) {
51
+ return "Vertex AI model id must be a single path segment without '/', '?', '#', '\\', '%' or '..'.";
52
+ }
53
+ return undefined;
54
+ }
55
+
56
+ /** Builds the inference URL for a selection; validates location before any authority is formed. */
57
+ export function vertexInferenceUrl(selection: VertexEndpointSelection, modelId: string, method: string, streamParam: string): string {
58
+ const modelError = vertexModelPathError(modelId);
59
+ if (modelError) throw new Error(modelError);
60
+ if (selection.kind === "express") {
61
+ return `https://aiplatform.googleapis.com/v1/publishers/google/models/${modelId}:${method}${streamParam}`;
62
+ }
63
+ const locationError = googleVertexLocationConfigError(selection.location);
64
+ if (locationError) throw new Error(locationError);
65
+ const host = selection.location === "global" ? "aiplatform.googleapis.com" : `${selection.location}-aiplatform.googleapis.com`;
66
+ return `https://${host}/v1/projects/${encodeURIComponent(selection.project)}/locations/${selection.location}/publishers/google/models/${modelId}:${method}${streamParam}`;
67
+ }