@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.
- package/gui/dist/assets/{App-ZfYbLHkB.js → App-CHC89EXQ.js} +3 -3
- package/gui/dist/assets/{Tray-DCd-A1az.js → Tray-B5bC7AWL.js} +1 -1
- package/gui/dist/assets/index-B5bsvxor.js +94 -0
- package/gui/dist/assets/{quota-summary-ChvmKGOk.js → quota-summary-D8y2wTsB.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic-rejection-memo.ts +71 -0
- package/src/adapters/anthropic-replay.ts +135 -0
- package/src/adapters/anthropic-tool-catalog.ts +11 -0
- package/src/adapters/anthropic.ts +76 -45
- package/src/adapters/google-progress-history.ts +53 -0
- package/src/adapters/google.ts +17 -1
- package/src/bridge/internal.ts +6 -2
- package/src/bridge/response-json.ts +2 -2
- package/src/bridge/sse.ts +7 -6
- package/src/claude/inbound-content-options.ts +66 -0
- package/src/claude/outbound.ts +13 -5
- package/src/cli/cross-home-owner.ts +16 -2
- package/src/cli/loopback-family.ts +31 -0
- package/src/codex/catalog/routed-gather.ts +3 -1
- package/src/codex/websocket-activity.ts +161 -0
- package/src/codex/websocket-registry.ts +25 -1
- package/src/config/opencode-zen-upgrade.ts +9 -8
- package/src/generated/compatibility-version.json +88 -52
- package/src/images/loop.ts +72 -21
- package/src/lib/request-failure-model.ts +2 -0
- package/src/oauth/generic-account-failover.ts +7 -4
- package/src/protocols/encoders/adapter-events.ts +5 -5
- package/src/protocols/encoders/messages.ts +2 -2
- package/src/providers/opencode-zen-rate-limit.ts +7 -2
- package/src/responses/reasoning-replay-cache.ts +50 -4
- package/src/routing/analytics.ts +7 -15
- package/src/server/claude-messages.ts +2 -1
- package/src/server/index/live-sideband.ts +3 -2
- package/src/server/index/serve-options.ts +11 -5
- package/src/server/index/websocket-handler.ts +18 -4
- package/src/server/index.ts +7 -7
- package/src/server/lifecycle.ts +13 -1
- package/src/server/messages-native.ts +16 -3
- package/src/server/responses/adapter-continuation.ts +34 -18
- package/src/server/responses/adapter-delivery.ts +53 -6
- package/src/server/responses/adapter-dispatch.ts +27 -1
- package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
- package/src/server/responses/combo-stream-preflight.ts +3 -2
- package/src/server/responses/compaction-recovery.ts +2 -0
- package/src/server/responses/core-codex-account.ts +2 -2
- package/src/server/responses/core-combo.ts +3 -2
- package/src/server/responses/core-lifetime.ts +3 -2
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core-replay.ts +5 -1
- package/src/server/responses/passthrough-execution.ts +3 -2
- package/src/server/responses/request-prepare.ts +1 -0
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/response-tool-ownership.ts +21 -0
- package/src/server/responses/sidecar-execution.ts +43 -7
- package/src/server/responses/sidecar-send-budget.ts +2 -2
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/ws-bridge.ts +2 -0
- package/src/types/opencode-zen.ts +9 -3
- package/src/types/request.ts +10 -2
- package/src/types/wire.ts +4 -2
- package/src/types.ts +1 -0
- package/src/usage/summary.ts +9 -3
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/web-search/anthropic-history.ts +59 -0
- package/src/web-search/loop.ts +181 -68
- package/src/web-search/progress-stream.ts +11 -1
- 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.
|
|
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 {
|
package/src/server/ws-bridge.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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);
|
package/src/types/request.ts
CHANGED
|
@@ -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
|
|
447
|
-
|
|
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" &&
|
|
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
package/src/usage/summary.ts
CHANGED
|
@@ -665,8 +665,12 @@ function addEstimatedCost(
|
|
|
665
665
|
entry: Pick<PersistedUsageEntry, "usageStatus" | "usage" | "attempts">,
|
|
666
666
|
costInfo: EntryCostInfo,
|
|
667
667
|
): void {
|
|
668
|
-
|
|
669
|
-
|
|
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
|
-
|
|
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 {
|
|
@@ -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
|
+
}
|