@bitkyc08/opencodex 2.76.0 → 2.77.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-COdEeqSj.css +1 -0
- package/gui/dist/assets/App-DEpCJEP_.js +54 -0
- package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-gloIYNaW.js} +1 -1
- package/gui/dist/assets/index-Cd7ittMv.css +1 -0
- package/gui/dist/assets/index-DwJKFJm5.js +94 -0
- package/gui/dist/assets/quota-summary-DGcEJU3h.js +1 -0
- package/gui/dist/index.html +4 -3
- package/package.json +1 -1
- package/src/adapters/antigravity-refusal.ts +28 -0
- package/src/adapters/cursor/protobuf-request.ts +6 -0
- package/src/adapters/cursor/request-builder.ts +3 -0
- package/src/adapters/cursor/structured-output.ts +13 -0
- package/src/adapters/cursor/types.ts +2 -0
- package/src/adapters/ollama-native.ts +29 -2
- package/src/adapters/openai-responses/passthrough.ts +133 -103
- package/src/adapters/openai-responses/request-strips.ts +10 -4
- package/src/adapters/zed.ts +26 -4
- package/src/bridge/response-json.ts +14 -14
- package/src/bridge/sse.ts +15 -5
- package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
- package/src/claude/agents-inject.ts +4 -43
- package/src/claude/gateway-cache.ts +16 -13
- package/src/claude/inbound-model-options.ts +9 -2
- package/src/claude/intercept/cli-catalog.ts +27 -16
- package/src/claude/intercept/picker-bootstrap.ts +37 -16
- package/src/claude/intercept/picker-budget.ts +41 -0
- package/src/claude/outbound.ts +2 -0
- package/src/claude/subagent-force-status.ts +53 -0
- package/src/claude/subagent-model.ts +93 -0
- package/src/cli/account-api.ts +3 -0
- package/src/cli/account.ts +3 -1
- package/src/cli/agent.ts +10 -0
- package/src/cli/capabilities.ts +9 -0
- package/src/cli/chatgpt-command.ts +10 -7
- package/src/cli/claude.ts +22 -6
- package/src/cli/codex-shim-readiness.ts +89 -0
- package/src/cli/connect.ts +19 -0
- package/src/cli/dispatch.ts +3 -3
- package/src/cli/export-command.ts +20 -9
- package/src/cli/gui.ts +8 -3
- package/src/cli/help-catalog.ts +84 -0
- package/src/cli/help-models-context.ts +17 -0
- package/src/cli/help-navigation.ts +50 -0
- package/src/cli/help-recovery.ts +66 -0
- package/src/cli/help.ts +52 -8
- package/src/cli/models-runtime.ts +2 -1
- package/src/cli/opencode.ts +54 -10
- package/src/cli/provider.ts +3 -29
- package/src/cli/registry.ts +29 -5
- package/src/cli/root.ts +30 -9
- package/src/cli/runtime-api.ts +1 -1
- package/src/cli/tray-proxy.ts +3 -2
- package/src/clients/config-export/contracts.ts +100 -0
- package/src/clients/config-export/droid.ts +54 -2
- package/src/clients/config-export/kilo.ts +12 -2
- package/src/clients/config-export/model-metadata.ts +10 -4
- package/src/clients/config-export/reasoning-metadata.ts +47 -0
- package/src/clients/config-export.ts +46 -66
- package/src/clients/omo-role-models.ts +17 -3
- package/src/codex/account-credit-use.ts +4 -2
- package/src/codex/account-runtime-state.ts +21 -0
- package/src/codex/account-store.ts +11 -47
- package/src/codex/account-usability.ts +11 -8
- package/src/codex/auth-api/account-list.ts +73 -63
- package/src/codex/auth-api/main-account-probe.ts +25 -12
- package/src/codex/auth-api/pool-mode-gate.ts +6 -9
- package/src/codex/auth-api/pool-quota-probe.ts +2 -4
- package/src/codex/auth-api/reset-credit-service.ts +3 -2
- package/src/codex/auth-context.ts +30 -11
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/model-hints.ts +2 -1
- package/src/codex/catalog/model-visibility.ts +6 -3
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +13 -4
- package/src/codex/catalog/retained-sync.ts +2 -1
- package/src/codex/chatgpt-refresh-failure.ts +56 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/main-account.ts +144 -11
- package/src/codex/quota-refresh-outcome.ts +17 -2
- package/src/codex/quota-types.ts +33 -1
- package/src/codex/quota.ts +56 -15
- package/src/codex/routing/cooldown-math.ts +10 -4
- package/src/codex/routing/selection.ts +11 -4
- package/src/codex/routing.ts +9 -9
- package/src/codex/shim-path-resolution.ts +133 -0
- package/src/codex/shim.ts +39 -39
- package/src/codex/subagent-model-fallback.ts +2 -1
- package/src/combos/failover.ts +82 -1
- package/src/combos/jev.ts +13 -3
- package/src/config/diagnostics.ts +4 -0
- package/src/config/load-degrade.ts +4 -0
- package/src/config/provider-validation.ts +3 -0
- package/src/config/schema/compaction-triggers.ts +8 -0
- package/src/config/schema/config-schema.ts +11 -0
- package/src/config/schema/leaf-validators.ts +5 -1
- package/src/config/subagent-models.ts +12 -0
- package/src/generated/compatibility-version.json +245 -153
- package/src/grok/session-identity.ts +33 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +4 -2
- package/src/integrations/state.ts +68 -6
- package/src/integrations/writer.ts +18 -4
- package/src/lib/gui-pair-capability.ts +17 -0
- package/src/lib/provider-client-headers.ts +62 -0
- package/src/oauth/anthropic-continuity.ts +65 -0
- package/src/oauth/anthropic-identity.ts +75 -0
- package/src/oauth/anthropic-routing.ts +3 -3
- package/src/oauth/anthropic.ts +7 -1
- package/src/oauth/generic-account-failover.ts +21 -0
- package/src/oauth/health.ts +32 -5
- package/src/oauth/index.ts +36 -27
- package/src/oauth/local-token-detect.ts +7 -0
- package/src/oauth/refresh-policy.ts +2 -0
- package/src/oauth/store.ts +55 -6
- package/src/oauth/types.ts +9 -0
- package/src/protocols/encoders/messages.ts +4 -3
- package/src/providers/antigravity-effort-families.ts +75 -0
- package/src/providers/antigravity-models.ts +54 -16
- package/src/providers/antigravity-wire-snapshot.ts +66 -0
- package/src/providers/derive.ts +2 -0
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/new-model-policy.ts +48 -10
- package/src/providers/registry/entries-extended.ts +3 -0
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/model-seeds.ts +5 -0
- package/src/providers/registry/types.ts +4 -0
- package/src/responses/compaction.ts +19 -0
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +9 -0
- package/src/server/chat-completions.ts +10 -0
- package/src/server/claude-messages.ts +18 -11
- package/src/server/droid-reasoning-default.ts +50 -0
- package/src/server/github-copilot-responses-repair.ts +3 -6
- package/src/server/grok-responses-control-frame.ts +2 -2
- package/src/server/grok-responses-snapshot-repair.ts +4 -5
- package/src/server/gui-session.ts +33 -11
- package/src/server/index/claude-intercept-lifecycle.ts +4 -0
- package/src/server/index/serve-options.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +6 -129
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/integration-routes.ts +64 -6
- package/src/server/management/link-routes.ts +20 -13
- package/src/server/management/model-row-export-metadata.ts +150 -0
- package/src/server/management/model-rows.ts +148 -9
- package/src/server/management/route-registry.ts +4 -3
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/subagent-model-routes.ts +214 -0
- package/src/server/relay.ts +14 -44
- package/src/server/request-log-generation-window.ts +38 -0
- package/src/server/request-log.ts +13 -0
- package/src/server/responses/adapter-dispatch.ts +100 -1
- package/src/server/responses/codex-auth-error.ts +10 -2
- package/src/server/responses/combo-stream-preflight.ts +29 -3
- package/src/server/responses/compaction-routing.ts +10 -1
- package/src/server/responses/core-auth.ts +57 -26
- package/src/server/responses/core-codex-account.ts +6 -3
- package/src/server/responses/core-combo-failure.ts +5 -1
- package/src/server/responses/core-combo-native.ts +7 -1
- package/src/server/responses/core-combo.ts +18 -1
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/fetch-helpers.ts +9 -0
- package/src/server/responses/passthrough-delivery.ts +1 -1
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/request-prepare.ts +5 -0
- package/src/server/responses/request-spend.ts +30 -30
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/responses-hosted-image-display.ts +70 -9
- package/src/server/responses-terminal-repair.ts +15 -2
- package/src/server/sse-frame-buffer.ts +38 -36
- package/src/server/sse-payload-rewrite.ts +78 -49
- package/src/server/ws-bridge.ts +1 -11
- package/src/service/windows-taskxml.ts +1 -2
- package/src/service-manager-probe.ts +36 -2
- package/src/types/config.ts +4 -0
- package/src/types/provider.ts +18 -0
- package/src/update/npm-invocation.mjs +35 -3
- package/src/update/proxy-liveness-probe.mjs +38 -8
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +7 -0
- package/src/usage/summary.ts +82 -1
- package/src/web-search/loop.ts +11 -8
- package/src/web-search/run-turn-loop.ts +10 -7
- package/gui/dist/assets/App-CddorHdA.js +0 -51
- package/gui/dist/assets/App-DMudtISW.css +0 -1
- package/gui/dist/assets/index-Btze1I3T.css +0 -1
- package/gui/dist/assets/index-CvnY7E6i.js +0 -86
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
package/src/types/config.ts
CHANGED
|
@@ -219,6 +219,8 @@ export interface OcxClaudeCodeConfig {
|
|
|
219
219
|
* definition. Unset inherits the parent session effort.
|
|
220
220
|
*/
|
|
221
221
|
subagentEffort?: "low" | "medium" | "high" | "xhigh" | "max";
|
|
222
|
+
/** Optional roster-style model forced on subagents at the next routed Claude Code launch. */
|
|
223
|
+
subagentModelForce?: string;
|
|
222
224
|
/** Claude-originated web-search override. Unset fields inherit the global sidecar settings. */
|
|
223
225
|
webSearchSidecar?: { backend?: "openai" | "anthropic" | "xai" | "gemini" | "exa"; model?: string };
|
|
224
226
|
/** Claude-originated vision override. Unset fields inherit the global sidecar settings. */
|
|
@@ -750,6 +752,8 @@ export interface OcxConfig {
|
|
|
750
752
|
modelPinnedEfforts?: Record<string, string>;
|
|
751
753
|
compactionRouting?: {
|
|
752
754
|
model: string;
|
|
755
|
+
/** Incoming model allowlist: exact selectors or provider/*; omitted means all models. */
|
|
756
|
+
sourceModels?: string[];
|
|
753
757
|
reasoningEffort?: string;
|
|
754
758
|
/** Compaction triggers this override covers; omission means `["manual"]`. */
|
|
755
759
|
triggers?: ("manual" | "auto")[];
|
package/src/types/provider.ts
CHANGED
|
@@ -411,6 +411,22 @@ export interface OcxProviderConfig {
|
|
|
411
411
|
* `ocxr1` envelopes are still stripped because no upstream can decrypt them.
|
|
412
412
|
*/
|
|
413
413
|
preserveResponsesReasoningContent?: boolean;
|
|
414
|
+
/**
|
|
415
|
+
* Whether to preserve Codex-private `input[].id` on `store: false` requests.
|
|
416
|
+
* Disabled by default: ordinary Responses upstreams interpret input IDs as references
|
|
417
|
+
* to nonexistent stored items and return 404. Upstream launcher relays such as
|
|
418
|
+
* Codex Web GPT (`chatgpt-web/*`) require the current-turn user message ID for
|
|
419
|
+
* browser-session replay (#6220).
|
|
420
|
+
*/
|
|
421
|
+
preserveResponsesInputItemIds?: boolean;
|
|
422
|
+
/**
|
|
423
|
+
* Whether to preserve ChatGPT-internal `internal_chat_message_metadata_passthrough`
|
|
424
|
+
* on outgoing input messages for noncanonical destinations. Disabled by default:
|
|
425
|
+
* public Responses gateways reject the private field as an unknown parameter.
|
|
426
|
+
* Upstream launcher relays such as Codex Web GPT require this metadata (or the item ID)
|
|
427
|
+
* to extract turn provenance (#6220).
|
|
428
|
+
*/
|
|
429
|
+
preserveResponsesMessageMetadata?: boolean;
|
|
414
430
|
/**
|
|
415
431
|
* Treat this provider's `modelReasoningEfforts` as authoritative at the wire, not only in the
|
|
416
432
|
* catalog. Adapters that ship their own per-model effort table (currently `command-code`)
|
|
@@ -652,6 +668,8 @@ export interface OcxProviderConfig {
|
|
|
652
668
|
*/
|
|
653
669
|
autoReviewModelOverrides?: Record<string, string>;
|
|
654
670
|
headers?: Record<string, string>;
|
|
671
|
+
/** Inbound client metadata headers to copy to this provider when the outbound field is otherwise unset. */
|
|
672
|
+
forwardClientHeaders?: string[];
|
|
655
673
|
/** Default provider-routing preferences for models sent through the canonical OpenRouter API. */
|
|
656
674
|
openRouterRouting?: OpenRouterProviderRouting;
|
|
657
675
|
/** Exact model-id overrides for `openRouterRouting`. Each matching entry replaces the default. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
2
2
|
import { win32 } from "node:path";
|
|
3
3
|
|
|
4
4
|
const CMD_META = /([()%!^"`<>&|;, *?])/g;
|
|
@@ -38,11 +38,41 @@ export function resolveNpmCommand(
|
|
|
38
38
|
) {
|
|
39
39
|
if (platform !== "win32") return "npm";
|
|
40
40
|
const exists = deps.exists ?? existsSync;
|
|
41
|
+
const realpath = deps.realpath ?? realpathSync.native;
|
|
41
42
|
const cwd = deps.cwd ?? process.cwd();
|
|
42
43
|
const trustedRoots = [env.APPDATA, env.LOCALAPPDATA, env.ProgramFiles, env["ProgramFiles(x86)"],
|
|
43
44
|
env.USERPROFILE && win32.join(env.USERPROFILE, "scoop", "shims")]
|
|
44
45
|
.filter(root => typeof root === "string" && win32.isAbsolute(root));
|
|
45
46
|
const trustedEntry = entry => trustedRoots.some(root => isInside(root, entry) && !isInside(root, cwd));
|
|
47
|
+
// Scoop installs Node's npm in the app tree, not in scoop/shims. Admit only
|
|
48
|
+
// the two Node apps' current npm directories, never arbitrary Scoop apps or
|
|
49
|
+
// a launch directory inside the installation itself.
|
|
50
|
+
const scoopNodeRoots = typeof env.USERPROFILE === "string" && win32.isAbsolute(env.USERPROFILE)
|
|
51
|
+
? ["nodejs", "nodejs-lts"].map(app => win32.join(env.USERPROFILE, "scoop", "apps", app))
|
|
52
|
+
: [];
|
|
53
|
+
const trustedScoopNodeEntry = (entry, candidate) => {
|
|
54
|
+
const app = scoopNodeRoots.find(root => {
|
|
55
|
+
const current = win32.join(root, "current");
|
|
56
|
+
return isSamePath(current, entry) || isSamePath(win32.join(current, "bin"), entry);
|
|
57
|
+
});
|
|
58
|
+
if (!app) return null;
|
|
59
|
+
try {
|
|
60
|
+
const appPath = realpath(app);
|
|
61
|
+
const currentPath = realpath(win32.join(app, "current"));
|
|
62
|
+
const entryPath = realpath(entry);
|
|
63
|
+
const candidatePath = realpath(candidate);
|
|
64
|
+
const cwdPath = realpath(cwd);
|
|
65
|
+
const persistBin = win32.join(realpath(env.USERPROFILE), "scoop", "persist", win32.basename(app), "bin");
|
|
66
|
+
const insideInstall = isInside(currentPath, entryPath);
|
|
67
|
+
const persistedBin = isSamePath(entry, win32.join(app, "current", "bin"))
|
|
68
|
+
&& isSamePath(entryPath, persistBin);
|
|
69
|
+
return !isSamePath(appPath, currentPath) && isInside(appPath, currentPath)
|
|
70
|
+
&& (insideInstall || persistedBin) && isInside(entryPath, candidatePath)
|
|
71
|
+
&& !isInside(currentPath, cwdPath) && !isInside(entryPath, cwdPath);
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
46
76
|
const extensions = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD")
|
|
47
77
|
.split(";")
|
|
48
78
|
.filter(Boolean);
|
|
@@ -54,10 +84,12 @@ export function resolveNpmCommand(
|
|
|
54
84
|
for (const entry of pathEntries) {
|
|
55
85
|
if (!win32.isAbsolute(entry)) continue;
|
|
56
86
|
if (isSamePath(entry, cwd)) continue;
|
|
57
|
-
if (isInside(cwd, entry) && !trustedEntry(entry)) continue;
|
|
58
87
|
for (const extension of extensions) {
|
|
59
88
|
const candidate = win32.join(entry, `npm${extension.toLowerCase()}`);
|
|
60
|
-
if (exists(candidate))
|
|
89
|
+
if (!exists(candidate)) continue;
|
|
90
|
+
const scoopTrust = trustedScoopNodeEntry(entry, candidate);
|
|
91
|
+
if (scoopTrust === false || (scoopTrust === null && isInside(cwd, entry) && !trustedEntry(entry))) continue;
|
|
92
|
+
return win32.resolve(candidate);
|
|
61
93
|
}
|
|
62
94
|
}
|
|
63
95
|
return null;
|
|
@@ -16,7 +16,16 @@ import { spawnSync } from "node:child_process";
|
|
|
16
16
|
* stop. Fail-open was wrong here: a listener that accepts connections but withholds
|
|
17
17
|
* `/healthz`, or a probe that times out, is exactly the state where replacing package
|
|
18
18
|
* files is most dangerous, and "we could not tell" is not evidence the proxy is gone.
|
|
19
|
-
*
|
|
19
|
+
* A refused connection or a definitive non-OpenCodex answer earns `"dead"`. So does one
|
|
20
|
+
* more observation when the dial is neither answered nor refused (a timeout, or a dropped
|
|
21
|
+
* SYN as with a listener bound to a tailnet address): the child tries one transient
|
|
22
|
+
* exclusive bind of the same host and port, and a successful bind means nothing holds the
|
|
23
|
+
* port at that instant. The fallback runs only for a literal IP address, because a name can
|
|
24
|
+
* resolve to different addresses for the dial and the bind. A failed bind (`EADDRINUSE`,
|
|
25
|
+
* `EADDRNOTAVAIL`, any error) stays
|
|
26
|
+
* `"unknown"`, a held-but-silent port included. The whole probe is bounded by the child's
|
|
27
|
+
* own `timeoutMs` plus a 1500 ms spawn ceiling, after which it is `"unknown"`. A successful
|
|
28
|
+
* bind is a statement about that moment, not a promise that the endpoint cannot restart.
|
|
20
29
|
*/
|
|
21
30
|
export function probeProxyLiveness(port, hostname = "127.0.0.1", timeoutMs = 1500) {
|
|
22
31
|
// An unusable port is not an ambiguous probe: there is nothing to ask.
|
|
@@ -38,6 +47,8 @@ export function probeProxyLiveness(port, hostname = "127.0.0.1", timeoutMs = 150
|
|
|
38
47
|
const script = [
|
|
39
48
|
"const http = require('node:http');",
|
|
40
49
|
"const [host, port, timeout] = process.argv.slice(1);",
|
|
50
|
+
"let settled = false;",
|
|
51
|
+
"const settle = word => { if (!settled) { settled = true; process.stdout.write(word); } };",
|
|
41
52
|
"const req = http.get({ host, port: Number(port), path: '/healthz', timeout: Number(timeout) }, res => {",
|
|
42
53
|
" let body = '';",
|
|
43
54
|
" res.setEncoding('utf8');",
|
|
@@ -56,15 +67,34 @@ export function probeProxyLiveness(port, hostname = "127.0.0.1", timeoutMs = 150
|
|
|
56
67
|
" && typeof parsed.uptime === 'number'));",
|
|
57
68
|
" // Only a clean 200 decides anything. Any other status means the endpoint is",
|
|
58
69
|
" // answering but not telling us what it is, which is not evidence of absence.",
|
|
59
|
-
" if (res.statusCode !== 200)
|
|
60
|
-
" else
|
|
61
|
-
" } catch {
|
|
70
|
+
" if (res.statusCode !== 200) settle('UNKNOWN');",
|
|
71
|
+
" else settle(isOpencodex ? 'LIVE' : 'DEAD');",
|
|
72
|
+
" } catch { settle('UNKNOWN'); }",
|
|
62
73
|
" });",
|
|
63
74
|
"});",
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
// A refused connection is the clean proof that nothing is listening. Everything else -
|
|
76
|
+
// reset, unreachable host, a silent drop - leaves the question open, with one more way
|
|
77
|
+
// to close it: a port nobody listens on can be bound, a held one cannot (EADDRINUSE).
|
|
78
|
+
// A proxy bound to a Tailscale address is the case that needs this: the stack drops
|
|
79
|
+
// the SYN instead of refusing it, so the dial only ever times out.
|
|
80
|
+
"let bindStarted = false;",
|
|
81
|
+
"const bindCheck = () => {",
|
|
82
|
+
" if (bindStarted || settled) return;",
|
|
83
|
+
" bindStarted = true;",
|
|
84
|
+
// A name can resolve to several addresses, and the dial and the bind resolve it on
|
|
85
|
+
// their own: a live proxy on one address would not stop a bind on another. Only a
|
|
86
|
+
// literal address names the same endpoint on both sides.
|
|
87
|
+
" if (require('node:net').isIP(host) === 0) { settle('UNKNOWN'); return; }",
|
|
88
|
+
" const server = require('node:net').createServer();",
|
|
89
|
+
" server.on('connection', socket => socket.destroy());",
|
|
90
|
+
" server.once('error', () => settle('UNKNOWN'));",
|
|
91
|
+
" server.listen({ host, port: Number(port), exclusive: true }, () => server.close(() => settle('DEAD')));",
|
|
92
|
+
"};",
|
|
93
|
+
"req.on('timeout', () => { req.destroy(); bindCheck(); });",
|
|
94
|
+
"req.on('error', err => {",
|
|
95
|
+
" if (err && err.code === 'ECONNREFUSED') settle('DEAD');",
|
|
96
|
+
" else bindCheck();",
|
|
97
|
+
"});",
|
|
68
98
|
].join("\n");
|
|
69
99
|
try {
|
|
70
100
|
const probe = spawnSync(
|
|
@@ -370,6 +370,15 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
|
|
|
370
370
|
{ provider: "google-antigravity", modelId: "claude-opus-4-6-thinking", cost4: CLAUDE_OPUS_46, source: `anthropic official Claude Opus 4.6 ${ANTHROPIC_PRICING}`, verifiedAt: "2026-07-23", status: "verified" },
|
|
371
371
|
// Alias without the Antigravity "-thinking" suffix (if logs/UI ever surface it).
|
|
372
372
|
{ provider: "google-antigravity", modelId: "claude-opus-4-6", cost4: CLAUDE_OPUS_46, source: `anthropic official Claude Opus 4.6 ${ANTHROPIC_PRICING}`, verifiedAt: "2026-07-23", status: "verified" },
|
|
373
|
+
// Antigravity uses subscription quota; these are derived Anthropic reference estimates.
|
|
374
|
+
{ provider: "google-antigravity", modelId: "claude-sonnet-5-5", cost4: CLAUDE_SONNET_55, source: `derived: Anthropic Claude Sonnet 5.5 reference price; ${ANTHROPIC_PRICING}`, verifiedAt: "2026-10-03", status: "verified-derived" },
|
|
375
|
+
{ provider: "google-antigravity", modelId: "claude-sonnet-5-5-low", cost4: CLAUDE_SONNET_55, source: `derived: Anthropic Claude Sonnet 5.5 reference price; ${ANTHROPIC_PRICING}`, verifiedAt: "2026-10-03", status: "verified-derived" },
|
|
376
|
+
{ provider: "google-antigravity", modelId: "claude-sonnet-5-5-medium", cost4: CLAUDE_SONNET_55, source: `derived: Anthropic Claude Sonnet 5.5 reference price; ${ANTHROPIC_PRICING}`, verifiedAt: "2026-10-03", status: "verified-derived" },
|
|
377
|
+
{ provider: "google-antigravity", modelId: "claude-sonnet-5-5-high", cost4: CLAUDE_SONNET_55, source: `derived: Anthropic Claude Sonnet 5.5 reference price; ${ANTHROPIC_PRICING}`, verifiedAt: "2026-10-03", status: "verified-derived" },
|
|
378
|
+
{ provider: "google-antigravity", modelId: "claude-opus-5-5", cost4: CLAUDE_OPUS_55, source: `derived: Anthropic Claude Opus 5.5 reference price; ${ANTHROPIC_PRICING}`, verifiedAt: "2026-10-03", status: "verified-derived" },
|
|
379
|
+
{ provider: "google-antigravity", modelId: "claude-opus-5-5-low", cost4: CLAUDE_OPUS_55, source: `derived: Anthropic Claude Opus 5.5 reference price; ${ANTHROPIC_PRICING}`, verifiedAt: "2026-10-03", status: "verified-derived" },
|
|
380
|
+
{ provider: "google-antigravity", modelId: "claude-opus-5-5-medium", cost4: CLAUDE_OPUS_55, source: `derived: Anthropic Claude Opus 5.5 reference price; ${ANTHROPIC_PRICING}`, verifiedAt: "2026-10-03", status: "verified-derived" },
|
|
381
|
+
{ provider: "google-antigravity", modelId: "claude-opus-5-5-high", cost4: CLAUDE_OPUS_55, source: `derived: Anthropic Claude Opus 5.5 reference price; ${ANTHROPIC_PRICING}`, verifiedAt: "2026-10-03", status: "verified-derived" },
|
|
373
382
|
{ provider: "google-antigravity", modelId: "gpt-oss-120b-medium", cost4: { input: 0.03, output: 0.15, cacheRead: 0, cacheWrite: 0 }, source: "derived: gpt-oss-120b open-weights — OpenRouter advertised lowest https://openrouter.ai/openai/gpt-oss-120b/providers", verifiedAt: "2026-07-20", status: "verified-derived" },
|
|
374
383
|
// Kimi / Moonshot — official price tables are now published (2026-07-20 re-check;
|
|
375
384
|
// previously empty). kimi = Kimi Code OAuth surface, moonshot = CN key surface,
|
package/src/usage/log.ts
CHANGED
|
@@ -347,6 +347,9 @@ export interface PersistedUsageEntry {
|
|
|
347
347
|
durationMs: number;
|
|
348
348
|
/** TTFT relative to the request start (WP4); unset for non-streaming/tool-only. */
|
|
349
349
|
firstOutputMs?: number;
|
|
350
|
+
/** Request-relative generation window (#6309): first output item, last output delta. Written as a pair. */
|
|
351
|
+
genStartMs?: number;
|
|
352
|
+
lastOutputMs?: number;
|
|
350
353
|
usageStatus: UsageStatus;
|
|
351
354
|
usage?: OcxUsage;
|
|
352
355
|
totalTokens?: number;
|
|
@@ -1035,6 +1038,10 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
|
|
|
1035
1038
|
...(isNonNegativeFiniteNumber(entry.firstOutputMs)
|
|
1036
1039
|
? { firstOutputMs: entry.firstOutputMs }
|
|
1037
1040
|
: {}),
|
|
1041
|
+
...(isNonNegativeFiniteNumber(entry.genStartMs) && isNonNegativeFiniteNumber(entry.lastOutputMs)
|
|
1042
|
+
&& entry.lastOutputMs >= entry.genStartMs
|
|
1043
|
+
? { genStartMs: entry.genStartMs, lastOutputMs: entry.lastOutputMs }
|
|
1044
|
+
: {}),
|
|
1038
1045
|
usageStatus: entry.usageStatus,
|
|
1039
1046
|
...(entry.usage ? { usage: normalizeUsageValue(entry.usage) } : {}),
|
|
1040
1047
|
...(typeof entry.totalTokens === "number" ? { totalTokens: entry.totalTokens } : {}),
|
package/src/usage/summary.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
type PersistedUsageEntry,
|
|
11
11
|
type UsageStatus,
|
|
12
12
|
} from "./log";
|
|
13
|
-
import { type AttemptCostEstimate, type CostEstimate, estimateAttemptCost, estimateRequestCost, serviceTierContext, type ServiceTierContext } from "./cost";
|
|
13
|
+
import { type AttemptCostEstimate, type CostEstimate, estimateAttemptCost, estimateRequestCost, serviceTierContext, type ServiceTierContext, tokensPerSecond } from "./cost";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Canonical range members. The warm-up loop in the management usage route
|
|
@@ -73,6 +73,18 @@ export interface UsageSummaryTotals {
|
|
|
73
73
|
cacheSynthesizedRequests?: number;
|
|
74
74
|
/** Rows with no cache detail at all. Not a miss, and not a zero. */
|
|
75
75
|
cacheUnknownRequests?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Aggregated output throughput (#6309): output tokens and wall-clock milliseconds summed over
|
|
78
|
+
* the attempts whose usage was measured, plus how many attempts qualified. The rate is the
|
|
79
|
+
* token sum over the duration sum — a time-weighted average, not a mean of per-request rates —
|
|
80
|
+
* so one fast tiny request cannot outweigh many slow large ones. Attempts without both a
|
|
81
|
+
* positive token count and a positive duration contribute nothing.
|
|
82
|
+
*/
|
|
83
|
+
throughputOutputTokens?: number;
|
|
84
|
+
throughputDurationMs?: number;
|
|
85
|
+
throughputSamples?: number;
|
|
86
|
+
/** Derived at finalize time: throughputOutputTokens / throughputDurationMs * 1000. */
|
|
87
|
+
throughputTokensPerSec?: number;
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
export interface UsageDay {
|
|
@@ -118,6 +130,10 @@ export interface UsageModel {
|
|
|
118
130
|
totalTokens: number;
|
|
119
131
|
inputTokens: number;
|
|
120
132
|
outputTokens: number;
|
|
133
|
+
/** Aggregate output throughput for this row (#6309): summed measured tokens over summed duration. */
|
|
134
|
+
throughputTokensPerSec?: number;
|
|
135
|
+
/** Attempts behind the throughput aggregate; absent when nothing was measured. */
|
|
136
|
+
throughputSamples?: number;
|
|
121
137
|
cachedInputTokens?: number;
|
|
122
138
|
cacheReadInputTokens?: number;
|
|
123
139
|
cacheCreationInputTokens?: number;
|
|
@@ -141,6 +157,12 @@ export interface UsageProvider {
|
|
|
141
157
|
totalTokens: number;
|
|
142
158
|
inputTokens?: number;
|
|
143
159
|
outputTokens?: number;
|
|
160
|
+
/**
|
|
161
|
+
* Aggregate output throughput for this row (#6309): summed measured output tokens over summed
|
|
162
|
+
* wall-clock duration. Undefined when no attempt in the row reported both.
|
|
163
|
+
*/
|
|
164
|
+
throughputTokensPerSec?: number;
|
|
165
|
+
throughputSamples?: number;
|
|
144
166
|
cachedInputTokens?: number;
|
|
145
167
|
cacheReadInputTokens?: number;
|
|
146
168
|
cacheCreationInputTokens?: number;
|
|
@@ -420,6 +442,8 @@ export interface UsageAttribution {
|
|
|
420
442
|
usageStatus: UsageStatus;
|
|
421
443
|
usage?: PersistedUsageEntry["usage"];
|
|
422
444
|
totalTokens?: number;
|
|
445
|
+
/** Wall-clock duration of the attempt, or of the row when it has no attempts. */
|
|
446
|
+
durationMs?: number;
|
|
423
447
|
/** Attempt provenance when the row has one, else the entry's; never assumed observed. */
|
|
424
448
|
cacheProvenance?: CacheTelemetryProvenance;
|
|
425
449
|
}
|
|
@@ -465,6 +489,7 @@ export function usageAttributions(entry: PersistedUsageEntry): UsageAttribution[
|
|
|
465
489
|
usageStatus: entry.usageStatus,
|
|
466
490
|
...(entry.usage ? { usage: entry.usage } : {}),
|
|
467
491
|
...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
|
|
492
|
+
durationMs: entry.durationMs,
|
|
468
493
|
...(entry.cacheProvenance ? { cacheProvenance: entry.cacheProvenance } : {}),
|
|
469
494
|
}];
|
|
470
495
|
}
|
|
@@ -482,6 +507,7 @@ export function usageAttributions(entry: PersistedUsageEntry): UsageAttribution[
|
|
|
482
507
|
usageStatus: attempt.usageStatus,
|
|
483
508
|
...(attempt.usage ? { usage: attempt.usage } : {}),
|
|
484
509
|
...(attempt.totalTokens !== undefined ? { totalTokens: attempt.totalTokens } : {}),
|
|
510
|
+
durationMs: attempt.durationMs,
|
|
485
511
|
...(cacheProvenance ? { cacheProvenance } : {}),
|
|
486
512
|
};
|
|
487
513
|
});
|
|
@@ -611,6 +637,30 @@ function finalizeCoverage(totals: UsageSummaryTotals): void {
|
|
|
611
637
|
totals.coverageRatio = totals.requests === 0 ? 0 : totals.measuredRequests / totals.requests;
|
|
612
638
|
}
|
|
613
639
|
|
|
640
|
+
/**
|
|
641
|
+
* Aggregate output throughput inputs (#6309) into the window totals. A measured attempt
|
|
642
|
+
* contributes its output tokens and duration only when both are positive, so a zero-token
|
|
643
|
+
* or zero-duration row cannot deflate or inflate the rate. Without attempts, the row-level
|
|
644
|
+
* usage and duration describe the request directly.
|
|
645
|
+
*/
|
|
646
|
+
function addThroughputTotals(totals: UsageSummaryTotals, entry: PersistedUsageEntry): void {
|
|
647
|
+
const samples = entry.attempts?.length
|
|
648
|
+
? entry.attempts
|
|
649
|
+
: [{ usage: entry.usage, durationMs: entry.durationMs }];
|
|
650
|
+
for (const sample of samples) {
|
|
651
|
+
if (!sample.usage || tokensPerSecond(sample.usage.outputTokens, sample.durationMs) === null) continue;
|
|
652
|
+
totals.throughputOutputTokens = (totals.throughputOutputTokens ?? 0) + sample.usage.outputTokens;
|
|
653
|
+
totals.throughputDurationMs = (totals.throughputDurationMs ?? 0) + sample.durationMs;
|
|
654
|
+
totals.throughputSamples = (totals.throughputSamples ?? 0) + 1;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/** Fold the raw throughput sums into the rate the dashboard and API expose. */
|
|
659
|
+
function finalizeThroughput(totals: UsageSummaryTotals): void {
|
|
660
|
+
if ((totals.throughputDurationMs ?? 0) <= 0) return;
|
|
661
|
+
totals.throughputTokensPerSec = tokensPerSecond(totals.throughputOutputTokens ?? 0, totals.throughputDurationMs!) ?? undefined;
|
|
662
|
+
}
|
|
663
|
+
|
|
614
664
|
function addEstimatedCost(
|
|
615
665
|
totals: UsageSummaryTotals,
|
|
616
666
|
entry: Pick<PersistedUsageEntry, "usageStatus" | "usage" | "attempts">,
|
|
@@ -670,6 +720,9 @@ interface UsageModelAccumulator {
|
|
|
670
720
|
cacheObserved: boolean;
|
|
671
721
|
/** Input tokens from attributions with OBSERVED cache detail; the hit-rate denominator. */
|
|
672
722
|
cacheObservedInputTokens: number;
|
|
723
|
+
throughputOutputTokens: number;
|
|
724
|
+
throughputDurationMs: number;
|
|
725
|
+
throughputSamples: number;
|
|
673
726
|
estimatedCostUsd?: number;
|
|
674
727
|
requestCounts: UsageRequestCounts;
|
|
675
728
|
requestFacts?: Map<number, number>;
|
|
@@ -820,6 +873,9 @@ function mergeTotals(target: UsageSummaryTotals, source: UsageSummaryTotals): vo
|
|
|
820
873
|
target.cacheObservedRequests = mergeOptionalTotal(target.cacheObservedRequests, source.cacheObservedRequests);
|
|
821
874
|
target.cacheSynthesizedRequests = mergeOptionalTotal(target.cacheSynthesizedRequests, source.cacheSynthesizedRequests);
|
|
822
875
|
target.cacheUnknownRequests = mergeOptionalTotal(target.cacheUnknownRequests, source.cacheUnknownRequests);
|
|
876
|
+
target.throughputOutputTokens = mergeOptionalTotal(target.throughputOutputTokens, source.throughputOutputTokens);
|
|
877
|
+
target.throughputDurationMs = mergeOptionalTotal(target.throughputDurationMs, source.throughputDurationMs);
|
|
878
|
+
target.throughputSamples = mergeOptionalTotal(target.throughputSamples, source.throughputSamples);
|
|
823
879
|
}
|
|
824
880
|
|
|
825
881
|
/** Sum an optional total. Absent on one side means "not computed there", so it contributes nothing. */
|
|
@@ -849,6 +905,9 @@ function blankModelAccumulator(
|
|
|
849
905
|
cacheCreationInputTokens: 0,
|
|
850
906
|
cacheObserved: false,
|
|
851
907
|
cacheObservedInputTokens: 0,
|
|
908
|
+
throughputOutputTokens: 0,
|
|
909
|
+
throughputDurationMs: 0,
|
|
910
|
+
throughputSamples: 0,
|
|
852
911
|
requestCounts: blankRequestCounts(),
|
|
853
912
|
...(mode === "exact" ? { requestFacts: new Map() } : {}),
|
|
854
913
|
};
|
|
@@ -877,6 +936,9 @@ function mergeModelAccumulator(target: UsageModelAccumulator, source: UsageModel
|
|
|
877
936
|
target.cacheCreationInputTokens += source.cacheCreationInputTokens;
|
|
878
937
|
target.cacheObserved ||= source.cacheObserved;
|
|
879
938
|
target.cacheObservedInputTokens += source.cacheObservedInputTokens;
|
|
939
|
+
target.throughputOutputTokens += source.throughputOutputTokens;
|
|
940
|
+
target.throughputDurationMs += source.throughputDurationMs;
|
|
941
|
+
target.throughputSamples += source.throughputSamples;
|
|
880
942
|
if (source.estimatedCostUsd !== undefined) {
|
|
881
943
|
target.estimatedCostUsd = (target.estimatedCostUsd ?? 0) + source.estimatedCostUsd;
|
|
882
944
|
}
|
|
@@ -1081,6 +1143,12 @@ function buildUsageModels(
|
|
|
1081
1143
|
totalTokens: model.summaryTotalTokens,
|
|
1082
1144
|
inputTokens: model.inputTokens,
|
|
1083
1145
|
outputTokens: model.outputTokens,
|
|
1146
|
+
...(model.throughputDurationMs > 0
|
|
1147
|
+
? {
|
|
1148
|
+
throughputTokensPerSec: tokensPerSecond(model.throughputOutputTokens, model.throughputDurationMs) ?? undefined,
|
|
1149
|
+
throughputSamples: model.throughputSamples,
|
|
1150
|
+
}
|
|
1151
|
+
: {}),
|
|
1084
1152
|
cachedInputTokens: model.cacheReadInputTokens,
|
|
1085
1153
|
cacheReadInputTokens: model.cacheReadInputTokens,
|
|
1086
1154
|
cacheCreationInputTokens: model.cacheCreationInputTokens,
|
|
@@ -1120,6 +1188,12 @@ function buildUsageProviders(
|
|
|
1120
1188
|
totalTokens: provider.summaryTotalTokens,
|
|
1121
1189
|
inputTokens: provider.inputTokens,
|
|
1122
1190
|
outputTokens: provider.outputTokens,
|
|
1191
|
+
...(provider.throughputDurationMs > 0
|
|
1192
|
+
? {
|
|
1193
|
+
throughputTokensPerSec: tokensPerSecond(provider.throughputOutputTokens, provider.throughputDurationMs) ?? undefined,
|
|
1194
|
+
throughputSamples: provider.throughputSamples,
|
|
1195
|
+
}
|
|
1196
|
+
: {}),
|
|
1123
1197
|
cachedInputTokens: provider.cacheReadInputTokens,
|
|
1124
1198
|
cacheReadInputTokens: provider.cacheReadInputTokens,
|
|
1125
1199
|
cacheCreationInputTokens: provider.cacheCreationInputTokens,
|
|
@@ -1298,6 +1372,11 @@ class StreamingUsageSummaryAccumulator implements UsageSummaryAccumulator {
|
|
|
1298
1372
|
if (attribution.usage) {
|
|
1299
1373
|
breakdown.inputTokens += attribution.usage.inputTokens;
|
|
1300
1374
|
breakdown.outputTokens += attribution.usage.outputTokens;
|
|
1375
|
+
if (tokensPerSecond(attribution.usage.outputTokens, attribution.durationMs ?? 0) !== null) {
|
|
1376
|
+
breakdown.throughputOutputTokens += attribution.usage.outputTokens;
|
|
1377
|
+
breakdown.throughputDurationMs += attribution.durationMs!;
|
|
1378
|
+
breakdown.throughputSamples += 1;
|
|
1379
|
+
}
|
|
1301
1380
|
const { read, creation, provenance } = cacheObservationFromUsage(
|
|
1302
1381
|
attribution.usage,
|
|
1303
1382
|
attribution.cacheProvenance,
|
|
@@ -1463,6 +1542,7 @@ class StreamingUsageSummaryAccumulator implements UsageSummaryAccumulator {
|
|
|
1463
1542
|
addTokens(partition.totals, entry);
|
|
1464
1543
|
addSpendTotals(partition.totals, entry);
|
|
1465
1544
|
addCacheProvenanceTotals(partition.totals, entry);
|
|
1545
|
+
addThroughputTotals(partition.totals, entry);
|
|
1466
1546
|
addEstimatedCost(partition.totals, entry, costInfo);
|
|
1467
1547
|
|
|
1468
1548
|
const requestKey = this.mode === "exact" ? this.requestKey(entry.requestId) : null;
|
|
@@ -1556,6 +1636,7 @@ class StreamingUsageSummaryAccumulator implements UsageSummaryAccumulator {
|
|
|
1556
1636
|
day.modelOverlaps.push(...partition.modelOverlaps.values());
|
|
1557
1637
|
}
|
|
1558
1638
|
finalizeCoverage(totals);
|
|
1639
|
+
finalizeThroughput(totals);
|
|
1559
1640
|
|
|
1560
1641
|
const customDates = this.window ? new Set(customWindowDates(this.window)) : null;
|
|
1561
1642
|
const dayCount = customDates?.size ?? (range === "all" ? dayCountForAllRange(oldestTimestamp, now) : fixedDays);
|
package/src/web-search/loop.ts
CHANGED
|
@@ -382,9 +382,6 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
382
382
|
const messages: OcxMessage[] = [...parsed.context.messages];
|
|
383
383
|
const loopT0 = Date.now();
|
|
384
384
|
const allTools = parsed.context.tools ?? [];
|
|
385
|
-
// For the forced-answer pass we drop the synthetic web_search tool so the model MUST answer from the
|
|
386
|
-
// results already in `messages` (can't search again) — this guarantees a non-empty final answer.
|
|
387
|
-
const toolsNoWebSearch = allTools.filter(t => !t.webSearch);
|
|
388
385
|
let searchesExecuted = 0;
|
|
389
386
|
let executedSearchCount = 0;
|
|
390
387
|
// Queries whose search already failed this turn — repeats are short-circuited so a model that keeps
|
|
@@ -438,8 +435,8 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
438
435
|
* restart) before the `on429` key rotation.
|
|
439
436
|
*/
|
|
440
437
|
const prepareIterationEvents = async function* (forceAnswer: boolean): AsyncGenerator<AdapterEvent, IterationResponse> {
|
|
441
|
-
// On the forced-answer pass the
|
|
442
|
-
//
|
|
438
|
+
// On the forced-answer pass the model is asked to answer from the results already in
|
|
439
|
+
// `messages`. A weak model can still produce a thin answer that
|
|
443
440
|
// ignores what the search found, which reads to the user as "the search did nothing". Nudge it
|
|
444
441
|
// (iteration-locally — never mutate the shared `messages`) to actually use the gathered results.
|
|
445
442
|
// Only when a REAL search ran (executedSearchCount, not empty-query/limit/repeat placeholders).
|
|
@@ -455,7 +452,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
455
452
|
const iterParsed: OcxParsedRequest = {
|
|
456
453
|
...parsed, stream: true,
|
|
457
454
|
...(recoveringEmptyAnswer ? { options: { ...parsed.options, toolChoice: "none" as const } } : {}),
|
|
458
|
-
context: { ...parsed.context, messages: iterMessages, tools: recoveringEmptyAnswer ? [] :
|
|
455
|
+
context: { ...parsed.context, messages: iterMessages, tools: recoveringEmptyAnswer ? [] : allTools },
|
|
459
456
|
};
|
|
460
457
|
// One cumulative header deadline spans every pool-key 429 rotation in this model iteration.
|
|
461
458
|
// clear() stops only its timer after final headers; the direct turn signal remains attached to
|
|
@@ -885,8 +882,13 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
885
882
|
|
|
886
883
|
// Loop (search + re-ask) ONLY when the model's actionable output is purely web_search. A real
|
|
887
884
|
// tool call (e.g. shell/apply_patch) means this turn is terminal for Codex — finalize so those
|
|
888
|
-
// calls reach Codex.
|
|
889
|
-
|
|
885
|
+
// calls reach Codex.
|
|
886
|
+
// The budget check happens per query inside runSearchCall, so a
|
|
887
|
+
// forced pass that emits one more web_search call is served the
|
|
888
|
+
// limit-reached result instead of failing the turn (#6464: stripping
|
|
889
|
+
// the declaration makes some models emit the raw call as visible
|
|
890
|
+
// text).
|
|
891
|
+
const shouldLoop = split.calls.length > 0 && !split.hasRealToolCall;
|
|
890
892
|
if (!shouldLoop) {
|
|
891
893
|
// #1001: a forced-answer pass that ends `done` must have produced
|
|
892
894
|
// usable output — never a malformed tool call, and never silence.
|
|
@@ -959,6 +961,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
959
961
|
return;
|
|
960
962
|
}
|
|
961
963
|
}
|
|
964
|
+
yield { type: "error", message: "web-search loop exceeded its iteration cap" };
|
|
962
965
|
} finally {
|
|
963
966
|
if (abortSignal) abortSignal.removeEventListener("abort", linkAbort);
|
|
964
967
|
}
|
|
@@ -196,7 +196,6 @@ export async function* runTurnWebSearchLoop(
|
|
|
196
196
|
const { parsed, plan, abortSignal } = deps;
|
|
197
197
|
const messages: OcxMessage[] = [...parsed.context.messages];
|
|
198
198
|
const allTools = [...(parsed.context.tools ?? []), buildWebSearchTool()];
|
|
199
|
-
const toolsNoWebSearch = (parsed.context.tools ?? []).filter(t => !t.webSearch);
|
|
200
199
|
const failedQueries = new Set<string>();
|
|
201
200
|
let searchesExecuted = 0;
|
|
202
201
|
let executedSearchCount = 0;
|
|
@@ -469,9 +468,12 @@ export async function* runTurnWebSearchLoop(
|
|
|
469
468
|
}
|
|
470
469
|
const forceAnswer = searchesExecuted >= plan.maxSearches;
|
|
471
470
|
// Loop only when the model's actionable output is purely web_search calls:
|
|
472
|
-
// a real tool call belongs to Codex
|
|
473
|
-
//
|
|
474
|
-
|
|
471
|
+
// a real tool call belongs to Codex. The budget check happens per query
|
|
472
|
+
// inside runSearchCall, so a forced pass that emits one more web_search
|
|
473
|
+
// call is served the limit-reached result instead of failing the turn
|
|
474
|
+
// (#6464: stripping the declaration makes some models emit the raw call
|
|
475
|
+
// as visible text).
|
|
476
|
+
const shouldLoop = split.calls.length > 0 && !split.hasRealToolCall;
|
|
475
477
|
if (!shouldLoop) {
|
|
476
478
|
// A forced-answer pass that ends `done` must have produced usable output —
|
|
477
479
|
// never a malformed tool call, and never silence. A truncated/refusal
|
|
@@ -497,6 +499,7 @@ export async function* runTurnWebSearchLoop(
|
|
|
497
499
|
eventTypes: [...new Set(split.passthrough.map(event => event.type))],
|
|
498
500
|
}));
|
|
499
501
|
if (!split.hasMalformedToolCall && !split.hasRealToolCall && emptyAnswerRetries === 0) {
|
|
502
|
+
if (i + 1 === HARD_CAP) break;
|
|
500
503
|
emptyAnswerRetries++;
|
|
501
504
|
console.warn("[web-search-runturn] empty forced answer — retrying once without tools");
|
|
502
505
|
yield { type: "heartbeat" };
|
|
@@ -540,6 +543,7 @@ export async function* runTurnWebSearchLoop(
|
|
|
540
543
|
return;
|
|
541
544
|
}
|
|
542
545
|
|
|
546
|
+
if (i + 1 === HARD_CAP) break;
|
|
543
547
|
const nextForceAnswer = searchesExecuted >= plan.maxSearches;
|
|
544
548
|
const iterParsed: OcxParsedRequest = {
|
|
545
549
|
...parsed,
|
|
@@ -548,15 +552,14 @@ export async function* runTurnWebSearchLoop(
|
|
|
548
552
|
messages: nextForceAnswer && executedSearchCount > 0
|
|
549
553
|
? [...messages, forcedAnswerNudge()]
|
|
550
554
|
: messages,
|
|
551
|
-
tools:
|
|
555
|
+
tools: allTools,
|
|
552
556
|
},
|
|
553
557
|
};
|
|
554
558
|
currentParsed = iterParsed;
|
|
555
559
|
source = deps.dispatch(iterParsed);
|
|
556
560
|
}
|
|
557
561
|
|
|
558
|
-
//
|
|
559
|
-
// first), but never hang a client stream if an adapter misbehaves.
|
|
562
|
+
// Repeated over-budget calls terminate instead of leaving an unfinished stream.
|
|
560
563
|
yield { type: "error", message: "web-search runTurn loop exceeded its iteration cap" };
|
|
561
564
|
} catch (error) {
|
|
562
565
|
if (!isTranslatorBudgetExceededError(error)) throw error;
|