@bitkyc08/opencodex 2.87.0 → 2.88.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 (68) hide show
  1. package/gui/dist/assets/{App-ZfYbLHkB.js → App-CHC89EXQ.js} +3 -3
  2. package/gui/dist/assets/{Tray-DCd-A1az.js → Tray-B5bC7AWL.js} +1 -1
  3. package/gui/dist/assets/index-B5bsvxor.js +94 -0
  4. package/gui/dist/assets/{quota-summary-ChvmKGOk.js → quota-summary-D8y2wTsB.js} +1 -1
  5. package/gui/dist/index.html +1 -1
  6. package/package.json +1 -1
  7. package/src/adapters/anthropic-rejection-memo.ts +71 -0
  8. package/src/adapters/anthropic-replay.ts +135 -0
  9. package/src/adapters/anthropic-tool-catalog.ts +11 -0
  10. package/src/adapters/anthropic.ts +76 -45
  11. package/src/adapters/google-progress-history.ts +53 -0
  12. package/src/adapters/google.ts +17 -1
  13. package/src/bridge/internal.ts +6 -2
  14. package/src/bridge/response-json.ts +2 -2
  15. package/src/bridge/sse.ts +7 -6
  16. package/src/claude/inbound-content-options.ts +66 -0
  17. package/src/claude/outbound.ts +13 -5
  18. package/src/cli/cross-home-owner.ts +16 -2
  19. package/src/cli/loopback-family.ts +31 -0
  20. package/src/codex/catalog/routed-gather.ts +3 -1
  21. package/src/codex/websocket-activity.ts +161 -0
  22. package/src/codex/websocket-registry.ts +25 -1
  23. package/src/config/opencode-zen-upgrade.ts +9 -8
  24. package/src/generated/compatibility-version.json +88 -52
  25. package/src/images/loop.ts +72 -21
  26. package/src/lib/request-failure-model.ts +2 -0
  27. package/src/oauth/generic-account-failover.ts +7 -4
  28. package/src/protocols/encoders/adapter-events.ts +5 -5
  29. package/src/protocols/encoders/messages.ts +2 -2
  30. package/src/providers/opencode-zen-rate-limit.ts +7 -2
  31. package/src/responses/reasoning-replay-cache.ts +50 -4
  32. package/src/routing/analytics.ts +7 -15
  33. package/src/server/claude-messages.ts +2 -1
  34. package/src/server/index/live-sideband.ts +3 -2
  35. package/src/server/index/serve-options.ts +11 -5
  36. package/src/server/index/websocket-handler.ts +18 -4
  37. package/src/server/index.ts +7 -7
  38. package/src/server/lifecycle.ts +13 -1
  39. package/src/server/messages-native.ts +16 -3
  40. package/src/server/responses/adapter-continuation.ts +34 -18
  41. package/src/server/responses/adapter-delivery.ts +53 -6
  42. package/src/server/responses/adapter-dispatch.ts +27 -1
  43. package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
  44. package/src/server/responses/combo-stream-preflight.ts +3 -2
  45. package/src/server/responses/compaction-recovery.ts +2 -0
  46. package/src/server/responses/core-codex-account.ts +2 -2
  47. package/src/server/responses/core-combo.ts +3 -2
  48. package/src/server/responses/core-lifetime.ts +3 -2
  49. package/src/server/responses/core-options.ts +2 -0
  50. package/src/server/responses/core-replay.ts +5 -1
  51. package/src/server/responses/passthrough-execution.ts +3 -2
  52. package/src/server/responses/request-prepare.ts +1 -0
  53. package/src/server/responses/response-effects.ts +1 -0
  54. package/src/server/responses/response-tool-ownership.ts +21 -0
  55. package/src/server/responses/sidecar-execution.ts +43 -7
  56. package/src/server/responses/sidecar-send-budget.ts +2 -2
  57. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  58. package/src/server/ws-bridge.ts +2 -0
  59. package/src/types/opencode-zen.ts +9 -3
  60. package/src/types/request.ts +10 -2
  61. package/src/types/wire.ts +4 -2
  62. package/src/types.ts +1 -0
  63. package/src/usage/summary.ts +9 -3
  64. package/src/usage/telemetry-contract.ts +1 -0
  65. package/src/web-search/anthropic-history.ts +59 -0
  66. package/src/web-search/loop.ts +181 -68
  67. package/src/web-search/progress-stream.ts +11 -1
  68. package/gui/dist/assets/index-CfYDMj34.js +0 -94
@@ -144,7 +144,8 @@ function cooledPoolAccountLabel(config: OcxConfig, providerName: string, modelId
144
144
  * through `accountLogLabel`, but the per-request account budget can end the pool's own rotation
145
145
  * before the last refused account is recorded. Hand that account to the pool's rotator, which
146
146
  * cools it under the pool's own Retry-After policy, and spare the target only when the pool still
147
- * has an eligible sibling. A lone account or an all-cooled pool returns null and cools the target.
147
+ * has an eligible sibling. The rotator only peeks round-robin here: nothing is sent to that sibling.
148
+ * A lone account or an all-cooled pool returns null and cools the target.
148
149
  */
149
150
  function cooledGenericOAuthAccountLabel(
150
151
  config: OcxConfig, providerName: string, modelId: string, label: string | undefined, retryAfter: string | undefined,
@@ -155,7 +156,7 @@ function cooledGenericOAuthAccountLabel(
155
156
  const matches = getAccountSet(providerName)?.accounts.filter(account =>
156
157
  oauthAccountLogLabel(account.id, base) === label) ?? [];
157
158
  return matches.length === 1
158
- && rotateGenericOAuthAccountOn429(config, providerName, matches[0]!.id, retryAfter, Date.now(), modelId)
159
+ && rotateGenericOAuthAccountOn429(config, providerName, matches[0]!.id, retryAfter, Date.now(), modelId, false)
159
160
  ? label : undefined;
160
161
  }
161
162
 
@@ -1,4 +1,5 @@
1
1
  import type { TranslatorBudget } from "../../lib/translator-budget";
2
+ import { carryResponseToolOwnership } from "./response-tool-ownership";
2
3
  import { finalizeTranslatorBudgetResponse } from "../../lib/translator-budget";
3
4
  import { isNonReplayableResponse, markResponseNonReplayable } from "../../lib/upstream-retry";
4
5
  import {
@@ -50,7 +51,7 @@ export function finalizeOwnedTranslatorBudget(response: Response, budget: Transl
50
51
  if (isPreinspectedJsonResponse(response)) markPreinspectedJsonResponse(finalizedResponse);
51
52
  if (isNonReplayableResponse(response)) markResponseNonReplayable(finalizedResponse);
52
53
  if (runTurnAdapterSseResponses.has(response)) runTurnAdapterSseResponses.add(finalizedResponse);
53
- return finalizedResponse;
54
+ return carryResponseToolOwnership(response, finalizedResponse);
54
55
  }
55
56
 
56
57
  /** Release a serving-account slot when the client body ends, errors or is cancelled. */
@@ -75,7 +76,7 @@ export function finalizeAccountLease(response: Response, release: () => void, on
75
76
  if (isEagerRelaySseResponse(response)) markEagerRelaySseResponse(wrapped);
76
77
  if (isPreinspectedJsonResponse(response)) markPreinspectedJsonResponse(wrapped);
77
78
  if (runTurnAdapterSseResponses.has(response)) runTurnAdapterSseResponses.add(wrapped);
78
- return wrapped;
79
+ return carryResponseToolOwnership(response, wrapped);
79
80
  }
80
81
 
81
82
 
@@ -177,6 +177,8 @@ export interface HandleResponsesOptions {
177
177
  nativeReasoningMint?: { owner?: NativeReasoningOwner };
178
178
  /** The Claude client's typed `web_search_*` / `web_fetch_*` declarations; kept only for `inboundWire: "anthropic"`. */
179
179
  anthropicServerTools?: readonly Record<string, unknown>[];
180
+ /** The Claude client's replayable server-tool exchanges (`replayedAnthropicServerBlocks`); same scope. */
181
+ anthropicServerReplay?: ReadonlyMap<string, readonly Record<string, unknown>[]>;
180
182
  /** Droid's per-request effort default; each concrete combo or policy target applies it only if its ladder allows it. */
181
183
  droidDefaultEffort?: string;
182
184
  /** PF-07: the Chat source a combo child may send natively; set only by the Chat ingress. */
@@ -1,3 +1,5 @@
1
+ import { bindAnthropicRejectionPolicy } from "../../adapters/anthropic-rejection-memo";
2
+ import { getReasoningReplayOpaqueBlobRejection } from "../../responses/reasoning-replay-cache";
1
3
  import type {
2
4
  OcxProviderContinuationOwner,
3
5
  OcxProviderContinuationState,
@@ -177,8 +179,10 @@ export function bindRouteReasoningReplayScope(args: {
177
179
  parsed._dropForeignReasoningItemIds = true;
178
180
  }
179
181
  }
182
+ const rejectionMemo = getReasoningReplayOpaqueBlobRejection(parsed._reasoningReplayScope);
183
+ bindAnthropicRejectionPolicy(parsed, adapterName === "anthropic" ? rejectionMemo : undefined);
180
184
  if (reasoningReplayOpaqueBlobRejectionMemoized(parsed._reasoningReplayScope)) {
181
- parsed._stripReasoningEncryptedContent = true;
185
+ if (adapterName !== "anthropic" || rejectionMemo?.mode !== "selective") parsed._stripReasoningEncryptedContent = true;
182
186
  parsed._stripNativeCompactionEncryptedContent = true;
183
187
  parsed._dropForeignReasoningItemIds = true;
184
188
  }
@@ -14,6 +14,7 @@ import { guardDirectPassthroughBodyInactivity } from "../../lib/response-body-in
14
14
  import { resolveStallTimeoutMs } from "../../stall-timeout";
15
15
  import { releaseUpstreamHostAdmission } from "../../codex/upstream-host-health";
16
16
  import { releaseCodexAuthContextProbeLease } from "../../codex/auth-context";
17
+ import { attachResponseToolOwnership } from "./response-tool-ownership";
17
18
 
18
19
  /** Owns the native host lease across dispatch, recovery, and response construction. */
19
20
  export async function executePassthroughResponse(
@@ -50,11 +51,11 @@ export async function executePassthroughResponse(
50
51
  // Delivery has already classified SSE (including a missing upstream content type)
51
52
  // and consumed bounded JSON/errors. Guard only its remaining direct body, outside
52
53
  // the relay/lifetime wrappers so their prefetch cannot arm our inactivity clock.
53
- return guardDirectPassthroughBodyInactivity(
54
+ return attachResponseToolOwnership(guardDirectPassthroughBodyInactivity(
54
55
  response,
55
56
  nativeExchange.upstream.signal,
56
57
  resolveStallTimeoutMs(requestContext.config.stallTimeoutSec, { localUpstream: nativeExchange.localUpstream }),
57
- );
58
+ ), nativeExchange.providerExecutedCallTypes);
58
59
  } finally {
59
60
  if (nativeHostState.lease) {
60
61
  releaseUpstreamHostAdmission(nativeHostState.lease);
@@ -476,6 +476,7 @@ export async function prepareResponsesRequest(
476
476
  parsed._nativeReasoningReplay = options.nativeReasoningReplay;
477
477
  parsed._nativeReasoningMint = options.nativeReasoningMint;
478
478
  parsed._anthropicServerTools = options.anthropicServerTools;
479
+ parsed._anthropicServerReplay = options.anthropicServerReplay;
479
480
  }
480
481
  parsed._promptCacheKeyIsSharedCohort = options.promptCacheKeyIsSharedCohort;
481
482
  // The body may have been rebuilt since the inbound observation (previous-response
@@ -90,6 +90,7 @@ export function createResponsesEffects(
90
90
  delete parsed.context.tools;
91
91
  delete parsed._webSearch;
92
92
  delete parsed._anthropicServerTools;
93
+ delete parsed._anthropicServerReplay;
93
94
  delete parsed.options.toolChoice;
94
95
  delete parsed.options.parallelToolCalls;
95
96
  // The compaction turn is a plain prose summary; a surviving structured-output format
@@ -0,0 +1,21 @@
1
+ import type { ProviderExecutedCallType } from "../responses-undeclared-tool-guard";
2
+
3
+ // Route-authorized execution ownership follows only the response selected for delivery.
4
+ // It never becomes a wire header or mutable state shared by competing route attempts.
5
+ const providerCalls = new WeakMap<Response, ReadonlySet<ProviderExecutedCallType>>();
6
+
7
+ export function attachResponseToolOwnership(response: Response, types: ReadonlySet<ProviderExecutedCallType>): Response {
8
+ if (types.size > 0) providerCalls.set(response, new Set(types));
9
+ return response;
10
+ }
11
+
12
+ export function responseToolOwnership(response: Response): ReadonlySet<ProviderExecutedCallType> | undefined {
13
+ return providerCalls.get(response);
14
+ }
15
+
16
+ /** Carry ownership only when rebuilding the same selected response body. */
17
+ export function carryResponseToolOwnership(source: Response, target: Response): Response {
18
+ const types = providerCalls.get(source);
19
+ if (types) providerCalls.set(target, types);
20
+ return target;
21
+ }
@@ -1,3 +1,4 @@
1
+ import { createAnthropicThinkingRecovery } from "./anthropic-thinking-recovery";
1
2
  import { rotateAnthropicAccountOnResponseForInstance } from "../../oauth/anthropic-account-refusal";
2
3
  import type { ResponsesRequestContext } from "./core-options";
3
4
  import type { PreparedResponsesRequest } from "./request-prepare";
@@ -20,7 +21,7 @@ import {
20
21
  runWithImageBridge,
21
22
  clampImageMaxRounds,
22
23
  } from "../../images";
23
- import type { ProviderAdapter } from "../../adapters/base";
24
+ import type { AdapterRequest, ProviderAdapter } from "../../adapters/base";
24
25
  import type { OcxParsedRequest, OcxUsage } from "../../types";
25
26
  import { rotateProviderTransportOn429, rateLimitRetryPolicyFor } from "../../providers/key-failover";
26
27
  import {
@@ -32,7 +33,7 @@ import {
32
33
  failoverAccountSnapshot,
33
34
  } from "../../oauth/generic-account-failover";
34
35
  import { classifyKiroRefusal } from "../../adapters/kiro-refusal";
35
- import { isNonReplayableResponse } from "../../lib/upstream-retry";
36
+ import { isNonReplayableResponse, SendBudgetExhaustedError } from "../../lib/upstream-retry";
36
37
  import { unwrapUpstreamRetryEvidenceError } from "../../codex/auth-context";
37
38
  import { AdmissionModelDeniedError, admissionModelDeniedResponse } from "../admission-model-scope";
38
39
  import { noteKiroMonthlyRefusal, noteKiroServedSuccess } from "../../providers/kiro-usage";
@@ -100,7 +101,7 @@ export async function executeResponsesSidecars(
100
101
  | "notifyResponseComplete"
101
102
  | "cancelResponseCompletion"
102
103
  >,
103
- sendBudgetState: Pick<ResponsesSendBudget, "reserveCredentialHop" | "refundableAdapterDispatchBudget" | "adapterDispatchBudget" | "noteInitialDispatch" | "noteAdapterPhysicalSend" | "noteAdapterRecoveryWithheld" | "pendingHopPermit">,
104
+ sendBudgetState: Pick<ResponsesSendBudget, "sendBudgetExhausted" | "reserveCredentialHop" | "refundableAdapterDispatchBudget" | "adapterDispatchBudget" | "noteInitialDispatch" | "noteAdapterPhysicalSend" | "noteAdapterRecoveryWithheld" | "pendingHopPermit">,
104
105
  ) {
105
106
  const { config, options, logCtx } = requestContext;
106
107
  const antigravityPoolActivated = requestState.route.providerName === "google-antigravity"
@@ -379,7 +380,7 @@ export async function executeResponsesSidecars(
379
380
  recoveryKind = "oauth-account-429";
380
381
  if (!options.comboInitialSend && canRunWebSearch && route.providerName === "google-antigravity")
381
382
  sendBudgetState.pendingHopPermit = hop.permit;
382
- else sidecarBudget.ownCredentialHop(hop.permit);
383
+ else sidecarBudget.ownCredentialHop(hop.permit, (canRunWebSearch || !!imgPlan || !!vidPlan) && transportState.adapter.name === "anthropic");
383
384
  } else if (
384
385
  // Anthropic's pool is excluded from generic failover, so without this arm a 429 inside a
385
386
  // web-search or image-bridge turn was terminal even with the pool fully enabled -- while
@@ -421,7 +422,7 @@ export async function executeResponsesSidecars(
421
422
  }
422
423
  recoveryKind = originalResponse?.status === 401 ? "oauth-401"
423
424
  : originalResponse?.status === 403 ? "oauth-account-403" : "anthropic-oauth-429";
424
- sidecarBudget.ownCredentialHop(hop.permit);
425
+ sidecarBudget.ownCredentialHop(hop.permit, (canRunWebSearch || !!imgPlan || !!vidPlan) && transportState.adapter.name === "anthropic");
425
426
  } else {
426
427
  // No key pool, no generic OAuth roster, no Anthropic pool could produce a replacement
427
428
  // credential. The 429 is terminal for this sidecar turn.
@@ -454,6 +455,35 @@ export async function executeResponsesSidecars(
454
455
  });
455
456
  return { adapter: rotatedAdapter, recoveryKind };
456
457
  };
458
+ // One guard follows the active sidecar across all iterations. Enforced spend has no
459
+ // authoritative receipt for earlier legacy sends, so recovery cannot claim headroom there.
460
+ const thinkingRecovery = createAnthropicThinkingRecovery(parsed,
461
+ options.abortSignal ?? new AbortController().signal, { attempted: false },
462
+ () => !sendBudgetState.sendBudgetExhausted()
463
+ && !!sendBudgetState.adapterDispatchBudget
464
+ && sendBudgetState.adapterDispatchBudget.spendEnforced !== true);
465
+ const thinkingReceipt = (request: AdapterRequest, adapter: ProviderAdapter, recovery?: AttemptRecoveryKind) =>
466
+ adapter.name !== "anthropic" ? {} : { onPhysicalDispatch: () => {
467
+ if (recovery === "opaque-blob-rejection") {
468
+ const decision = sendBudgetState.adapterDispatchBudget?.reserveDispatch({
469
+ sendClass: "repair", targetKey: `${route.providerName}|${request.url}|${route.modelId}`,
470
+ });
471
+ if (sendBudgetState.adapterDispatchBudget?.spendEnforced || !decision?.allowed) {
472
+ if (decision?.allowed) decision.permit.release();
473
+ throw new SendBudgetExhaustedError("sidecar thinking recovery");
474
+ }
475
+ try { sendBudgetState.noteInitialDispatch(decision.permit); }
476
+ finally { decision.permit.release(); }
477
+ } else {
478
+ const existing = sidecarBudget.fetchOptions(adapter).onPhysicalDispatch;
479
+ if (existing) existing();
480
+ else {
481
+ const prepaid = sendBudgetState.pendingHopPermit;
482
+ sendBudgetState.pendingHopPermit = undefined;
483
+ sendBudgetState.noteInitialDispatch(prepaid);
484
+ }
485
+ }
486
+ } };
457
487
  // A loop's hidden rounds before a rotation were spent by the account it left. Bill them to that
458
488
  // account's attempt while it is still open; the next send seals it. Declined when that send
459
489
  // keeps the account (no new attempt), so the loop's final total still carries them.
@@ -520,6 +550,7 @@ export async function executeResponsesSidecars(
520
550
  );
521
551
  const imgResponse = await runWithImageBridge({
522
552
  parsed, adapter: transportState.adapter,
553
+ ...(transportState.adapter.name === "anthropic" ? { thinkingRecovery } : {}),
523
554
  clientToolCatalogRestriction: route.provider.authMode === "forward" ? undefined : requestState.clientToolCatalogRestriction,
524
555
  incomingMeta: { headers: requestState.selectedForwardHeaders, providerName: route.providerName, abortSignal: options.abortSignal, translatorBudget, ...(options.comboInitialSend ? { providerFetch: imageProviderFetch } : {}), ...sidecarBudget.incomingMeta },
525
556
  onProducerStart: sidecarBudget.takeProducerOwnership,
@@ -540,10 +571,11 @@ export async function executeResponsesSidecars(
540
571
  stallTimeoutSec: config.stallTimeoutSec,
541
572
  waitForRequestSlot: imageProviderFetch.waitForPacing,
542
573
  fetchImpl: imageProviderFetch.unpacedFetch ?? imageProviderFetch,
543
- fetchForRequest: (request, iterParsed, adapter) => {
574
+ fetchForRequest: (request, iterParsed, adapter, recovery) => {
544
575
  const fetch = providerFetch(route.provider, options.codexWsRuntimeIdentity, {
545
576
  dispatchOverride: oauthDispatch(request, iterParsed),
546
577
  providerName: route.providerName, modelId: route.modelId, ...sidecarBudget.fetchOptions(adapter),
578
+ ...thinkingReceipt(request, adapter, recovery),
547
579
  });
548
580
  return fetch.unpacedFetch ?? fetch;
549
581
  },
@@ -569,6 +601,7 @@ export async function executeResponsesSidecars(
569
601
  ...(options.forceEmptyResponseId ? { forceEmptyResponseId: true } : {}),
570
602
  terminalRefusalAsFailed: inboundWire !== "chat" && inboundWire !== "anthropic",
571
603
  onCompletedResponse: (response, providerState) => {
604
+ if (!options.abortSignal?.aborted) thinkingRecovery.rememberSuccess(response);
572
605
  const served = transportState.replayOAuthCredentialSnapshot;
573
606
  if (route.providerName === "kiro" && response.status === "completed" && served
574
607
  && noteKiroServedSuccess(served.accountId, served.generation)) persistKiroAccountState();
@@ -618,11 +651,13 @@ export async function executeResponsesSidecars(
618
651
  })(input, init)) as typeof globalThis.fetch;
619
652
  const loopResponse = await runWithWebSearch({
620
653
  parsed, adapter: transportState.adapter,
654
+ ...(transportState.adapter.name === "anthropic" ? { thinkingRecovery } : {}),
621
655
  clientToolCatalogRestriction: route.provider.authMode === "forward" ? undefined : requestState.clientToolCatalogRestriction,
622
656
  onIterationEnd: sidecarBudget.releaseUnsentHop,
623
- fetchForRequest: (request, iterParsed, adapter) => providerFetch(route.provider, options.codexWsRuntimeIdentity, {
657
+ fetchForRequest: (request, iterParsed, adapter, recovery) => providerFetch(route.provider, options.codexWsRuntimeIdentity, {
624
658
  dispatchOverride: oauthDispatch(request, iterParsed),
625
659
  providerName: route.providerName, modelId: route.modelId, ...sidecarBudget.fetchOptions(adapter),
660
+ ...thinkingReceipt(request, adapter, recovery),
626
661
  }),
627
662
  incomingMeta: {
628
663
  headers: requestState.selectedForwardHeaders,
@@ -683,6 +718,7 @@ export async function executeResponsesSidecars(
683
718
  if (route.providerName === "kiro" && response.status === "completed" && served
684
719
  && noteKiroServedSuccess(served.accountId, served.generation)) persistKiroAccountState();
685
720
  commitReasoningReplayServingRoute();
721
+ if (!options.abortSignal?.aborted) thinkingRecovery.rememberSuccess(response);
686
722
  notifyResponseComplete(response);
687
723
  },
688
724
  });
@@ -22,8 +22,8 @@ export function createSidecarSendBudget(
22
22
  };
23
23
  return {
24
24
  /** Retain a Combo hop until dispatch; direct requests keep eager settlement. */
25
- ownCredentialHop(permit?: SingleUseDispatchPermit): void {
26
- if (!initial) { permit?.use(); return; } // Preserve direct callers' reporting contract.
25
+ ownCredentialHop(permit?: SingleUseDispatchPermit, retainUntilDispatch = false): void {
26
+ if (!initial && !retainUntilDispatch) { permit?.use(); return; } // Preserve direct callers' reporting contract.
27
27
  releaseHop();
28
28
  hopPermit = permit;
29
29
  budget.pendingHopPermit = permit;
@@ -298,7 +298,7 @@ export function collectProviderExecutedCallTypes(body: unknown): Set<ProviderExe
298
298
  return callTypes;
299
299
  }
300
300
 
301
- function isAuthorizedProviderExecutedCall(
301
+ export function isAuthorizedProviderExecutedCall(
302
302
  item: Record<string, unknown>,
303
303
  callTypes: ProviderExecutedCallTypes,
304
304
  ): boolean {
@@ -11,6 +11,7 @@ import { BoundedSseFrameBuffer } from "./sse-frame-buffer";
11
11
  import { sseDataPayload as parseSseBlock } from "./sse-payload-rewrite";
12
12
  import { safeResponseHeaders } from "./safe-response-headers";
13
13
  import type { AudioSocketTarget } from "./audio-dictation";
14
+ import type { CodexWebSocketActivity } from "../codex/websocket-activity";
14
15
 
15
16
  export { safeResponseHeaders } from "./safe-response-headers";
16
17
 
@@ -19,6 +20,7 @@ type ResponsesTerminalReporter = (status: ResponsesTerminalStatus) => void;
19
20
  type ResponsesPayloadObserver = (payload: string) => void;
20
21
 
21
22
  export interface WsData {
23
+ responsesActivity?: CodexWebSocketActivity;
22
24
  nativeControl?: NativeResponseControl;
23
25
  /** Content-free per-turn explanation; never a model capability assertion. */
24
26
  nativeSteeringUnavailable?: string;
@@ -1,14 +1,14 @@
1
- /** Fixed-key OpenCode Zen destinations exposing the native Gemini endpoint. */
1
+ /** Fixed-key OpenCode Zen destinations supporting native model protocols. */
2
2
  export interface OpenCodeZenDestination {
3
3
  readonly baseUrl?: unknown;
4
4
  readonly authMode?: unknown;
5
5
  readonly googleMode?: unknown;
6
6
  }
7
7
 
8
- export function openCodeZenBaseUrl(provider: OpenCodeZenDestination | undefined): string | undefined {
8
+ /** Match the official Zen destination and API-key authentication. */
9
+ export function openCodeZenKeyBaseUrl(provider: OpenCodeZenDestination | undefined): string | undefined {
9
10
  if (!provider || typeof provider.baseUrl !== "string") return undefined;
10
11
  if (provider.authMode !== undefined && provider.authMode !== "key") return undefined;
11
- if (provider.googleMode !== undefined && provider.googleMode !== "ai-studio") return undefined;
12
12
  try {
13
13
  const url = new URL(provider.baseUrl.trim());
14
14
  const pathname = url.pathname.replace(/\/+$/, "");
@@ -20,6 +20,12 @@ export function openCodeZenBaseUrl(provider: OpenCodeZenDestination | undefined)
20
20
  }
21
21
  }
22
22
 
23
+ /** Gemini's native endpoint applies only in AI Studio mode. */
24
+ export function openCodeZenBaseUrl(provider: OpenCodeZenDestination | undefined): string | undefined {
25
+ if (provider?.googleMode !== undefined && provider.googleMode !== "ai-studio") return undefined;
26
+ return openCodeZenKeyBaseUrl(provider);
27
+ }
28
+
23
29
  /** Keep the selected destination; Zen omits AI Studio's /v1beta segment. */
24
30
  export function openCodeZenGeminiEndpoint(provider: OpenCodeZenDestination, modelId: string): string | undefined {
25
31
  const baseUrl = openCodeZenBaseUrl(provider);
@@ -68,6 +68,11 @@ export interface OcxParsedRequest {
68
68
  * them, and only for api.anthropic.com; every other destination never sees them.
69
69
  */
70
70
  _anthropicServerTools?: readonly Record<string, unknown>[];
71
+ /**
72
+ * Completed server-tool exchanges from the Claude client's history, keyed by the block that
73
+ * followed them (`call:<tool_use id>` / `text:<text>`). Same audience as `_anthropicServerTools`.
74
+ */
75
+ _anthropicServerReplay?: ReadonlyMap<string, readonly Record<string, unknown>[]>;
71
76
  /**
72
77
  * Boundary between replayed history and this turn's newly appended input. Usually the
73
78
  * items the proxy restored from local previous_response_id state; also set when the
@@ -414,6 +419,9 @@ export interface OcxProviderContinuationState {
414
419
  [provider: string]: Record<string, unknown> | undefined;
415
420
  }
416
421
 
422
+ /** One Anthropic server-side call as the API returned it: `server_tool_use` + its `*_tool_result`. */
423
+ export interface AnthropicServerBlocks { use: Record<string, unknown>; result: Record<string, unknown> }
424
+
417
425
  export type AdapterEvent =
418
426
  | { type: "heartbeat"; replayUnsafe?: true; preflightReady?: true }
419
427
  | { type: "text_delta"; text: string; phase?: OcxMessagePhase;
@@ -443,8 +451,8 @@ export type AdapterEvent =
443
451
  | { type: "web_search_call_begin"; id: string }
444
452
  | {
445
453
  type: "web_search_call_end"; id: string; queries: string[]; status?: "completed" | "failed"; sources?: OcxUrlCitation[];
446
- /** Anthropic's server-side web_fetch (adapters/anthropic.ts): the opened page and its `web_fetch_tool_result` content, verbatim. */
447
- fetch?: { url: string; result: unknown };
454
+ /** Anthropic's server-side call (adapters/anthropic.ts): its `server_tool_use` and result block, verbatim. */
455
+ anthropic?: AnthropicServerBlocks;
448
456
  }
449
457
  | {
450
458
  type: "done";
package/src/types/wire.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { isOpenCodeGoGeminiDestination, OPENCODE_GO_BASE_URL, OPENCODE_GO_MODEL_WIRES, OPENCODE_GO_LEGACY_NAME_PINS, type OpenCodeGoDestination } from "./opencode-go";
2
- import { openCodeZenBaseUrl, type OpenCodeZenDestination } from "./opencode-zen";
2
+ import { openCodeZenBaseUrl, openCodeZenKeyBaseUrl, type OpenCodeZenDestination } from "./opencode-zen";
3
3
 
4
4
  /**
5
5
  * Accepted values for the per-provider upstream HTTP-version pin (#1668). Shared by the
@@ -106,7 +106,9 @@ const CREDENTIAL_PINNED_ADAPTERS: Readonly<Record<string, Readonly<Record<string
106
106
 
107
107
  function prefixPinsFor(providerName: string, provider: WirePinProvider | undefined): Readonly<Record<string, string>> | undefined {
108
108
  if (Object.hasOwn(CREDENTIAL_PINNED_ADAPTERS, providerName)) return CREDENTIAL_PINNED_ADAPTERS[providerName];
109
- if (providerName === "opencode-zen" && openCodeZenBaseUrl(provider)) return { "gemini-": "google" };
109
+ if (providerName === "opencode-zen" && openCodeZenKeyBaseUrl(provider)) {
110
+ return openCodeZenBaseUrl(provider) ? { "gemini-": "google", "claude-": "anthropic" } : { "claude-": "anthropic" };
111
+ }
110
112
  if (!provider || !Object.hasOwn(WIRE_ADAPTER_PIN_PREFIXES, providerName)) return undefined;
111
113
  const rule = WIRE_ADAPTER_PIN_PREFIXES[providerName]!;
112
114
  return normalizedEndpoint(provider.baseUrl) === rule.endpoint ? rule.prefixes : undefined;
package/src/types.ts CHANGED
@@ -61,6 +61,7 @@ export type {
61
61
  OcxProviderContinuationOwner,
62
62
  OcxProviderContinuationState,
63
63
  AdapterEvent,
64
+ AnthropicServerBlocks,
64
65
  OcxUrlCitation,
65
66
  OcxUsage,
66
67
  } from "./types/request";
@@ -665,8 +665,12 @@ function addEstimatedCost(
665
665
  entry: Pick<PersistedUsageEntry, "usageStatus" | "usage" | "attempts">,
666
666
  costInfo: EntryCostInfo,
667
667
  ): void {
668
- if (entry.usageStatus === "unreported" || entry.usageStatus === "unsupported"
669
- || (!entry.usage && !entry.attempts?.length)) {
668
+ // A row of attempts reads `unreported` when any leg lacks usage. Once one leg is measured the row
669
+ // is metered and the attempt-level pricing decides, matching the model and provider breakdowns.
670
+ const measuredAttempt = entry.attempts?.some(attempt => attempt.usage && isMeasuredStatus(attempt.usageStatus));
671
+ if (!measuredAttempt
672
+ && (entry.usageStatus === "unreported" || entry.usageStatus === "unsupported"
673
+ || (!entry.usage && !entry.attempts?.length))) {
670
674
  totals.unmeteredRequests += 1;
671
675
  return;
672
676
  }
@@ -822,7 +826,9 @@ function blankRequestCounts(): UsageRequestCounts {
822
826
  function spentNothing(attempt: NonNullable<PersistedUsageEntry["attempts"]>[number] | undefined): boolean {
823
827
  if (!attempt || attempt.usage) return false;
824
828
  if (attempt.sendCount === 0 || attempt.locallyAnswered === true) return true;
825
- return attempt.status >= 400 && attempt.streamAborted !== true && attempt.firstOutputMs === undefined;
829
+ // A 499 is the client hanging up on a send upstream may already be generating for.
830
+ return attempt.status >= 400 && attempt.status !== 499
831
+ && attempt.streamAborted !== true && attempt.firstOutputMs === undefined;
826
832
  }
827
833
 
828
834
  function bumpRequestCounts(counts: UsageRequestCounts, facts: number, amount = 1): void {
@@ -32,6 +32,7 @@ export const ATTEMPT_RECOVERY_KIND_ROSTER = Object.freeze([
32
32
  "rate-limit-429",
33
33
  "anthropic-oauth-429",
34
34
  "oauth-account-429",
35
+ "oauth-account-402",
35
36
  "image-413",
36
37
  "console-go-upload-retry",
37
38
  "opaque-blob-rejection",
@@ -0,0 +1,59 @@
1
+ import type { AdapterEvent, OcxAssistantContentPart, OcxThinkingContent, OcxToolCall } from "../types";
2
+
3
+ /** Rebuild one validated Anthropic model response without changing its search arguments or order. */
4
+ export function anthropicSearchAssistantContent(events: readonly AdapterEvent[]): OcxAssistantContentPart[] {
5
+ const content: OcxAssistantContentPart[] = [];
6
+ let thinking: OcxThinkingContent | undefined;
7
+ let text: Extract<OcxAssistantContentPart, { type: "text" }> | undefined;
8
+ let call: OcxToolCall | undefined;
9
+ let argumentsText = "";
10
+ for (const event of events) {
11
+ if (event.type === "heartbeat") continue;
12
+ // A signature is a replacement snapshot until the next semantic block starts.
13
+ if (thinking?.signature !== undefined && event.type !== "thinking_signature") thinking = undefined;
14
+ if (event.type !== "text_delta") text = undefined;
15
+ switch (event.type) {
16
+ case "thinking_delta":
17
+ if (!thinking) { thinking = { type: "thinking", thinking: "" }; content.push(thinking); }
18
+ thinking.thinking += event.thinking;
19
+ break;
20
+ case "thinking_signature":
21
+ if (!thinking) { thinking = { type: "thinking", thinking: "" }; content.push(thinking); }
22
+ thinking.signature = event.signature;
23
+ break;
24
+ case "redacted_thinking":
25
+ thinking = undefined;
26
+ content.push({ type: "thinking", thinking: "", redacted: [event.data] });
27
+ break;
28
+ case "text_delta":
29
+ thinking = undefined;
30
+ if (event.anthropicThinkingSignature) {
31
+ text = undefined;
32
+ content.push({ type: "thinking", thinking: event.text, signature: event.anthropicThinkingSignature });
33
+ break;
34
+ }
35
+ if (!text) { text = { type: "text", text: "" }; content.push(text); }
36
+ text.text += event.text;
37
+ break;
38
+ case "tool_call_start":
39
+ thinking = undefined;
40
+ call = { type: "toolCall", id: event.id, name: event.name, arguments: {},
41
+ ...(event.providerMetadata ? { providerMetadata: event.providerMetadata } : {}) };
42
+ content.push(call);
43
+ argumentsText = "";
44
+ break;
45
+ case "tool_call_delta":
46
+ argumentsText += event.arguments;
47
+ break;
48
+ case "tool_call_end": {
49
+ if (!call) throw new Error("search history has an unpaired tool call");
50
+ const args: unknown = JSON.parse(argumentsText || "{}");
51
+ if (!args || typeof args !== "object" || Array.isArray(args)) throw new Error("search history tool input is not an object");
52
+ call.arguments = args as Record<string, unknown>;
53
+ call = undefined;
54
+ break;
55
+ }
56
+ }
57
+ }
58
+ return content;
59
+ }