@bitkyc08/opencodex 2.58.0 → 2.60.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/README.md +28 -10
- package/gui/dist/assets/index-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +34 -1
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-request.ts +8 -5
- package/src/adapters/cursor/request-builder.ts +21 -4
- package/src/adapters/cursor/thread-continuity.ts +105 -31
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +45 -2
- package/src/adapters/devin/cloud-direct/chat.ts +14 -3
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
- package/src/adapters/devin.ts +125 -26
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +55 -4
- package/src/adapters/google-http.ts +57 -11
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +60 -10
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +10 -11
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +25 -4
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +28 -35
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +35 -4
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +26 -6
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/dispatch.ts +57 -3
- package/src/cli/doctor.ts +218 -4
- package/src/cli/help.ts +4 -1
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +95 -22
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +2 -0
- package/src/client/machine-api.ts +6 -5
- package/src/client/machine-listener.ts +16 -3
- package/src/client/runtime.ts +26 -2
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +146 -5
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +44 -10
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +16 -12
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +22 -2
- package/src/codex/catalog/model-hints.ts +66 -33
- package/src/codex/catalog/parsing.ts +90 -5
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +41 -26
- package/src/codex/catalog/routed-gather.ts +61 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +24 -167
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject.ts +3 -2
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +31 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +80 -2
- package/src/codex/observed-model-denials.ts +230 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +289 -16
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +20 -16
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +114 -2
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +125 -5
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +43 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +33 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +31 -7
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +25 -0
- package/src/config/schema/leaf-validators.ts +76 -17
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +410 -258
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +82 -15
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-outbound.ts +53 -5
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +741 -0
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +332 -35
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +41 -27
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +327 -25
- package/src/providers/model-rename-migration.ts +56 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +131 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +87 -21
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/responses/apply-patch-envelope.ts +32 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +9 -2
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +13 -5
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +30 -5
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +24 -25
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +48 -7
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +136 -34
- package/src/server/request-log.ts +80 -3
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +74 -30
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +63 -30
- package/src/server/responses/compact.ts +89 -18
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +90 -24
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +16 -15
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +68 -2
- package/src/server/responses/passthrough-delivery.ts +41 -15
- package/src/server/responses/passthrough-dispatch.ts +145 -53
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +96 -17
- package/src/server/responses/request-send-budget.ts +89 -8
- package/src/server/responses/request-sidecar-auth.ts +17 -9
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +15 -12
- package/src/server/responses/run-turn-execution.ts +45 -8
- package/src/server/responses/sidecar-execution.ts +19 -2
- package/src/server/responses/ws-upstream.ts +16 -28
- package/src/server/responses-custom-tool-repair.ts +29 -56
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +37 -10
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/service/cli.ts +16 -6
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +111 -6
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +9 -1
- package/src/types/tools.ts +14 -0
- package/src/types/wire.ts +9 -1
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-BbrHOIY0.js +0 -128
- package/gui/dist/assets/index-C5-RdDmD.css +0 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Same-target retry of an explicit, pre-output 429 refusal with a stated
|
|
3
|
+
* recovery delay. No event-producing attempt is ever automatically replayed.
|
|
4
|
+
* This is a refusal-specific policy, not a claim that every eventless POST
|
|
5
|
+
* is idempotent; ambiguous transport failures still propagate unchanged.
|
|
6
|
+
*/
|
|
7
|
+
import { parseRetryAfterFromMessage } from '../../../lib/retry-delay.js';
|
|
8
|
+
import type { AdapterFetchContext } from '../../base.js';
|
|
9
|
+
import { createAdapterPhysicalSend } from '../../physical-send.js';
|
|
10
|
+
import { abortError, SendBudgetExhaustedError, sleepWithAbort } from '../../../lib/upstream-retry.js';
|
|
11
|
+
import { CloudChatError, streamChatEvents, type CloudChatEvent, type CloudChatRequest } from './chat.js';
|
|
12
|
+
|
|
13
|
+
/** 1 initial attempt plus at most 2 replays. */
|
|
14
|
+
export const STATED_RESET_MAX_REPLAYS = 2;
|
|
15
|
+
/** Default cumulative wait allowance for one invocation (30 minutes). */
|
|
16
|
+
export const STATED_RESET_MAX_WAIT_MS = 1_800_000;
|
|
17
|
+
/** Absolute maximum cumulative allowance, including explicit overrides. */
|
|
18
|
+
export const STATED_RESET_WAIT_CEILING_MS = 3_600_000;
|
|
19
|
+
|
|
20
|
+
function statedResetMaxWaitMs(): number {
|
|
21
|
+
const raw = process.env.OPENCODEX_DEVIN_STATED_RESET_WAIT_MS?.trim();
|
|
22
|
+
if (!raw) return STATED_RESET_MAX_WAIT_MS;
|
|
23
|
+
const parsed = Number(raw);
|
|
24
|
+
if (!Number.isFinite(parsed) || parsed < 0) return STATED_RESET_MAX_WAIT_MS;
|
|
25
|
+
// Zero explicitly disables local waiting. Values above one hour are capped.
|
|
26
|
+
return Math.min(Math.floor(parsed), STATED_RESET_WAIT_CEILING_MS);
|
|
27
|
+
}
|
|
28
|
+
export const statedResetMaxWaitMsForTests = statedResetMaxWaitMs;
|
|
29
|
+
|
|
30
|
+
export interface StatedResetRetryOptions {
|
|
31
|
+
/** Test seam: defaults to the real cloud stream. */
|
|
32
|
+
stream?: (req: CloudChatRequest) => AsyncGenerator<CloudChatEvent>;
|
|
33
|
+
/** Test seam: must either honour the whole delay or reject on cancellation. */
|
|
34
|
+
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
35
|
+
maxReplays?: number;
|
|
36
|
+
/** CUMULATIVE wait allowance, not a fresh allowance on every failure. */
|
|
37
|
+
maxWaitMs?: number;
|
|
38
|
+
/** Request-wide execution authority. Absent preserves context-free callers' unlimited fetch. */
|
|
39
|
+
execution?: Pick<
|
|
40
|
+
AdapterFetchContext,
|
|
41
|
+
"executor" | "sendBudget" | "onPhysicalSend" | "onRecoveryWithheld"
|
|
42
|
+
>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function replayLimit(value: number | undefined): number {
|
|
46
|
+
if (value === undefined) return STATED_RESET_MAX_REPLAYS;
|
|
47
|
+
if (!Number.isInteger(value) || value < 0 || value > STATED_RESET_MAX_REPLAYS) {
|
|
48
|
+
throw new RangeError('maxReplays must be an integer from 0 to 2');
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function waitLimit(value: number | undefined): number {
|
|
54
|
+
if (value === undefined) return statedResetMaxWaitMs();
|
|
55
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
56
|
+
throw new RangeError('maxWaitMs must be a finite non-negative number');
|
|
57
|
+
}
|
|
58
|
+
return Math.min(Math.floor(value), STATED_RESET_WAIT_CEILING_MS);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function* streamChatEventsWithResetRetry(
|
|
62
|
+
req: CloudChatRequest,
|
|
63
|
+
options?: StatedResetRetryOptions,
|
|
64
|
+
): AsyncGenerator<CloudChatEvent> {
|
|
65
|
+
const stream = options?.stream ?? streamChatEvents;
|
|
66
|
+
const sleep = options?.sleep ?? sleepWithAbort;
|
|
67
|
+
const maxReplays = replayLimit(options?.maxReplays);
|
|
68
|
+
const maxWaitMs = waitLimit(options?.maxWaitMs);
|
|
69
|
+
const execution = options?.execution;
|
|
70
|
+
// One sender owns the whole invocation so ordinals span the initial POST and both replays.
|
|
71
|
+
// Its executor remains lazy: replay admission happens after the provider-stated wait, never
|
|
72
|
+
// while a reservation could be held for up to an hour.
|
|
73
|
+
const send = execution
|
|
74
|
+
? createAdapterPhysicalSend({ ...execution, abortSignal: req.signal })
|
|
75
|
+
: undefined;
|
|
76
|
+
let replays = 0;
|
|
77
|
+
let waitedMs = 0;
|
|
78
|
+
let replaySourceError: CloudChatError | undefined;
|
|
79
|
+
while (true) {
|
|
80
|
+
// Check again after sleeping: cancellation can race with timer completion.
|
|
81
|
+
// A pre-aborted request must not even enter a custom transport.
|
|
82
|
+
if (req.signal?.aborted) throw abortError(req.signal);
|
|
83
|
+
let yielded = false;
|
|
84
|
+
try {
|
|
85
|
+
const recovery = replays > 0 ? "rate-limit-429" as const : undefined;
|
|
86
|
+
const attemptRequest = send
|
|
87
|
+
? {
|
|
88
|
+
...req,
|
|
89
|
+
executor: ((input, init) => send({
|
|
90
|
+
url: typeof input === "string" ? input : input instanceof URL ? input.href : input.url,
|
|
91
|
+
...(recovery ? { sendClass: "auth-recovery" as const, recovery } : {}),
|
|
92
|
+
dispatch: executor => executor(input, init),
|
|
93
|
+
})) as typeof globalThis.fetch,
|
|
94
|
+
}
|
|
95
|
+
: req;
|
|
96
|
+
for await (const event of stream(attemptRequest)) {
|
|
97
|
+
// Latch before yielding, so a consumer-injected error is post-output.
|
|
98
|
+
yielded = true;
|
|
99
|
+
yield event;
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (req.signal?.aborted) throw abortError(req.signal);
|
|
104
|
+
if (!yielded && error instanceof SendBudgetExhaustedError && replaySourceError) {
|
|
105
|
+
// The provider's refusal is the real upstream answer. A local cap can withhold its
|
|
106
|
+
// recovery, but replacing the 429 would erase the status and stated reset metadata.
|
|
107
|
+
execution?.onRecoveryWithheld?.({ reason: "retry-send-budget" });
|
|
108
|
+
throw replaySourceError;
|
|
109
|
+
}
|
|
110
|
+
const retryableError = !yielded
|
|
111
|
+
&& error instanceof CloudChatError
|
|
112
|
+
&& error.status === 429
|
|
113
|
+
? error
|
|
114
|
+
: undefined;
|
|
115
|
+
const waitSec = retryableError
|
|
116
|
+
? parseRetryAfterFromMessage(retryableError.message)
|
|
117
|
+
: undefined;
|
|
118
|
+
const waitMs = waitSec === undefined ? undefined : waitSec * 1000;
|
|
119
|
+
if (
|
|
120
|
+
retryableError === undefined
|
|
121
|
+
|| waitMs === undefined
|
|
122
|
+
|| replays >= maxReplays
|
|
123
|
+
|| waitMs > maxWaitMs - waitedMs
|
|
124
|
+
) {
|
|
125
|
+
// Never shorten a provider's minimum delay to fit the local budget.
|
|
126
|
+
// Keep the original refusal so outer policy can preserve its metadata.
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
replays += 1;
|
|
130
|
+
replaySourceError = retryableError;
|
|
131
|
+
// Charge the complete scheduled wait once, before sleeping. This is a
|
|
132
|
+
// sleep allowance, not a wall-clock deadline on generation or timer
|
|
133
|
+
// scheduling: waking a few milliseconds late must not reject an already
|
|
134
|
+
// approved one-hour retry. No later wait can spend this allowance again.
|
|
135
|
+
waitedMs += waitMs;
|
|
136
|
+
await sleep(waitMs, req.signal);
|
|
137
|
+
if (req.signal?.aborted) throw abortError(req.signal);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
package/src/adapters/devin.ts
CHANGED
|
@@ -9,12 +9,14 @@
|
|
|
9
9
|
import type { AdapterEvent, OcxAssistantMessage, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTool, OcxToolCall, OcxToolResultMessage, OcxUsage } from "../types";
|
|
10
10
|
import { namespacedToolName } from "../types";
|
|
11
11
|
import type { IncomingMeta, ProviderAdapter } from "./base";
|
|
12
|
-
import {
|
|
12
|
+
import { streamChatEventsWithResetRetry, allocateCascadeId, CloudChatError, type ChatHistoryItem, type ToolDef } from "./devin/cloud-direct";
|
|
13
13
|
import type { ContentPart } from "./devin/cloud-direct/chat";
|
|
14
|
-
import { getCachedCatalog } from "./devin/cloud-direct/catalog";
|
|
14
|
+
import { getCachedCatalog, type CacheEntry } from "./devin/cloud-direct/catalog";
|
|
15
15
|
import { collapseDevinModelUid } from "./devin/live-models";
|
|
16
16
|
import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
|
|
17
17
|
import { DEVIN_DEFAULT_API_SERVER, resolveDevinApiServer } from "../oauth/devin";
|
|
18
|
+
import { isProviderIssuedThinkingSignature } from "../responses/reasoning-envelope";
|
|
19
|
+
import { SendBudgetExhaustedError } from "../lib/upstream-retry";
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Combine two usage frames from one turn by keeping the larger count per field.
|
|
@@ -155,6 +157,7 @@ async function resolveWireModelUid(
|
|
|
155
157
|
apiKey: string,
|
|
156
158
|
host: string,
|
|
157
159
|
reasoningEffort?: string,
|
|
160
|
+
catalog?: CacheEntry | null,
|
|
158
161
|
): Promise<string> {
|
|
159
162
|
const modelId = normalizeDevinModelId(rawModelId);
|
|
160
163
|
// Explicit effort wins over a suffix the picker already baked into the id, so
|
|
@@ -163,15 +166,18 @@ async function resolveWireModelUid(
|
|
|
163
166
|
const swe2 = resolveSwe2Variant(modelId, reasoningEffort);
|
|
164
167
|
if (swe2) return swe2;
|
|
165
168
|
if (hasEffortSuffix(modelId)) return modelId;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
+
// Callers that already read the catalog this turn pass it in; an explicit
|
|
170
|
+
// null records a failed lookup and must not trigger a same-turn retry —
|
|
171
|
+
// failures are not cached, so re-reading would only pay another timeout.
|
|
172
|
+
const entry = catalog !== undefined ? catalog : await getCachedCatalog(apiKey, host);
|
|
173
|
+
if (entry) {
|
|
174
|
+
if (entry.byUid.has(modelId)) return modelId;
|
|
169
175
|
const effort = reasoningEffort && CALLER_EFFORT_VALUES.has(reasoningEffort) ? reasoningEffort : "medium";
|
|
170
176
|
const suffixed = `${modelId}-${effort}`;
|
|
171
|
-
if (
|
|
177
|
+
if (entry.byUid.has(suffixed)) return suffixed;
|
|
172
178
|
// Fall back to any enabled variant of this base model.
|
|
173
|
-
for (const uid of
|
|
174
|
-
if (uid.startsWith(modelId + "-") && !
|
|
179
|
+
for (const uid of entry.byUid.keys()) {
|
|
180
|
+
if (uid.startsWith(modelId + "-") && !entry.byUid.get(uid)?.disabled) return uid;
|
|
175
181
|
}
|
|
176
182
|
}
|
|
177
183
|
// Degraded mode: append the default effort suffix.
|
|
@@ -186,6 +192,46 @@ async function resolveWireModelUid(
|
|
|
186
192
|
*/
|
|
187
193
|
export const resolveWireModelUidForTests = resolveWireModelUid;
|
|
188
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Resolve the INPUT ceiling for the exact UID selected for this turn. Catalog
|
|
197
|
+
* ClientModelConfig #18 and CompletionConfiguration #3 both carry input tokens;
|
|
198
|
+
* the independent output cap is not subtracted here. Smaller operator hints
|
|
199
|
+
* cap live evidence, never enlarge it. No evidence leaves the encoder's 128k
|
|
200
|
+
* fallback intact; an unrelated or opt-in long-context variant is not evidence.
|
|
201
|
+
*/
|
|
202
|
+
function resolveDevinMaxInputTokens(
|
|
203
|
+
provider: OcxProviderConfig,
|
|
204
|
+
modelUid: string,
|
|
205
|
+
liveWindow?: number,
|
|
206
|
+
): number | undefined {
|
|
207
|
+
const positive = (value: unknown): number | undefined =>
|
|
208
|
+
typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
209
|
+
const baseId = collapseDevinModelUid(modelUid);
|
|
210
|
+
const configured = (record: Record<string, number> | undefined): number | undefined => {
|
|
211
|
+
if (!record) return undefined;
|
|
212
|
+
for (const id of [modelUid, baseId]) {
|
|
213
|
+
// Prefer the canonical spelling; retain dotted/case-folded saved hints,
|
|
214
|
+
// matching the model-id normalization used for the inference request.
|
|
215
|
+
const exact = Object.hasOwn(record, id) ? positive(record[id]) : undefined;
|
|
216
|
+
if (exact !== undefined) return exact;
|
|
217
|
+
const matches = Object.entries(record)
|
|
218
|
+
.filter(([key]) => normalizeDevinModelId(key).toLowerCase() === id.toLowerCase())
|
|
219
|
+
.map(([, value]) => positive(value))
|
|
220
|
+
.filter((value): value is number => value !== undefined);
|
|
221
|
+
if (matches.length > 0) return Math.min(...matches);
|
|
222
|
+
}
|
|
223
|
+
return undefined;
|
|
224
|
+
};
|
|
225
|
+
const contextHint = configured(provider.modelContextWindows) ?? positive(provider.contextWindow);
|
|
226
|
+
const inputHint = configured(provider.modelMaxInputTokens);
|
|
227
|
+
const ceilings = [positive(liveWindow), contextHint, inputHint]
|
|
228
|
+
.filter((value): value is number => value !== undefined);
|
|
229
|
+
return ceilings.length > 0 ? Math.min(...ceilings) : undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Pure test seam; runtime uses the same resolver immediately before dispatch. */
|
|
233
|
+
export const resolveDevinMaxInputTokensForTests = resolveDevinMaxInputTokens;
|
|
234
|
+
|
|
189
235
|
export class DevinMissingCredentialError extends Error {
|
|
190
236
|
constructor() {
|
|
191
237
|
super("Devin live transport requires a Devin API key. Run ocx login devin to sign in with your Cognition/Devin account.");
|
|
@@ -209,6 +255,17 @@ function textFromParts(content: string | OcxContentPart[] | undefined): string {
|
|
|
209
255
|
return content.map((part) => (part.type === "text" ? part.text : "")).filter(Boolean).join("\n");
|
|
210
256
|
}
|
|
211
257
|
|
|
258
|
+
const MAX_DEVIN_REMOTE_IMAGE_URL_CHARS = 8_192;
|
|
259
|
+
|
|
260
|
+
function boundedDevinRemoteImageReference(imageUrl: string): string | undefined {
|
|
261
|
+
if (imageUrl.length > MAX_DEVIN_REMOTE_IMAGE_URL_CHARS) return undefined;
|
|
262
|
+
try {
|
|
263
|
+
return new URL(imageUrl).protocol === "https:" ? imageUrl : undefined;
|
|
264
|
+
} catch {
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
212
269
|
/**
|
|
213
270
|
* Convert inbound content parts to the multimodal shape the wire encoder accepts.
|
|
214
271
|
*
|
|
@@ -217,9 +274,10 @@ function textFromParts(content: string | OcxContentPart[] | undefined): string {
|
|
|
217
274
|
* text-only string and a message whose only content was an image was dropped
|
|
218
275
|
* entirely, which is why a pasted screenshot killed the turn and the only
|
|
219
276
|
* workaround was running OCR before sending. A data: URL carries everything
|
|
220
|
-
* field #10 needs; a remote https URL cannot be inlined without a fetch,
|
|
221
|
-
* stays as an explicit text reference rather than pretending the model can
|
|
222
|
-
* a picture it cannot.
|
|
277
|
+
* field #10 needs; a bounded remote https URL cannot be inlined without a fetch,
|
|
278
|
+
* so it stays as an explicit text reference rather than pretending the model can
|
|
279
|
+
* see a picture it cannot. Unsupported and oversized references become a fixed
|
|
280
|
+
* omission marker, never attacker-sized prompt text. Video has no Devin field.
|
|
223
281
|
*/
|
|
224
282
|
function mapOcxContentToWire(content: string | OcxContentPart[] | undefined): string | ContentPart[] {
|
|
225
283
|
if (typeof content === "string" || !Array.isArray(content)) return content ?? "";
|
|
@@ -230,7 +288,13 @@ function mapOcxContentToWire(content: string | OcxContentPart[] | undefined): st
|
|
|
230
288
|
} else if (part.type === "image") {
|
|
231
289
|
const m = part.imageUrl.match(/^data:([^;]+);base64,(.+)$/);
|
|
232
290
|
if (m) out.push({ type: "image", mimeType: m[1]!, base64Data: m[2]! });
|
|
233
|
-
else
|
|
291
|
+
else {
|
|
292
|
+
const remoteReference = boundedDevinRemoteImageReference(part.imageUrl);
|
|
293
|
+
out.push({
|
|
294
|
+
type: "text",
|
|
295
|
+
text: remoteReference ? `[image url: ${remoteReference}]` : "[image omitted: unsupported or oversized URL]",
|
|
296
|
+
});
|
|
297
|
+
}
|
|
234
298
|
}
|
|
235
299
|
}
|
|
236
300
|
return out;
|
|
@@ -266,23 +330,26 @@ function assistantText(message: OcxAssistantMessage): string {
|
|
|
266
330
|
* clients of the same service write #11 thinking with #12 signature and #18
|
|
267
331
|
* signature_type on the assistant prompt.
|
|
268
332
|
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
333
|
+
* Field #12 attests the exact text at #11, and the wire has room for one pair.
|
|
334
|
+
* Every block that carries text is replayed, so the chain stays intact; the
|
|
335
|
+
* signature rides along only when the text being replayed IS the text it
|
|
336
|
+
* attests, which is exactly the single-block case. Several independently signed
|
|
337
|
+
* blocks send an unsigned prompt rather than pairing one block's attestation
|
|
338
|
+
* with another block's words. A signature-only block attests encrypted thinking
|
|
339
|
+
* that is not being replayed at all, so it is not one of these blocks and
|
|
340
|
+
* cannot contribute the pair.
|
|
271
341
|
*/
|
|
272
342
|
function assistantThinking(
|
|
273
343
|
message: OcxAssistantMessage,
|
|
274
344
|
): { thinking?: string; signature?: string } {
|
|
275
345
|
const blocks = message.content.filter(
|
|
276
346
|
(part): part is Extract<typeof part, { type: "thinking" }> => part.type === "thinking",
|
|
277
|
-
);
|
|
347
|
+
).filter(part => Boolean(part.thinking));
|
|
278
348
|
if (blocks.length === 0) return {};
|
|
279
|
-
const
|
|
280
|
-
// Only one signature can ride the prompt, so take the last block that has
|
|
281
|
-
// one: that is the block the turn actually ended on.
|
|
282
|
-
const signature = blocks.filter(b => b.signature).at(-1)?.signature;
|
|
349
|
+
const signature = blocks.length === 1 ? blocks[0]!.signature : undefined;
|
|
283
350
|
return {
|
|
284
|
-
|
|
285
|
-
...(signature ? { signature } : {}),
|
|
351
|
+
thinking: blocks.map(part => part.thinking).join("\n"),
|
|
352
|
+
...(isProviderIssuedThinkingSignature(signature) ? { signature } : {}),
|
|
286
353
|
};
|
|
287
354
|
}
|
|
288
355
|
|
|
@@ -446,6 +513,10 @@ export function createDevinAdapter(
|
|
|
446
513
|
|
|
447
514
|
return {
|
|
448
515
|
name: "devin",
|
|
516
|
+
// Every GetChatMessage send, including the first, is admitted through the shared budget and
|
|
517
|
+
// reported from the executor that dispatches it. The caller therefore leaves the first
|
|
518
|
+
// send's accounting here rather than logging it before admission can refuse it.
|
|
519
|
+
reportsPhysicalSends: true,
|
|
449
520
|
|
|
450
521
|
buildRequest() {
|
|
451
522
|
return {
|
|
@@ -493,7 +564,16 @@ export function createDevinAdapter(
|
|
|
493
564
|
// entry: an EU or FedStart account that used provider.baseUrl would send
|
|
494
565
|
// every RPC to the US server it is not provisioned on.
|
|
495
566
|
const host = resolveDevinApiServer(provider.baseUrl, credentialProviderId);
|
|
496
|
-
|
|
567
|
+
// One catalog read per turn serves model-UID resolution, the input
|
|
568
|
+
// ceiling, and the chat pre-flight inside streamChatEvents. Failures are
|
|
569
|
+
// not cached, so a second read would only pay another fetch timeout on
|
|
570
|
+
// an otherwise valid turn.
|
|
571
|
+
const catalog = await getCachedCatalog(apiKey, host, incoming.abortSignal);
|
|
572
|
+
if (incoming.abortSignal?.aborted) {
|
|
573
|
+
emit({ type: "error", message: DEVIN_CLIENT_CLOSED_MESSAGE, status: 499, retryable: false });
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const modelUid = await resolveWireModelUid(rawModelId, apiKey, host, parsed.options.reasoning, catalog);
|
|
497
577
|
const returnedToolNames = buildDevinReturnedToolNameMap(parsed.context.tools);
|
|
498
578
|
let openToolId: string | undefined;
|
|
499
579
|
let usage: OcxUsage | undefined;
|
|
@@ -506,22 +586,38 @@ export function createDevinAdapter(
|
|
|
506
586
|
};
|
|
507
587
|
|
|
508
588
|
try {
|
|
509
|
-
|
|
589
|
+
// Read the selected UID's catalog row, not the picker's collapsed base.
|
|
590
|
+
const maxInputTokens = resolveDevinMaxInputTokens(
|
|
591
|
+
provider, modelUid, catalog?.byUid.get(modelUid)?.contextWindow,
|
|
592
|
+
);
|
|
593
|
+
// The reset-retry wrapper waits out a 429 that states its own recovery
|
|
594
|
+
// delay ("limit will reset in 35 seconds") and replays the identical
|
|
595
|
+
// request — but only while zero events have been yielded, so a
|
|
596
|
+
// post-output failure still takes the terminal path untouched.
|
|
597
|
+
for await (const event of streamChatEventsWithResetRetry({
|
|
510
598
|
apiKey,
|
|
511
599
|
apiServerUrl: host,
|
|
512
600
|
modelUid,
|
|
601
|
+
catalog,
|
|
513
602
|
messages: mapOcxMessagesToDevin(parsed),
|
|
514
603
|
tools: mapOcxToolsToDevin(parsed.context.tools),
|
|
515
604
|
cascadeId,
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
// that asked for a 4k cap never got one.
|
|
605
|
+
// Input and output ceilings are separate wire fields. Omitting the
|
|
606
|
+
// input hint used to force every model through the 128k default.
|
|
519
607
|
completionOpts: {
|
|
608
|
+
...(maxInputTokens !== undefined ? { maxInputTokens } : {}),
|
|
520
609
|
...(typeof parsed.options.maxOutputTokens === "number" ? { maxOutputTokens: parsed.options.maxOutputTokens } : {}),
|
|
521
610
|
...(typeof parsed.options.temperature === "number" ? { temperature: parsed.options.temperature } : {}),
|
|
522
611
|
...(typeof parsed.options.topP === "number" ? { topP: parsed.options.topP } : {}),
|
|
523
612
|
},
|
|
524
613
|
signal: incoming.abortSignal,
|
|
614
|
+
}, {
|
|
615
|
+
execution: {
|
|
616
|
+
executor: incoming.providerFetch,
|
|
617
|
+
sendBudget: incoming.sendBudget,
|
|
618
|
+
onPhysicalSend: incoming.onPhysicalSend,
|
|
619
|
+
onRecoveryWithheld: incoming.onRecoveryWithheld,
|
|
620
|
+
},
|
|
525
621
|
})) {
|
|
526
622
|
if (incoming.abortSignal?.aborted) {
|
|
527
623
|
// Emitting nothing here left the bridge to synthesize adapter_eof.
|
|
@@ -600,6 +696,9 @@ export function createDevinAdapter(
|
|
|
600
696
|
emit({ type: "error", message: DEVIN_CLIENT_CLOSED_MESSAGE, status: 499, retryable: false, ...(usage ? { usage } : {}) });
|
|
601
697
|
return;
|
|
602
698
|
}
|
|
699
|
+
// The Responses boundary already maps this local refusal to its structured 429 code.
|
|
700
|
+
// Converting it to an adapter event would make it an ordinary untyped upstream error.
|
|
701
|
+
if (error instanceof SendBudgetExhaustedError) throw error;
|
|
603
702
|
const message = error instanceof CloudChatError
|
|
604
703
|
? ("Devin cloud error" + (error.code ? " " + error.code : "") + ": " + error.message)
|
|
605
704
|
: error instanceof Error ? error.message : String(error);
|
|
@@ -629,7 +629,7 @@ export function antigravityUsesReplayCache(model: string): boolean {
|
|
|
629
629
|
* neither the wire metadata nor the replay cache can supply a real one, this is the official
|
|
630
630
|
* validator-bypass token.
|
|
631
631
|
*/
|
|
632
|
-
const THOUGHT_SIGNATURE_BYPASS = "skip_thought_signature_validator";
|
|
632
|
+
export const THOUGHT_SIGNATURE_BYPASS = "skip_thought_signature_validator";
|
|
633
633
|
|
|
634
634
|
/**
|
|
635
635
|
* True when the model speaks the Gemini wire dialect that requires a thought signature on the
|
|
@@ -77,7 +77,7 @@ export function antigravitySessionId(parsed: OcxParsedRequest): string {
|
|
|
77
77
|
//
|
|
78
78
|
// Clients that send no thread header keep the text anchor and its instability; this is a scoped
|
|
79
79
|
// repair, not a universal one.
|
|
80
|
-
const text =
|
|
80
|
+
const text = codexThreadAnchor(parsed).preimage ?? firstUserText(parsed);
|
|
81
81
|
if (!text) return `-${Math.floor(Math.random() * 9e18).toString()}`;
|
|
82
82
|
const digest = createHash("sha256").update(text, "utf8").digest();
|
|
83
83
|
const masked = digest.readBigUInt64BE(0) & 0x7fffffffffffffffn;
|
|
@@ -96,9 +96,60 @@ export function antigravitySessionId(parsed: OcxParsedRequest): string {
|
|
|
96
96
|
* functionCall identity (name+args), so a shared id does not misattribute them. Instability, not
|
|
97
97
|
* collision, is the failure mode this function exists to prevent.
|
|
98
98
|
*/
|
|
99
|
-
function
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
function codexThreadAnchor(parsed: OcxParsedRequest): {
|
|
100
|
+
kind: "parent-and-own" | "parent-only" | "absent";
|
|
101
|
+
preimage?: string;
|
|
102
|
+
} {
|
|
103
|
+
// `_clientThreadId` carries `x-codex-parent-thread-id`, which every parallel child of one
|
|
104
|
+
// parent presents identically, so anchoring on it alone collapsed concurrent children onto a
|
|
105
|
+
// single upstream Cloud Code Assist session (#5033).
|
|
106
|
+
//
|
|
107
|
+
// A thread id is only unique WITHIN its parent, which is why `codexConversationIdentity` keys
|
|
108
|
+
// on both. #5054 anchored on the child alone and therefore only moved the collision: two
|
|
109
|
+
// parents can each have a child of the same id (#5058). The pair is the identity, joined by a
|
|
110
|
+
// NUL so the encoding is injective: no Codex id contains one, so a pair cannot be re-read as a
|
|
111
|
+
// different pair, nor as the parent-only anchor below.
|
|
112
|
+
//
|
|
113
|
+
// Deliberately NOT the general lane key: `codexConversationKeyFor` is an HMAC under a
|
|
114
|
+
// process-random secret, so it changes across a proxy restart — and instability, not sharing,
|
|
115
|
+
// is the failure mode this derivation has to avoid. These ids are Codex's own values and
|
|
116
|
+
// survive both compaction and restart.
|
|
117
|
+
const own = parsed._codexOwnThreadId?.trim();
|
|
118
|
+
const parent = parsed._clientThreadId?.trim();
|
|
119
|
+
if (own && parent) return { kind: "parent-and-own", preimage: `codex-thread:${parent}\u0000${own}` };
|
|
120
|
+
// Parent-only clients keep the anchor they already had.
|
|
121
|
+
if (parent) return { kind: "parent-only", preimage: `codex-thread:${parent}` };
|
|
122
|
+
// A parentless ROOT deliberately omits the parent header. `src/server/context-history.ts` says
|
|
123
|
+
// so in as many words: root model requests use (session-id=root, thread-id=root) and do not
|
|
124
|
+
// fabricate a parent key. It has no pair to key on, and #5054's claim that a root presents
|
|
125
|
+
// `thread-id` equal to its parent was simply wrong.
|
|
126
|
+
//
|
|
127
|
+
// So it keeps the pre-#5054 anchor rather than gaining an own-thread one. That is not a
|
|
128
|
+
// preference: durable Antigravity replay state is keyed by model plus session id, and moving a
|
|
129
|
+
// root's anchor on upgrade strands every signature stored under the old session — the exact
|
|
130
|
+
// instability this derivation exists to avoid, introduced while fixing sharing.
|
|
131
|
+
return { kind: "absent" };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Which class of anchor {@link antigravitySessionId} derives this request's session id from.
|
|
136
|
+
*
|
|
137
|
+
* The id itself identifies a live conversation and is a Google-visible wire value, so it is not
|
|
138
|
+
* something a user can paste into a public bug report. The class is: it distinguishes the four
|
|
139
|
+
* stability regimes above — a Codex parent/child pair, a parent-only client, the content-derived
|
|
140
|
+
* text anchor that compaction can move, and the random fallback that changes every turn — while
|
|
141
|
+
* carrying no identity at all.
|
|
142
|
+
*
|
|
143
|
+
* It reads the SAME decision the id derivation reads, so the two cannot disagree about which
|
|
144
|
+
* regime a request is in. What it does not do is change how the id itself is derived: nothing
|
|
145
|
+
* about a Google-visible wire value moves because a diagnostic wants to describe it.
|
|
146
|
+
*/
|
|
147
|
+
export type AntigravitySessionAnchor = "parent-and-own" | "parent-only" | "text" | "none";
|
|
148
|
+
|
|
149
|
+
export function antigravitySessionAnchor(parsed: OcxParsedRequest): AntigravitySessionAnchor {
|
|
150
|
+
const anchor = codexThreadAnchor(parsed);
|
|
151
|
+
if (anchor.kind !== "absent") return anchor.kind;
|
|
152
|
+
return firstUserText(parsed) ? "text" : "none";
|
|
102
153
|
}
|
|
103
154
|
|
|
104
155
|
/** A Gemini content part as it appears in an Antigravity request body. */
|
|
@@ -2,8 +2,10 @@ import type { AdapterFetchContext, AdapterRequest } from "./base";
|
|
|
2
2
|
import { createAdapterPhysicalSend } from "./physical-send";
|
|
3
3
|
import type { SendClass } from "../lib/request-execution-budget";
|
|
4
4
|
import type { AttemptRecoveryKind } from "../usage/log";
|
|
5
|
+
import { debugProviderDiagnosticLazy } from "../lib/debug";
|
|
5
6
|
import { isQuotaExhaustedBody, retryableGoogleStatus, safeGoogleHttpErrorMessage } from "./google-errors";
|
|
6
|
-
import {
|
|
7
|
+
import { repairGoogleInvalidRequestBodyWithReport } from "./google-wire-compiler";
|
|
8
|
+
import type { GoogleToolSchemaPolicy, GoogleToolSchemaProfile } from "./google-tool-schema";
|
|
7
9
|
import { normalizeUpstreamHttpErrorResponse, readDisplaySafeErrorPayloadText } from "./upstream-http-error";
|
|
8
10
|
import {
|
|
9
11
|
abortError,
|
|
@@ -22,6 +24,8 @@ const GOOGLE_RETRY_MAX_MS = 2_000;
|
|
|
22
24
|
export interface GoogleRetryOptions {
|
|
23
25
|
/** Repair-and-replay structurally invalid 400 bodies (Vertex/Antigravity behavior). */
|
|
24
26
|
repairInvalid400?: boolean;
|
|
27
|
+
toolSchemaProfile?: GoogleToolSchemaProfile;
|
|
28
|
+
toolSchemaPolicy?: GoogleToolSchemaPolicy;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
async function normalizeFinalGoogleError(label: string, res: Response, signal?: AbortSignal): Promise<Response> {
|
|
@@ -45,6 +49,8 @@ export async function fetchGoogleWithRetry(
|
|
|
45
49
|
opts: GoogleRetryOptions = {},
|
|
46
50
|
): Promise<Response> {
|
|
47
51
|
const repairInvalid400 = opts.repairInvalid400 ?? true;
|
|
52
|
+
const toolSchemaProfile = opts.toolSchemaProfile ?? { endpointClass: "vertex" };
|
|
53
|
+
const toolSchemaPolicy = opts.toolSchemaPolicy ?? "compatible";
|
|
48
54
|
const timeoutMs = ctx.timeoutMs ?? 200_000;
|
|
49
55
|
const send = createAdapterPhysicalSend(ctx);
|
|
50
56
|
let lastError: unknown;
|
|
@@ -77,10 +83,22 @@ export async function fetchGoogleWithRetry(
|
|
|
77
83
|
} catch (error) {
|
|
78
84
|
if (ctx.abortSignal?.aborted) throw error;
|
|
79
85
|
}
|
|
80
|
-
const
|
|
81
|
-
if (
|
|
86
|
+
const repair = repairGoogleInvalidRequestBodyWithReport(activeRequest.body, payloadText, toolSchemaProfile);
|
|
87
|
+
if (repair !== undefined) {
|
|
88
|
+
if (repair.toolSchemaLoss) {
|
|
89
|
+
const changedSendAllowed = toolSchemaPolicy !== "reject-lossy";
|
|
90
|
+
debugProviderDiagnosticLazy("google", "google-tool-schema-repair", () => ({
|
|
91
|
+
...repair.toolSchemaLoss,
|
|
92
|
+
phase: "repair",
|
|
93
|
+
declarationCount: repair.toolSchemaDeclarationCount ?? 0,
|
|
94
|
+
changedSendAllowed,
|
|
95
|
+
}));
|
|
96
|
+
if (!changedSendAllowed) {
|
|
97
|
+
return ctx.returnRawErrors ? res : normalizeFinalGoogleError(label, res, ctx.abortSignal);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
82
100
|
compatibilityReplayUsed = true;
|
|
83
|
-
activeRequest = { ...activeRequest, body:
|
|
101
|
+
activeRequest = { ...activeRequest, body: repair.body };
|
|
84
102
|
pendingResponse = res;
|
|
85
103
|
sendClass = "repair";
|
|
86
104
|
attempt--; // The changed-request replay is separate from transient retry accounting.
|
|
@@ -112,7 +130,14 @@ export async function fetchGoogleWithRetry(
|
|
|
112
130
|
} catch (err) {
|
|
113
131
|
if (ctx.abortSignal?.aborted) throw err;
|
|
114
132
|
if (err instanceof SendBudgetExhaustedError) {
|
|
115
|
-
if (pendingResponse)
|
|
133
|
+
if (pendingResponse) {
|
|
134
|
+
// The ladder had already classified this response as retryable and was about to send
|
|
135
|
+
// again; the budget refused. Returning the original response is right — it is a real
|
|
136
|
+
// upstream answer — but it used to leave the log indistinguishable from a request
|
|
137
|
+
// where no retry was ever eligible (#5044).
|
|
138
|
+
ctx.onRecoveryWithheld?.({ reason: "retry-send-budget" });
|
|
139
|
+
return ctx.returnRawErrors ? pendingResponse : normalizeFinalGoogleError(label, pendingResponse, ctx.abortSignal);
|
|
140
|
+
}
|
|
116
141
|
throw err;
|
|
117
142
|
}
|
|
118
143
|
lastError = err;
|
|
@@ -138,16 +163,37 @@ export async function fetchGoogleWithRetry(
|
|
|
138
163
|
* demand" spikes, plus plain rate-limit 429s, both of which previously failed immediately
|
|
139
164
|
* because the default server fetch path only retries connection resets.
|
|
140
165
|
*/
|
|
141
|
-
export function fetchDirectGeminiWithRetry(
|
|
142
|
-
|
|
166
|
+
export function fetchDirectGeminiWithRetry(
|
|
167
|
+
request: AdapterRequest,
|
|
168
|
+
ctx: AdapterFetchContext = {},
|
|
169
|
+
schemaPolicy: Pick<GoogleRetryOptions, "toolSchemaProfile" | "toolSchemaPolicy"> = {},
|
|
170
|
+
): Promise<Response> {
|
|
171
|
+
return fetchGoogleWithRetry("Gemini", request, { ...ctx, returnRawErrors: true }, {
|
|
172
|
+
...schemaPolicy,
|
|
173
|
+
repairInvalid400: false,
|
|
174
|
+
});
|
|
143
175
|
}
|
|
144
176
|
|
|
145
177
|
/** Vertex AI retry wrapper. */
|
|
146
|
-
export function fetchVertexWithRetry(
|
|
147
|
-
|
|
178
|
+
export function fetchVertexWithRetry(
|
|
179
|
+
request: AdapterRequest,
|
|
180
|
+
ctx: AdapterFetchContext = {},
|
|
181
|
+
schemaPolicy: Pick<GoogleRetryOptions, "toolSchemaProfile" | "toolSchemaPolicy"> = {},
|
|
182
|
+
): Promise<Response> {
|
|
183
|
+
return fetchGoogleWithRetry("Vertex AI", request, ctx, {
|
|
184
|
+
toolSchemaProfile: { endpointClass: "vertex" },
|
|
185
|
+
...schemaPolicy,
|
|
186
|
+
});
|
|
148
187
|
}
|
|
149
188
|
|
|
150
189
|
/** Antigravity (Cloud Code Assist) retry wrapper. */
|
|
151
|
-
export function fetchAntigravityWithRetry(
|
|
152
|
-
|
|
190
|
+
export function fetchAntigravityWithRetry(
|
|
191
|
+
request: AdapterRequest,
|
|
192
|
+
ctx: AdapterFetchContext = {},
|
|
193
|
+
schemaPolicy: Pick<GoogleRetryOptions, "toolSchemaProfile" | "toolSchemaPolicy"> = {},
|
|
194
|
+
): Promise<Response> {
|
|
195
|
+
return fetchGoogleWithRetry("Antigravity", request, ctx, {
|
|
196
|
+
toolSchemaProfile: { endpointClass: "cloud-code-assist" },
|
|
197
|
+
...schemaPolicy,
|
|
198
|
+
});
|
|
153
199
|
}
|