@bitkyc08/opencodex 2.10.0 → 2.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +77 -0
- package/README.md +4 -10
- package/bin/ocx.mjs +71 -18
- package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
- package/gui/dist/assets/index-ChZQsmBY.js +70 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +8 -4
- package/src/adapters/anthropic.ts +208 -14
- package/src/adapters/base.ts +16 -5
- package/src/adapters/cursor/effort-map.ts +3 -2
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/live-transport.ts +105 -95
- package/src/adapters/cursor/native-exec.ts +32 -6
- package/src/adapters/cursor/protobuf-request.ts +20 -15
- package/src/adapters/cursor/request-builder.ts +21 -7
- package/src/adapters/cursor/types.ts +7 -0
- package/src/adapters/google-antigravity-replay.ts +237 -21
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +50 -9
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +10 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +37 -11
- package/src/adapters/openai-chat.ts +284 -83
- package/src/adapters/openai-responses.ts +182 -24
- package/src/bridge.ts +177 -7
- package/src/chat/outbound.ts +78 -23
- package/src/claude/agents-inject.ts +27 -5
- package/src/claude/inbound.ts +11 -1
- package/src/claude/model-info.ts +13 -10
- package/src/claude/outbound.ts +17 -0
- package/src/cli/account-api.ts +24 -0
- package/src/cli/account-auth.ts +31 -6
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +5 -0
- package/src/cli/claude.ts +2 -1
- package/src/cli/doctor.ts +93 -22
- package/src/cli/export-command.ts +26 -12
- package/src/cli/help.ts +8 -6
- package/src/cli/index.ts +56 -22
- package/src/cli/integrations.ts +84 -1
- package/src/cli/observe.ts +54 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +1 -1
- package/src/cli/v2.ts +36 -9
- package/src/clients/config-export.ts +687 -10
- package/src/codex/account-lifecycle.ts +30 -5
- package/src/codex/account-usability.ts +22 -2
- package/src/codex/admission.ts +255 -0
- package/src/codex/auth-api.ts +427 -140
- package/src/codex/auth-context.ts +155 -30
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +62 -0
- package/src/codex/catalog/aggregation.ts +14 -1
- package/src/codex/catalog/bundled.ts +282 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/metadata.ts +51 -6
- package/src/codex/catalog/parsing.ts +6 -3
- package/src/codex/catalog/provider-fetch.ts +576 -41
- package/src/codex/catalog/sync.ts +505 -66
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-write-serialization.ts +241 -0
- package/src/codex/catalog.ts +2 -1
- package/src/codex/codex-write-lock.ts +372 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +441 -0
- package/src/codex/desired-state.ts +177 -0
- package/src/codex/features.ts +52 -8
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +257 -0
- package/src/codex/history-lock.ts +241 -0
- package/src/codex/history-migration-guardian.ts +18 -5
- package/src/codex/history-provider.ts +9 -2
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +176 -0
- package/src/codex/inject-coordination.ts +245 -0
- package/src/codex/inject.ts +605 -124
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +10 -1
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +167 -0
- package/src/codex/native-main-lock-file.ts +158 -0
- package/src/codex/native-main-owner.ts +315 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1512 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +340 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +557 -0
- package/src/codex/project-config-warnings.ts +18 -4
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +86 -3
- package/src/codex/routing.ts +299 -62
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +39 -13
- package/src/codex/subagent-model-fallback.ts +73 -12
- package/src/codex/transition-state.ts +604 -0
- package/src/codex/upstream-host-health.ts +70 -0
- package/src/codex/user-identity.ts +266 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/config.ts +562 -26
- package/src/generated/jawcode-model-metadata.ts +2 -2
- package/src/grok/inject.ts +15 -4
- package/src/grok/inspect.ts +45 -0
- package/src/images/loop.ts +113 -20
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +165 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +101 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/bounded-body.ts +46 -8
- package/src/lib/bun-runtime.ts +110 -1
- package/src/lib/bun-stream-caps.ts +2 -1
- package/src/lib/redact.ts +407 -2
- package/src/lib/shadow-call.ts +24 -0
- package/src/lib/translator-budget.ts +10 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +154 -2
- package/src/lib/windows-secret-acl.ts +212 -11
- package/src/lib/winsw.ts +9 -3
- package/src/oauth/index.ts +61 -3
- package/src/oauth/key-providers.ts +4 -0
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/store.ts +31 -6
- package/src/oauth/token-guardian.ts +9 -3
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/derive.ts +33 -1
- package/src/providers/free-directory.ts +3 -1
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/openai-sidecar.ts +64 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota.ts +334 -26
- package/src/providers/registry.ts +284 -16
- package/src/providers/xai-transport.ts +11 -4
- package/src/responses/compaction.ts +8 -1
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +44 -2
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +105 -0
- package/src/responses/spill-store.ts +45 -8
- package/src/responses/state.ts +161 -17
- package/src/router.ts +302 -16
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +204 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +590 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile.ts +423 -0
- package/src/routing/quota.ts +124 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/auth-cors.ts +46 -6
- package/src/server/chat-completions.ts +28 -13
- package/src/server/claude-messages.ts +23 -15
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +10 -1
- package/src/server/index.ts +238 -52
- package/src/server/lifecycle.ts +228 -9
- package/src/server/live.ts +14 -3
- package/src/server/management/agent-settings-routes.ts +64 -14
- package/src/server/management/combo-routes.ts +5 -5
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +42 -2
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +1 -1
- package/src/server/management/model-routes.ts +32 -113
- package/src/server/management/model-rows.ts +117 -0
- package/src/server/management/native-integration-routes.ts +587 -0
- package/src/server/management/oauth-account-routes.ts +1 -1
- package/src/server/management/provider-routes.ts +218 -117
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +177 -0
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +276 -24
- package/src/server/management/system-routes.ts +4 -0
- package/src/server/management-api.ts +84 -9
- package/src/server/management-auth.ts +43 -5
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +120 -6
- package/src/server/request-log.ts +26 -6
- package/src/server/responses/collaboration.ts +63 -8
- package/src/server/responses/compact.ts +272 -41
- package/src/server/responses/core.ts +730 -132
- package/src/server/responses/fetch-helpers.ts +15 -1
- package/src/server/responses-item-id-repair.ts +32 -3
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/search.ts +51 -6
- package/src/server/sse-payload-rewrite.ts +89 -12
- package/src/server/startup-health-cache.ts +7 -1
- package/src/server/ws-bridge.ts +11 -17
- package/src/service-manager-probe.ts +297 -0
- package/src/service.ts +222 -32
- package/src/tray/windows-tray.ps1 +9 -0
- package/src/tray/windows.ts +15 -7
- package/src/types.ts +194 -14
- package/src/update/index.ts +13 -13
- package/src/update/job.ts +24 -21
- package/src/update/notify.ts +7 -3
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +129 -10
- package/src/usage/log.ts +50 -15
- package/src/usage/summary.ts +4 -4
- package/src/vision/index.ts +6 -1
- package/src/web-search/loop.ts +161 -34
- package/gui/dist/assets/index-OY43ubAq.css +0 -1
- package/gui/dist/assets/index-YwNnKZcL.js +0 -67
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
package/src/web-search/loop.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "../adapters/base";
|
|
2
|
-
import type { AdapterEvent, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxThinkingContent, OcxUsage } from "../types";
|
|
2
|
+
import type { AdapterEvent, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxThinkingContent, OcxUsage, RateLimitRetryPolicy } from "../types";
|
|
3
3
|
import { namespacedToolName } from "../types";
|
|
4
|
+
import type { AttemptRecoveryKind } from "../usage/log";
|
|
4
5
|
import { bridgeToResponsesSSE } from "../bridge";
|
|
5
6
|
import { runWebSearch, type SidecarOutcome, type SidecarOutcomeRecorder, type SidecarSettings } from "./executor";
|
|
6
7
|
import { runAnthropicWebSearch } from "./anthropic-executor";
|
|
7
8
|
import { clearableDeadline } from "../lib/abort";
|
|
8
9
|
import { redactSecretString } from "../lib/redact";
|
|
9
10
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
10
|
-
import { fetchWithResetRetry } from "../lib/upstream-retry";
|
|
11
|
+
import { fetchWithResetRetry, prepareSameTarget429Wait } from "../lib/upstream-retry";
|
|
12
|
+
import { rateLimitRetryDelayMs } from "../providers/key-failover";
|
|
11
13
|
import {
|
|
12
14
|
isTranslatorBudgetExceededError,
|
|
13
15
|
TRANSLATOR_MAX_TURN_BYTES,
|
|
@@ -61,40 +63,68 @@ export function scanEventsForWebSearch(events: AdapterEvent[]): {
|
|
|
61
63
|
calls: WebSearchCall[];
|
|
62
64
|
passthrough: AdapterEvent[];
|
|
63
65
|
hasRealToolCall: boolean;
|
|
66
|
+
hasMalformedToolCall: boolean;
|
|
64
67
|
} {
|
|
65
68
|
const calls: WebSearchCall[] = [];
|
|
66
69
|
const passthrough: AdapterEvent[] = [];
|
|
67
70
|
let hasRealToolCall = false;
|
|
68
|
-
let
|
|
71
|
+
let hasMalformedToolCall = false;
|
|
72
|
+
let pending: { name: string; id: string; argsBuf: string; closed: boolean; events: AdapterEvent[] } | null = null;
|
|
73
|
+
const isBlank = (value: string): boolean => value.trim().length === 0;
|
|
69
74
|
const flushPending = (): void => {
|
|
75
|
+
// A pending call that never saw tool_call_end is structurally malformed.
|
|
76
|
+
if (pending && !pending.closed) hasMalformedToolCall = true;
|
|
70
77
|
if (pending && pending.name !== WEB_SEARCH_TOOL_NAME) {
|
|
71
78
|
passthrough.push(...pending.events);
|
|
72
|
-
hasRealToolCall = true;
|
|
79
|
+
if (pending.closed && !isBlank(pending.id) && !isBlank(pending.name)) hasRealToolCall = true;
|
|
73
80
|
}
|
|
74
81
|
pending = null;
|
|
75
82
|
};
|
|
76
83
|
for (const e of events) {
|
|
77
84
|
if (e.type === "tool_call_start") {
|
|
78
85
|
flushPending();
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
if (isBlank(e.id) || isBlank(e.name)) hasMalformedToolCall = true;
|
|
87
|
+
pending = { name: e.name, id: e.id, argsBuf: "", closed: false, events: [e] };
|
|
88
|
+
} else if (e.type === "tool_call_delta") {
|
|
89
|
+
// Orphan delta (no open call) is malformed.
|
|
90
|
+
if (!pending) hasMalformedToolCall = true;
|
|
91
|
+
else {
|
|
92
|
+
pending.argsBuf += e.arguments;
|
|
93
|
+
pending.events.push(e);
|
|
94
|
+
}
|
|
95
|
+
} else if (e.type === "tool_call_end") {
|
|
96
|
+
// Orphan end (no open call) is malformed.
|
|
97
|
+
if (!pending) {
|
|
98
|
+
hasMalformedToolCall = true;
|
|
87
99
|
} else {
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
pending.events.push(e);
|
|
101
|
+
pending.closed = true;
|
|
102
|
+
if (pending.name === WEB_SEARCH_TOOL_NAME) {
|
|
103
|
+
calls.push({ id: pending.id, queries: parseQueries(pending.argsBuf) });
|
|
104
|
+
} else {
|
|
105
|
+
passthrough.push(...pending.events);
|
|
106
|
+
if (!isBlank(pending.id) && !isBlank(pending.name)) hasRealToolCall = true;
|
|
107
|
+
}
|
|
108
|
+
pending = null;
|
|
90
109
|
}
|
|
91
|
-
pending = null;
|
|
92
110
|
} else {
|
|
93
111
|
passthrough.push(e);
|
|
94
112
|
}
|
|
95
113
|
}
|
|
96
114
|
flushPending();
|
|
97
|
-
return { calls, passthrough, hasRealToolCall };
|
|
115
|
+
return { calls, passthrough, hasRealToolCall, hasMalformedToolCall };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Visible final-answer text: a non-whitespace text_delta that is NOT commentary
|
|
120
|
+
* (#1001). Commentary streams early for progress and must not satisfy the
|
|
121
|
+
* forced-answer output check; thinking alone never counts either.
|
|
122
|
+
*/
|
|
123
|
+
export function hasVisibleAssistantText(events: AdapterEvent[]): boolean {
|
|
124
|
+
return events.some(event =>
|
|
125
|
+
event.type === "text_delta"
|
|
126
|
+
&& event.phase !== "commentary"
|
|
127
|
+
&& event.text.trim().length > 0);
|
|
98
128
|
}
|
|
99
129
|
|
|
100
130
|
async function* replay(events: AdapterEvent[]): AsyncGenerator<AdapterEvent> {
|
|
@@ -206,6 +236,10 @@ class LoopError extends Error {
|
|
|
206
236
|
}
|
|
207
237
|
}
|
|
208
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Dependencies for one web-search loop iteration: parsed request, active adapter,
|
|
241
|
+
* incoming metadata, and the configured search executor.
|
|
242
|
+
*/
|
|
209
243
|
export interface WebSearchLoopDeps {
|
|
210
244
|
parsed: OcxParsedRequest;
|
|
211
245
|
adapter: ProviderAdapter;
|
|
@@ -239,11 +273,15 @@ export interface WebSearchLoopDeps {
|
|
|
239
273
|
onUsage?: (usage: OcxUsage | undefined) => void;
|
|
240
274
|
/** Observe the exact adapter request selected for each routed-model iteration. */
|
|
241
275
|
onRequestBuilt?: (request: AdapterRequest) => void;
|
|
276
|
+
/** Called before each routed-model dispatch in the loop, for attempt telemetry. Same-target 429 replays pass the `rate-limit-429` recovery kind. */
|
|
277
|
+
onAttemptSend?: (recovery?: AttemptRecoveryKind) => void;
|
|
242
278
|
/**
|
|
243
279
|
* 429 key-failover hook: rotate the provider's active pool key and return a rebuilt adapter,
|
|
244
280
|
* or null when the pool is exhausted (same semantics as the normal routed path).
|
|
245
281
|
*/
|
|
246
282
|
on429?: (retryAfterHeader: string | null) => ProviderAdapter | null;
|
|
283
|
+
/** Opt-in same-target 429 policy (key-auth providers). When present, 429 replays on the SAME key before on429 rotation. */
|
|
284
|
+
retryOn429Policy?: Required<RateLimitRetryPolicy> | null;
|
|
247
285
|
}
|
|
248
286
|
|
|
249
287
|
/**
|
|
@@ -260,6 +298,12 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
260
298
|
// Mutable: 429 key-failover (deps.on429) can swap in a rebuilt adapter mid-loop.
|
|
261
299
|
let adapter = deps.adapter;
|
|
262
300
|
|
|
301
|
+
// Bridge stall budget (seconds of silence before upstream_stall_timeout); the retry backoff
|
|
302
|
+
// heartbeat interval is derived from it so the watchdog is always fed during deliberate waits.
|
|
303
|
+
const stallTimeoutMs = typeof deps.stallTimeoutSec === "number" && Number.isFinite(deps.stallTimeoutSec) && deps.stallTimeoutSec > 0
|
|
304
|
+
? Math.floor(deps.stallTimeoutSec * 1000)
|
|
305
|
+
: 300_000;
|
|
306
|
+
|
|
263
307
|
const messages: OcxMessage[] = [...parsed.context.messages];
|
|
264
308
|
const loopT0 = Date.now();
|
|
265
309
|
const allTools = parsed.context.tools ?? [];
|
|
@@ -294,9 +338,20 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
294
338
|
}
|
|
295
339
|
type IterationSplit = ReturnType<typeof scanEventsForWebSearch>;
|
|
296
340
|
|
|
341
|
+
// Same-target 429 budget is per REQUEST, not per model iteration: later search rounds inherit
|
|
342
|
+
// what earlier rounds left of `attempts`, so a bounded multi-round turn can never exceed the
|
|
343
|
+
// configured replay count in total (a per-round reset would multiply it by maxSearches).
|
|
344
|
+
const rateLimitRetryPolicy = deps.retryOn429Policy ?? null;
|
|
345
|
+
let rateLimitRetries = 0;
|
|
346
|
+
|
|
297
347
|
// Acquire one iteration's final response headers. The first call is drained eagerly so an initial
|
|
298
348
|
// connect/header/HTTP failure stays a non-2xx JSON response. Its successful BODY is deliberately
|
|
299
349
|
// left unread until the downstream Responses SSE bridge exists.
|
|
350
|
+
/**
|
|
351
|
+
* Fetch one web-search iteration's final response headers, applying the response-header
|
|
352
|
+
* deadline and the same-target 429 retry policy (with awaited body release and deadline
|
|
353
|
+
* restart) before the `on429` key rotation.
|
|
354
|
+
*/
|
|
300
355
|
const prepareIterationEvents = async function* (forceAnswer: boolean): AsyncGenerator<AdapterEvent, IterationResponse> {
|
|
301
356
|
// On the forced-answer pass the synthetic web_search tool is gone, so the model MUST answer
|
|
302
357
|
// from the results already in `messages`. A weak model can still produce a thin answer that
|
|
@@ -313,30 +368,55 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
313
368
|
// One cumulative header deadline spans every pool-key 429 rotation in this model iteration.
|
|
314
369
|
// clear() stops only its timer after final headers; the direct turn signal remains attached to
|
|
315
370
|
// the returned response body through AbortSignal.any().
|
|
316
|
-
|
|
371
|
+
let headerDeadline = clearableDeadline(connectTimeoutMs, signal);
|
|
317
372
|
try {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
373
|
+
/**
|
|
374
|
+
* Build and fetch one web-search iteration on the given adapter, under the iteration
|
|
375
|
+
* header deadline. The caller owns same-target 429 replays and key rotation around it.
|
|
376
|
+
* The outbound request is cached per adapter so a same-target replay reuses the EXACT
|
|
377
|
+
* URL, serialized body, and headers (builder runs once per target sequence).
|
|
378
|
+
*/
|
|
379
|
+
let cachedRequest: AdapterRequest | undefined;
|
|
380
|
+
let cachedAdapter: ProviderAdapter | undefined;
|
|
381
|
+
/**
|
|
382
|
+
* Build and fetch one web-search iteration on the given adapter, under the iteration
|
|
383
|
+
* header deadline. The caller owns same-target 429 replays and key rotation around it.
|
|
384
|
+
*/
|
|
385
|
+
const fetchOnce = async (requestAdapter: ProviderAdapter, recovery?: AttemptRecoveryKind): Promise<IterationResponse> => {
|
|
386
|
+
let request: AdapterRequest;
|
|
387
|
+
if (cachedRequest !== undefined && cachedAdapter === requestAdapter) {
|
|
388
|
+
request = cachedRequest;
|
|
389
|
+
} else {
|
|
390
|
+
request = await requestAdapter.buildRequest(iterParsed, {
|
|
391
|
+
headers: selectedForwardHeaders,
|
|
392
|
+
abortSignal: headerDeadline.signal,
|
|
393
|
+
translatorBudget,
|
|
394
|
+
});
|
|
395
|
+
try {
|
|
396
|
+
deps.onRequestBuilt?.(request);
|
|
397
|
+
} catch {
|
|
398
|
+
// Diagnostics are best-effort and must never abort a web-search iteration.
|
|
399
|
+
}
|
|
400
|
+
cachedRequest = request;
|
|
401
|
+
cachedAdapter = requestAdapter;
|
|
328
402
|
}
|
|
329
403
|
let response: Response;
|
|
330
404
|
try {
|
|
331
|
-
|
|
332
|
-
|
|
405
|
+
if (requestAdapter.fetchResponse) {
|
|
406
|
+
deps.onAttemptSend?.(recovery);
|
|
407
|
+
response = await requestAdapter.fetchResponse(request, {
|
|
333
408
|
abortSignal: headerDeadline.signal,
|
|
334
409
|
timeoutMs: connectTimeoutMs,
|
|
335
410
|
returnRawErrors: true,
|
|
336
411
|
stream: true,
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
|
|
412
|
+
});
|
|
413
|
+
} else {
|
|
414
|
+
response = await fetchWithResetRetry(
|
|
415
|
+
(retryRecovery) => {
|
|
416
|
+
// Record every helper-driven send (the callback runs for the first attempt and
|
|
417
|
+
// each connection-reset replay); preserve the caller's recovery kind
|
|
418
|
+
// (rate-limit-429 / key-429) when the retry layer supplies none.
|
|
419
|
+
deps.onAttemptSend?.(retryRecovery ?? recovery);
|
|
340
420
|
const h = new Headers(request.headers);
|
|
341
421
|
if (!h.has("accept-encoding")) h.set("accept-encoding", "identity");
|
|
342
422
|
return fetch(request.url, {
|
|
@@ -347,7 +427,8 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
347
427
|
});
|
|
348
428
|
},
|
|
349
429
|
{ abortSignal: headerDeadline.signal, label: "web-search-loop" },
|
|
350
|
-
|
|
430
|
+
);
|
|
431
|
+
}
|
|
351
432
|
} finally {
|
|
352
433
|
request.releaseBodyObservation?.();
|
|
353
434
|
}
|
|
@@ -355,6 +436,39 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
355
436
|
};
|
|
356
437
|
|
|
357
438
|
let prepared = await fetchOnce(adapter);
|
|
439
|
+
// Same-target 429 wait-and-retry (opt-in `retryOn429`) BEFORE key rotation: a primary-key
|
|
440
|
+
// rate-limit blip replays on the SAME key; rotation only runs after attempts exhaust.
|
|
441
|
+
while (
|
|
442
|
+
prepared.response.status === 429
|
|
443
|
+
&& rateLimitRetryPolicy !== null
|
|
444
|
+
&& rateLimitRetries < rateLimitRetryPolicy.attempts
|
|
445
|
+
) {
|
|
446
|
+
rateLimitRetries += 1;
|
|
447
|
+
// Release unread body + heartbeat-fed wait via the shared same-target helper.
|
|
448
|
+
const retryAfterHeader = prepared.response.headers.get("retry-after");
|
|
449
|
+
// The old header deadline must not stay armed across the deliberate wait: clear it
|
|
450
|
+
// before sleeping so a stale expiry can never race the client-cancel path.
|
|
451
|
+
headerDeadline.clear();
|
|
452
|
+
try {
|
|
453
|
+
yield* prepareSameTarget429Wait({
|
|
454
|
+
body: prepared.response.body,
|
|
455
|
+
signal,
|
|
456
|
+
delayMs: rateLimitRetryDelayMs(rateLimitRetryPolicy, retryAfterHeader, Date.now()),
|
|
457
|
+
heartbeatIntervalMs: Math.min(10_000, Math.max(250, stallTimeoutMs / 2)),
|
|
458
|
+
});
|
|
459
|
+
} catch {
|
|
460
|
+
throw new LoopError(499, "client closed request during web-search");
|
|
461
|
+
}
|
|
462
|
+
// Client cancellation wins over any stale-deadline edge: re-check before telemetry/replay.
|
|
463
|
+
if (signal.aborted) throw new LoopError(499, "client closed request during web-search");
|
|
464
|
+
// The deliberate backoff must not consume the cumulative response-header deadline:
|
|
465
|
+
// start a fresh one so the replay gets a new connect budget (504 stays reserved for real
|
|
466
|
+
// upstream latency).
|
|
467
|
+
headerDeadline = clearableDeadline(connectTimeoutMs, signal);
|
|
468
|
+
// Stall-watchdog seam between bounded retry fetches.
|
|
469
|
+
yield { type: "heartbeat" };
|
|
470
|
+
prepared = await fetchOnce(adapter, "rate-limit-429");
|
|
471
|
+
}
|
|
358
472
|
// 429 key-failover parity with the normal routed path: rotate pool keys until one responds
|
|
359
473
|
// or the pool is exhausted (deps.on429 returns null — cooldown map guarantees termination).
|
|
360
474
|
while (prepared.response.status === 429 && deps.on429) {
|
|
@@ -366,7 +480,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
366
480
|
adapter = rotated;
|
|
367
481
|
// Stall-watchdog seam between bounded retry fetches (audit 011 B3).
|
|
368
482
|
yield { type: "heartbeat" };
|
|
369
|
-
prepared = await fetchOnce(adapter);
|
|
483
|
+
prepared = await fetchOnce(adapter, "key-429");
|
|
370
484
|
}
|
|
371
485
|
|
|
372
486
|
// Final headers have arrived. Clear only the deadline timer before ANY body read.
|
|
@@ -607,6 +721,18 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
607
721
|
// calls reach Codex. forceAnswer also finalizes.
|
|
608
722
|
const shouldLoop = split.calls.length > 0 && !split.hasRealToolCall && !forceAnswer;
|
|
609
723
|
if (!shouldLoop) {
|
|
724
|
+
// #1001: a forced-answer pass that ends `done` must have produced
|
|
725
|
+
// usable output — never a malformed tool call, and never silence.
|
|
726
|
+
if (forceAnswer) {
|
|
727
|
+
// An unterminated call flushes AFTER the terminal event, so find
|
|
728
|
+
// the terminal rather than assuming it is last (#1001).
|
|
729
|
+
const terminalEvent = split.passthrough.find(event => event.type === "done");
|
|
730
|
+
if (terminalEvent?.type === "done"
|
|
731
|
+
&& (split.hasMalformedToolCall
|
|
732
|
+
|| (!split.hasRealToolCall && !hasVisibleAssistantText(split.passthrough)))) {
|
|
733
|
+
throw new LoopError(502, "forced-answer pass produced no usable assistant output");
|
|
734
|
+
}
|
|
735
|
+
}
|
|
610
736
|
if (executedSearchCount > 0) {
|
|
611
737
|
const failedCount = failedQueries.size;
|
|
612
738
|
console.warn(
|
|
@@ -653,6 +779,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
653
779
|
}, undefined,
|
|
654
780
|
{
|
|
655
781
|
translatorBudget,
|
|
782
|
+
replayCacheScope: parsed._clientThreadId ?? "global",
|
|
656
783
|
...(deps.forceEmptyResponseId ? { responseId: "" } : {}),
|
|
657
784
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
658
785
|
...(deps.stallTimeoutSec !== undefined ? { stallTimeoutSec: deps.stallTimeoutSec } : {}),
|