@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005
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/bin/ocx.mjs +11 -10
- package/gui/dist/assets/App-CppMz-6w.js +54 -0
- package/gui/dist/assets/App-lCIslZ1L.css +1 -0
- package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
- package/gui/dist/assets/index-DQt9srNi.js +94 -0
- package/gui/dist/assets/index-DmQB8D_R.css +1 -0
- package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
- package/gui/dist/index.html +4 -3
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +19 -2
- package/src/adapters/anthropic/client-identity.ts +13 -4
- package/src/adapters/anthropic/native-client-preamble.ts +31 -0
- package/src/adapters/anthropic/passthrough.ts +103 -25
- 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/devin/cloud-direct/chat.ts +7 -2
- package/src/adapters/devin/trajectory.ts +49 -0
- package/src/adapters/devin.ts +13 -0
- package/src/adapters/ollama-native.ts +62 -19
- 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/client-hello.ts +103 -0
- package/src/claude/intercept/model-bindings.ts +3 -2
- package/src/claude/intercept/picker-bootstrap.ts +37 -16
- package/src/claude/intercept/picker-budget.ts +41 -0
- package/src/claude/intercept/picker-listener.ts +279 -66
- 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/access-audio-input.ts +84 -0
- package/src/cli/access-audio-live.ts +99 -0
- package/src/cli/access-audio.ts +123 -0
- package/src/cli/access-data-client.ts +127 -0
- package/src/cli/access-data-plane.ts +104 -0
- package/src/cli/access-data-response.ts +76 -0
- package/src/cli/access.ts +65 -2
- package/src/cli/account-api.ts +33 -6
- package/src/cli/account-auth.ts +111 -42
- package/src/cli/account-key-quota.ts +118 -0
- package/src/cli/account-main.ts +11 -4
- package/src/cli/account-next-actions.ts +63 -0
- package/src/cli/account-policy-dto.ts +68 -0
- package/src/cli/account-policy.ts +167 -0
- package/src/cli/account-target.ts +8 -0
- package/src/cli/account.ts +37 -9
- package/src/cli/agent-runtime-settings.ts +130 -0
- package/src/cli/agent-settings.ts +101 -0
- package/src/cli/agent.ts +29 -2
- package/src/cli/alias.ts +7 -2
- package/src/cli/aside-profiles.ts +2 -2
- package/src/cli/capabilities-access-remote.ts +147 -0
- package/src/cli/capabilities-accounts.ts +195 -0
- package/src/cli/capabilities-agents-routing.ts +414 -0
- package/src/cli/capabilities-base.ts +783 -0
- package/src/cli/capabilities-command.ts +1 -0
- package/src/cli/capabilities-integrations.ts +264 -0
- package/src/cli/capabilities-lab.ts +214 -0
- package/src/cli/capabilities-observe-system.ts +662 -0
- package/src/cli/capabilities-provider-models.ts +302 -0
- package/src/cli/capabilities.ts +30 -1190
- package/src/cli/capability-types.ts +54 -0
- package/src/cli/catalog-command-result.ts +57 -0
- package/src/cli/chatgpt-command.ts +35 -9
- package/src/cli/claude-agent-startup-sync.ts +17 -0
- package/src/cli/claude-desktop-profile.ts +100 -0
- package/src/cli/claude-desktop.ts +39 -6
- package/src/cli/claude.ts +22 -6
- package/src/cli/codex-shim-autorestore.ts +2 -1
- package/src/cli/codex-shim-readiness.ts +109 -0
- package/src/cli/combo-input.ts +88 -0
- package/src/cli/combo-result.ts +52 -0
- package/src/cli/combo-stats.ts +123 -0
- package/src/cli/combo.ts +37 -14
- package/src/cli/companion-timeline.ts +142 -0
- package/src/cli/companion-usage.ts +146 -0
- package/src/cli/companion.ts +12 -3
- package/src/cli/config-command.ts +24 -8
- package/src/cli/connect.ts +19 -0
- package/src/cli/dispatch.ts +55 -82
- package/src/cli/doctor.ts +22 -4
- package/src/cli/export-command.ts +20 -9
- package/src/cli/gui.ts +8 -3
- package/src/cli/help-catalog.ts +101 -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 +69 -11
- package/src/cli/index.ts +38 -27
- package/src/cli/injection-follow.ts +35 -0
- package/src/cli/inspect.ts +5 -0
- package/src/cli/integration-aside-sync.ts +69 -0
- package/src/cli/integration-cursor.ts +106 -0
- package/src/cli/integration-input.ts +60 -0
- package/src/cli/integration-journal.ts +49 -0
- package/src/cli/integration-plan-dto.ts +156 -0
- package/src/cli/integration-preview.ts +215 -0
- package/src/cli/integrations.ts +24 -18
- package/src/cli/json-input.ts +134 -0
- package/src/cli/link.ts +32 -4
- package/src/cli/local-sync-result.ts +41 -0
- package/src/cli/log-follow.ts +84 -0
- package/src/cli/log-view-filter.ts +146 -0
- package/src/cli/logout-command.ts +44 -0
- package/src/cli/model-picker-ordering.ts +137 -0
- package/src/cli/models-custom-input.ts +107 -0
- package/src/cli/models-custom-runtime.ts +112 -0
- package/src/cli/models-order.ts +194 -0
- package/src/cli/models-runtime-subcommands.ts +2 -0
- package/src/cli/models-runtime.ts +9 -1
- package/src/cli/models.ts +66 -81
- package/src/cli/observe-stream.ts +142 -0
- package/src/cli/observe.ts +75 -34
- package/src/cli/opencode.ts +54 -10
- package/src/cli/provider-batch.ts +77 -0
- package/src/cli/provider-lifecycle-runtime.ts +170 -0
- package/src/cli/provider-result.ts +124 -0
- package/src/cli/provider-runtime.ts +15 -12
- package/src/cli/provider-settings.ts +143 -0
- package/src/cli/provider.ts +116 -64
- package/src/cli/registry.ts +90 -24
- package/src/cli/remote-workspace-hub.ts +83 -0
- package/src/cli/remote-workspace.ts +6 -1
- package/src/cli/restart-failure.ts +27 -0
- package/src/cli/root.ts +42 -9
- package/src/cli/route-policy-write.ts +137 -0
- package/src/cli/route-policy.ts +8 -0
- package/src/cli/runtime-api.ts +57 -80
- package/src/cli/secret-args.ts +149 -0
- package/src/cli/settings-result.ts +30 -0
- package/src/cli/storage.ts +77 -7
- package/src/cli/system-command.ts +60 -2
- package/src/cli/system-restart-client.ts +20 -3
- package/src/cli/system-settings-parity.ts +83 -0
- package/src/cli/tray-proxy.ts +3 -2
- package/src/cli/update-restart-candidate.ts +21 -0
- package/src/cli/update-restart-child.ts +81 -0
- package/src/cli/update-restart-home.ts +38 -0
- package/src/cli/update-restart-transport.ts +193 -0
- package/src/cli/update-restart.ts +260 -0
- package/src/cli/usage-model-search.ts +58 -0
- package/src/cli/usage-report.ts +13 -5
- package/src/cli/v2-input.ts +66 -0
- package/src/cli/v2-local-output.ts +37 -0
- package/src/cli/v2-runtime.ts +90 -0
- package/src/cli/v2.ts +102 -144
- package/src/clients/config-export/contracts.ts +100 -0
- package/src/clients/config-export/droid.ts +54 -2
- package/src/clients/config-export/dsh.ts +21 -0
- 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 +53 -67
- 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/build-entries.ts +1 -1
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/heal-observation.ts +66 -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/publication-observer.ts +19 -0
- package/src/codex/catalog/remote.ts +13 -15
- package/src/codex/catalog/restore.ts +19 -6
- package/src/codex/catalog/retained-sync.ts +60 -11
- package/src/codex/catalog/routed-removal.ts +116 -0
- package/src/codex/catalog/write-audit-contract.ts +21 -0
- package/src/codex/catalog/write-audit.ts +171 -0
- package/src/codex/catalog-self-heal.ts +284 -0
- package/src/codex/catalog-write-serialization.ts +71 -7
- package/src/codex/chatgpt-refresh-failure.ts +56 -0
- package/src/codex/codex-home-owner.ts +124 -0
- package/src/codex/convergence.ts +57 -16
- package/src/codex/features.ts +8 -1
- package/src/codex/inject/restore.ts +46 -4
- package/src/codex/inject.ts +11 -1
- package/src/codex/internal/catalog-writer.ts +120 -13
- package/src/codex/journal.ts +49 -13
- package/src/codex/main-account.ts +144 -11
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/quota-refresh-outcome.ts +17 -2
- package/src/codex/quota-types.ts +36 -1
- package/src/codex/quota.ts +67 -16
- package/src/codex/refresh.ts +20 -9
- 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/runtime.ts +7 -1
- package/src/codex/shim-diagnostics.ts +80 -0
- package/src/codex/shim-fingerprint.ts +7 -2
- package/src/codex/shim-migration.ts +153 -0
- package/src/codex/shim-overlay.ts +509 -0
- package/src/codex/shim-path-resolution.ts +145 -0
- package/src/codex/shim-probe.ts +4 -3
- package/src/codex/shim-state-file.ts +93 -7
- package/src/codex/shim.ts +54 -107
- package/src/codex/subagent-model-fallback.ts +2 -1
- package/src/codex/sync.ts +24 -4
- package/src/combos/failover.ts +101 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/jev.ts +13 -3
- package/src/combos/resolve.ts +12 -0
- package/src/config/atomic-write.ts +13 -8
- package/src/config/diagnostics.ts +23 -1
- package/src/config/load-degrade.ts +7 -0
- package/src/config/provider-validation.ts +3 -0
- package/src/config/schema/chatgpt-desktop.ts +22 -0
- package/src/config/schema/compaction-triggers.ts +8 -0
- package/src/config/schema/config-schema.ts +43 -14
- package/src/config/schema/leaf-validators.ts +8 -3
- package/src/config/subagent-models.ts +12 -0
- package/src/generated/compatibility-version.json +645 -237
- package/src/grok/session-identity.ts +33 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/merge.ts +13 -5
- package/src/integrations/mutation-plan.ts +50 -5
- package/src/integrations/omp-yaml-source.ts +181 -1
- package/src/integrations/ownership-policy.ts +66 -0
- package/src/integrations/registry.ts +50 -1
- package/src/integrations/state.ts +106 -19
- package/src/integrations/target.ts +25 -2
- package/src/integrations/writer.ts +107 -35
- package/src/lib/gui-pair-capability.ts +17 -0
- package/src/lib/package-tree-integrity.ts +42 -1
- package/src/lib/provider-client-headers.ts +62 -0
- package/src/lib/proxy-env.ts +3 -0
- package/src/lib/test-home-guard.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +19 -8
- package/src/oauth/anthropic-continuity.ts +65 -0
- package/src/oauth/anthropic-identity.ts +75 -0
- package/src/oauth/anthropic-routing.ts +20 -15
- package/src/oauth/anthropic.ts +34 -3
- package/src/oauth/generic-account-failover.ts +21 -0
- package/src/oauth/health.ts +32 -5
- package/src/oauth/index.ts +42 -35
- package/src/oauth/local-token-detect.ts +7 -0
- package/src/oauth/pool-settings-capability.ts +9 -2
- 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/protocols/plan-snapshot.ts +1 -1
- package/src/protocols/settings.ts +28 -4
- 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/caller-session-identity.ts +32 -0
- package/src/server/chat-completions.ts +10 -0
- package/src/server/claude-messages.ts +29 -15
- 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 +13 -7
- package/src/server/index.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +12 -134
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +8 -1
- package/src/server/management/cursor-integration-routes.ts +3 -3
- package/src/server/management/integration-routes.ts +69 -11
- package/src/server/management/link-routes.ts +20 -13
- package/src/server/management/model-routes.ts +3 -3
- package/src/server/management/model-row-export-metadata.ts +150 -0
- package/src/server/management/model-rows.ts +148 -9
- package/src/server/management/native-integration-routes.ts +2 -2
- package/src/server/management/oauth-account-routes.ts +96 -30
- package/src/server/management/route-registry.ts +29 -27
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/subagent-model-routes.ts +214 -0
- package/src/server/messages-native-eligibility.ts +3 -6
- package/src/server/messages-native-oauth.ts +82 -35
- package/src/server/messages-native.ts +78 -16
- 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 +152 -28
- 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 +67 -30
- 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 +64 -7
- 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/cli.ts +6 -1
- package/src/service/ownership-mutation-lease.d.mts +26 -0
- package/src/service/ownership-mutation-lease.mjs +76 -1
- package/src/service/windows-taskxml.ts +1 -2
- package/src/service-manager-probe.ts +36 -2
- package/src/types/config.ts +6 -0
- package/src/types/provider.ts +28 -10
- package/src/update/index.ts +2 -1
- package/src/update/npm-invocation.mjs +35 -3
- package/src/update/proxy-liveness-probe.mjs +38 -8
- package/src/update/update-failure-guidance.d.mts +6 -0
- package/src/update/update-failure-guidance.mjs +30 -5
- 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
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import { captureRouteStaticPolicy, routeConcreteModel } from "../../router";
|
|
35
35
|
import { comboDefaultEffort, concreteComboRequestBody, getCombo } from "../../combos";
|
|
36
36
|
import { supportedLadderFor } from "../effort-policy";
|
|
37
|
+
import { applyDroidReasoningDefault } from "../droid-reasoning-default";
|
|
37
38
|
import { resolveWireProtocolOverride } from "../adapter-resolve";
|
|
38
39
|
import { resolveOpenCodeGoTransport } from "../../providers/opencode-go-transport";
|
|
39
40
|
import { getOrAllocateRequestSessionLane } from "../request-log-conversation";
|
|
@@ -105,10 +106,11 @@ export function createComboProtocolLanes(input: {
|
|
|
105
106
|
config: OcxConfig;
|
|
106
107
|
logCtx: RequestLogContext;
|
|
107
108
|
admission: DataPlaneAdmission | undefined;
|
|
109
|
+
droidDefaultEffort?: string;
|
|
108
110
|
comboId: string;
|
|
109
111
|
targets: readonly ComboTarget[];
|
|
110
112
|
}): ComboProtocolLanes | undefined {
|
|
111
|
-
const { source, req, config, logCtx, admission, comboId, targets } = input;
|
|
113
|
+
const { source, req, config, logCtx, admission, droidDefaultEffort, comboId, targets } = input;
|
|
112
114
|
if (!source) return undefined;
|
|
113
115
|
const envelope = source.envelope;
|
|
114
116
|
const reject = resolveProtocolSettings(config).unrepresentable === "reject";
|
|
@@ -207,6 +209,10 @@ export function createComboProtocolLanes(input: {
|
|
|
207
209
|
const route = settle(target, targetRoute);
|
|
208
210
|
const body = nativeBody(target, route);
|
|
209
211
|
if (!route || !body) return undefined;
|
|
212
|
+
applyDroidReasoningDefault(body, droidDefaultEffort, {
|
|
213
|
+
provider: targetRoute.provider,
|
|
214
|
+
modelId: targetRoute.modelId,
|
|
215
|
+
});
|
|
210
216
|
applyComboEffort(body, config, comboId, target, targetRoute);
|
|
211
217
|
return { route, body, sendBudget: targetSendBudget };
|
|
212
218
|
},
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import type { CodexAccountModelRefusal } from "../../combos/failover";
|
|
1
2
|
import {
|
|
2
3
|
isCodexReasoningEffort,
|
|
3
4
|
isDeclaredReasoningEffort,
|
|
4
5
|
resolveEffortAtOrBelow,
|
|
5
6
|
} from "../../reasoning-effort";
|
|
7
|
+
import { resolveAdmissionModelScope, routeAllowedByScope } from "../admission-model-scope";
|
|
6
8
|
import { recordAttemptRequestedEffort } from "../request-log";
|
|
7
9
|
import {
|
|
8
10
|
CODEX_TEXT_GUARDED_BUDGET_POLICY,
|
|
@@ -26,6 +28,7 @@ import {
|
|
|
26
28
|
concreteComboRequestBody,
|
|
27
29
|
comboDefaultEffort,
|
|
28
30
|
isComboTargetInCooldown,
|
|
31
|
+
snapshotComboQuotaCooldowns,
|
|
29
32
|
noteComboSuccess,
|
|
30
33
|
comboFailureDecision,
|
|
31
34
|
advanceComboAfterFailure,
|
|
@@ -65,9 +68,12 @@ import {
|
|
|
65
68
|
recoverEncryptedAgentTaskWithResult,
|
|
66
69
|
} from "./agent-task-recovery";
|
|
67
70
|
import { isThreadSpawnRequest, supportedLadderFor } from "../effort-policy";
|
|
71
|
+
import { applyDroidResponsesReasoningDefault } from "../droid-reasoning-default";
|
|
72
|
+
import { isPlainObject } from "../../lib/plain-data";
|
|
68
73
|
import {
|
|
69
74
|
clientCancelledResponse,
|
|
70
75
|
comboUnavailable,
|
|
76
|
+
comboUnavailableResponse,
|
|
71
77
|
targetIncompatibleResponse,
|
|
72
78
|
unreadableEncryptedAgentTaskResponse,
|
|
73
79
|
} from "./core-errors";
|
|
@@ -327,6 +333,7 @@ export async function executeComboResponses(
|
|
|
327
333
|
config,
|
|
328
334
|
logCtx,
|
|
329
335
|
admission: options.admission,
|
|
336
|
+
droidDefaultEffort: options.droidDefaultEffort,
|
|
330
337
|
comboId,
|
|
331
338
|
targets: combo.targets,
|
|
332
339
|
});
|
|
@@ -407,8 +414,8 @@ export async function executeComboResponses(
|
|
|
407
414
|
}
|
|
408
415
|
};
|
|
409
416
|
const adoptFailedChildLog = (childLog: RequestLogContext): void => {
|
|
410
|
-
// Attempts remain the complete physical history; the logical row mirrors the
|
|
411
|
-
//
|
|
417
|
+
// Attempts remain the complete physical history; the logical row mirrors the failed
|
|
418
|
+
// target whose response is returned, including an earlier quota refusal on exhaustion.
|
|
412
419
|
Object.assign(logCtx, childLog, {
|
|
413
420
|
requestedModel,
|
|
414
421
|
model: requestedModel,
|
|
@@ -572,6 +579,9 @@ export async function executeComboResponses(
|
|
|
572
579
|
candidates: choices.map(choice => choice.candidate),
|
|
573
580
|
fallback,
|
|
574
581
|
config,
|
|
582
|
+
isDestinationAllowed: (providerName, modelId) => routeAllowedByScope(
|
|
583
|
+
resolveAdmissionModelScope(config, options.admission), { providerName, modelId },
|
|
584
|
+
),
|
|
575
585
|
...(combo.decisionProvider ? { decisionProvider: combo.decisionProvider } : {}),
|
|
576
586
|
...(combo.decisionModel
|
|
577
587
|
? {
|
|
@@ -668,6 +678,41 @@ export async function executeComboResponses(
|
|
|
668
678
|
// is gone, so carry the loop's own classification decision instead of re-deriving a
|
|
669
679
|
// weaker one from the status alone (#4149).
|
|
670
680
|
let lastFailureClassifiesOverflow = false;
|
|
681
|
+
// The first quota/rate-limit refusal of the ladder. A fallback that then refuses its own
|
|
682
|
+
// credential or plan (401/403, or a 400 the ladder hopped past) says nothing about why the
|
|
683
|
+
// request failed: the primary target ran out. Returning that fallback refusal told clients
|
|
684
|
+
// "OpenAI account pool has no usable account credential" while every Claude account was spent.
|
|
685
|
+
let quotaFailure: Response | undefined;
|
|
686
|
+
let quotaFailedChildLog: RequestLogContext | undefined;
|
|
687
|
+
// Snapshotted before dispatch: a 401/403 inside this ladder cools its own provider, and that
|
|
688
|
+
// fresh cooldown is not a reason the request found no target.
|
|
689
|
+
const quotaCooldownSnapshot = snapshotComboQuotaCooldowns(config, comboId);
|
|
690
|
+
const exhaustedFailure = (): Response => {
|
|
691
|
+
const returnsQuota = lastFailure && [400, 401, 403].includes(lastFailure.status) && quotaFailure;
|
|
692
|
+
const failedChildLog = returnsQuota ? quotaFailedChildLog : lastFailedChildLog;
|
|
693
|
+
if (failedChildLog) adoptFailedChildLog(failedChildLog);
|
|
694
|
+
if (!lastFailure || ![400, 401, 403].includes(lastFailure.status)) return lastFailure!;
|
|
695
|
+
if (quotaFailure) return quotaFailure;
|
|
696
|
+
// The quota-refused target was cooled before this request picked, so the ladder never saw
|
|
697
|
+
// its refusal. The cooldown, with its Retry-After, is still the honest answer.
|
|
698
|
+
let quotaCooldownUntil: number | undefined;
|
|
699
|
+
const now = Date.now();
|
|
700
|
+
for (const { target, cooldownUntil } of quotaCooldownSnapshot) {
|
|
701
|
+
if (cooldownUntil <= now) continue;
|
|
702
|
+
try {
|
|
703
|
+
if (!targetEligible(target)) continue;
|
|
704
|
+
} catch {
|
|
705
|
+
// An eligibility check can exhaust the translator budget; it is not quota evidence.
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
quotaCooldownUntil = Math.min(quotaCooldownUntil ?? cooldownUntil, cooldownUntil);
|
|
709
|
+
}
|
|
710
|
+
const remainingQuotaCooldownMs = quotaCooldownUntil === undefined ? 0 : quotaCooldownUntil - Date.now();
|
|
711
|
+
return remainingQuotaCooldownMs > 0
|
|
712
|
+
? comboUnavailableResponse(`No available targets for combo: ${comboId}`, {
|
|
713
|
+
retryAfter: String(Math.max(1, Math.ceil(remainingQuotaCooldownMs / 1000))),
|
|
714
|
+
}) : lastFailure;
|
|
715
|
+
};
|
|
671
716
|
while (pick) {
|
|
672
717
|
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
673
718
|
const firstComboTarget = comboTargetsDispatched === 0;
|
|
@@ -690,8 +735,7 @@ export async function executeComboResponses(
|
|
|
690
735
|
// return the last real upstream answer with its status, headers and any quota body
|
|
691
736
|
// intact rather than to mint a synthetic error, and a later target only exists because
|
|
692
737
|
// an earlier one already recorded one.
|
|
693
|
-
|
|
694
|
-
return lastFailure!;
|
|
738
|
+
return exhaustedFailure();
|
|
695
739
|
}
|
|
696
740
|
const targetSendBudget = comboSendScope
|
|
697
741
|
? comboTargetSendBudget(comboSendScope, combo.targets.length - 1 - comboTargetsDispatched)
|
|
@@ -709,8 +753,13 @@ export async function executeComboResponses(
|
|
|
709
753
|
modelId: targetRoute.modelId,
|
|
710
754
|
});
|
|
711
755
|
const initialJevDecision = firstComboTarget ? jevDecision : undefined;
|
|
756
|
+
const childInput = options.droidDefaultEffort && isPlainObject(body) ? { ...body } : body;
|
|
757
|
+
applyDroidResponsesReasoningDefault(childInput, options.droidDefaultEffort, {
|
|
758
|
+
provider: targetRoute.provider,
|
|
759
|
+
modelId: targetRoute.modelId,
|
|
760
|
+
});
|
|
712
761
|
const childBody = concreteComboRequestBody(
|
|
713
|
-
|
|
762
|
+
childInput,
|
|
714
763
|
pick.target,
|
|
715
764
|
initialJevDecision ? initialJevDecision.effort : comboDefaultEffort(config, comboId),
|
|
716
765
|
initialJevDecision?.effort === null ? [] : targetReasoningEfforts,
|
|
@@ -773,6 +822,7 @@ export async function executeComboResponses(
|
|
|
773
822
|
const completedTarget = { provider: pick.target.provider, model: pick.target.model };
|
|
774
823
|
const writerGeneration = pick.writerGeneration;
|
|
775
824
|
let consumedChildFailure: ConsumedComboFailure | undefined;
|
|
825
|
+
let preflightModelRefusal: CodexAccountModelRefusal | undefined;
|
|
776
826
|
const callbackGate = createChildPassthroughCallbackGate({
|
|
777
827
|
...options,
|
|
778
828
|
onResponseComplete: model => {
|
|
@@ -895,6 +945,7 @@ export async function executeComboResponses(
|
|
|
895
945
|
callbackGate.discard();
|
|
896
946
|
terminalRecorder?.("failed", preflight.response.status);
|
|
897
947
|
response = preflight.response;
|
|
948
|
+
preflightModelRefusal = preflight.codexModelRefusal;
|
|
898
949
|
} else {
|
|
899
950
|
response = preflight.response;
|
|
900
951
|
if (nativePassthrough) markNativePassthroughSseResponse(response);
|
|
@@ -937,6 +988,7 @@ export async function executeComboResponses(
|
|
|
937
988
|
try {
|
|
938
989
|
failure = consumedChildFailure
|
|
939
990
|
?? await consumeComboFailure(response, options.abortSignal);
|
|
991
|
+
if (preflightModelRefusal !== undefined) failure = { ...failure, codexModelRefusal: preflightModelRefusal };
|
|
940
992
|
} catch (error) {
|
|
941
993
|
if (options.abortSignal?.aborted) {
|
|
942
994
|
retainCancelledAttempt();
|
|
@@ -963,6 +1015,10 @@ export async function executeComboResponses(
|
|
|
963
1015
|
);
|
|
964
1016
|
attemptRetained = true;
|
|
965
1017
|
lastFailure = failure.response;
|
|
1018
|
+
if (!quotaFailure && (lastFailure.status === 429 || lastFailure.status === 402)) {
|
|
1019
|
+
quotaFailure = lastFailure;
|
|
1020
|
+
quotaFailedChildLog = childLog;
|
|
1021
|
+
}
|
|
966
1022
|
lastFailedChildLog = childLog;
|
|
967
1023
|
// A replacement that answers 200 is unmarked, and its zero-output failure only exists once
|
|
968
1024
|
// preflight has rebuilt the stream as a fresh Response. A spent grant never hops: a status the
|
|
@@ -981,6 +1037,7 @@ export async function executeComboResponses(
|
|
|
981
1037
|
? "stop"
|
|
982
1038
|
: comboFailureDecision(failure.response.status, failure.classificationText, {
|
|
983
1039
|
code: failure.upstreamCode,
|
|
1040
|
+
codexModelRefusal: failure.codexModelRefusal,
|
|
984
1041
|
});
|
|
985
1042
|
const wantsStream = (rawBody as { stream?: unknown } | null)?.stream === true;
|
|
986
1043
|
// Local byte admission has its own diagnostic; do not relabel it as an upstream refusal.
|
|
@@ -1124,9 +1181,9 @@ export async function executeComboResponses(
|
|
|
1124
1181
|
}
|
|
1125
1182
|
// Waiting or recovery may have observed cancellation after the check above.
|
|
1126
1183
|
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
1127
|
-
adoptFailedChildLog(childLog);
|
|
1128
1184
|
}
|
|
1129
1185
|
}
|
|
1186
|
+
const failure = exhaustedFailure();
|
|
1130
1187
|
if (
|
|
1131
1188
|
lastFailure?.status === 413
|
|
1132
1189
|
&& lastFailureClassifiesOverflow
|
|
@@ -1135,5 +1192,5 @@ export async function executeComboResponses(
|
|
|
1135
1192
|
? streamingContextOverflowResponse(requestedModel, options.translatorBudget)
|
|
1136
1193
|
: jsonContextOverflowResponse();
|
|
1137
1194
|
}
|
|
1138
|
-
return
|
|
1195
|
+
return failure;
|
|
1139
1196
|
}
|
|
@@ -38,6 +38,7 @@ import { isInjectionDebugEnabled } from "../../lib/debug-settings";
|
|
|
38
38
|
import { injectionDebugLog } from "../../lib/injection-debug-log";
|
|
39
39
|
import { recordAttemptRequestedEffort } from "../request-log";
|
|
40
40
|
import type { ResolvedFastPolicy } from "../../providers/fastwire";
|
|
41
|
+
import { applyDroidResponsesReasoningDefault } from "../droid-reasoning-default";
|
|
41
42
|
import { applyXaiOauthFastModel, xaiOauthFastModelForDecision } from "../../providers/xai-fast-model";
|
|
42
43
|
|
|
43
44
|
/** Preview the billed xAI lane without mutating request state or resolving credentials. */
|
|
@@ -140,9 +141,15 @@ export async function applyFinalRouteRequestNormalization(args: {
|
|
|
140
141
|
inboundWire: InboundWire;
|
|
141
142
|
inboundTransport?: "websocket";
|
|
142
143
|
claudeGoAffinity?: HandleResponsesOptions["claudeGoAffinity"];
|
|
144
|
+
droidDefaultEffort?: HandleResponsesOptions["droidDefaultEffort"];
|
|
143
145
|
}): Promise<void> {
|
|
144
146
|
const { parsed, route, config, req, logCtx, inboundWire, inboundTransport } = args;
|
|
145
147
|
const effortSelector = prepareEffortNormalization(parsed, route);
|
|
148
|
+
if (applyDroidResponsesReasoningDefault(
|
|
149
|
+
parsed._rawBody,
|
|
150
|
+
args.droidDefaultEffort,
|
|
151
|
+
{ provider: route.provider, modelId: route.modelId },
|
|
152
|
+
)) parsed.options.reasoning = args.droidDefaultEffort;
|
|
146
153
|
|
|
147
154
|
// Only Anthropic message routes retain the Codex-facing selector. Other providers must keep
|
|
148
155
|
// their existing response.model contract even when their public and wire model ids differ.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CodexAccountModelRefusal } from "../../combos/failover";
|
|
1
2
|
import type { NativeResponseControl } from "./native-response-control";
|
|
2
3
|
import type { AdapterEvent, OcxUsage, OcxProviderContinuationState, OcxConfig } from "../../types";
|
|
3
4
|
import type { RouteResult } from "../../router";
|
|
@@ -24,6 +25,8 @@ import type { PolicyRequestScope } from "./policy-request-scope";
|
|
|
24
25
|
export interface ConsumedComboFailure {
|
|
25
26
|
response: Response;
|
|
26
27
|
classificationText: string;
|
|
28
|
+
/** Complete bounded-envelope evidence captured before display truncation; never serialized. */
|
|
29
|
+
codexModelRefusal?: CodexAccountModelRefusal;
|
|
27
30
|
/** Structured upstream `error.code` when present in the failure body. */
|
|
28
31
|
upstreamCode?: string;
|
|
29
32
|
/** Complete structured provider type, retained for conservative recovery classification. */
|
|
@@ -131,6 +134,8 @@ export interface HandleResponsesOptions {
|
|
|
131
134
|
* it. Omitted means a genuine Responses inbound.
|
|
132
135
|
*/
|
|
133
136
|
inboundWire?: InboundWire;
|
|
137
|
+
/** Droid's per-request effort default; each concrete combo or policy target applies it only if its ladder allows it. */
|
|
138
|
+
droidDefaultEffort?: string;
|
|
134
139
|
/** PF-07: the Chat source a combo child may send natively; set only by the Chat ingress. */
|
|
135
140
|
protocolSource?: import("./core-combo-native").ComboProtocolSource;
|
|
136
141
|
/** Internal transport identity for route-scoped upstream compatibility policy. */
|
|
@@ -192,8 +192,17 @@ export function sendWithConnectionPolicy(
|
|
|
192
192
|
const egressInit = redirectedToLoopback
|
|
193
193
|
? { proxy: false as const }
|
|
194
194
|
: decide ? providerEgressSendInit(egress, physicalFetch, input) : {};
|
|
195
|
+
// Bun can reset large native Codex string uploads before receiving headers while
|
|
196
|
+
// the identical UTF-8 buffer succeeds. Convert only at the final HTTP send so
|
|
197
|
+
// WebSocket selection still sees the serialized string and no retry is added.
|
|
198
|
+
const target = input instanceof Request ? input.url : String(input);
|
|
199
|
+
const body = init?.body;
|
|
200
|
+
const largeCodexBody = typeof body === "string"
|
|
201
|
+
&& /^https:\/\/chatgpt\.com\/backend-api\/codex\/responses(?:\/compact)?$/.test(target)
|
|
202
|
+
&& Buffer.byteLength(body, "utf8") >= 1024 * 1024;
|
|
195
203
|
return physicalFetch(input, {
|
|
196
204
|
...init,
|
|
205
|
+
...(largeCodexBody ? { body: Buffer.from(body, "utf8") } : {}),
|
|
197
206
|
headers,
|
|
198
207
|
redirect: "manual",
|
|
199
208
|
...(fresh ? { keepalive: false } : {}),
|
|
@@ -1329,7 +1329,7 @@ export async function deliverPassthroughResponse(
|
|
|
1329
1329
|
clientJson = imageDisplay.json(clientJson);
|
|
1330
1330
|
} catch (error) {
|
|
1331
1331
|
if (error instanceof RangeError) {
|
|
1332
|
-
return formatErrorResponse(502, "upstream_error", "hosted image result
|
|
1332
|
+
return formatErrorResponse(502, "upstream_error", "hosted image result exceeds local display limits");
|
|
1333
1333
|
}
|
|
1334
1334
|
throw error;
|
|
1335
1335
|
} finally { imageDisplay.dispose?.(); }
|
|
@@ -111,7 +111,7 @@ async function shouldHopPolicyCandidate(response: Response, signal?: AbortSignal
|
|
|
111
111
|
if (isPolicyCandidateRefusal(response)) return true;
|
|
112
112
|
try {
|
|
113
113
|
const inspected = await readBoundedResponseBody(response.clone(), { signal });
|
|
114
|
-
const text = inspected.displaySafe ? inspected.text : "";
|
|
114
|
+
const text = inspected.displaySafe && !inspected.truncated ? inspected.text : "";
|
|
115
115
|
return comboFailureDecision(response.status, text, { code: errorCodeFromText(text) }) === "hop";
|
|
116
116
|
} catch {
|
|
117
117
|
return false;
|
|
@@ -676,6 +676,7 @@ export async function prepareResponsesRequest(
|
|
|
676
676
|
route,
|
|
677
677
|
options,
|
|
678
678
|
credentialDomainWasRewritten,
|
|
679
|
+
config,
|
|
679
680
|
);
|
|
680
681
|
// Does the CALLER own the credential this request will authenticate with? Validated exactly
|
|
681
682
|
// the way final auth validates it: the route ownership predicate AND the caller-bearer check
|
|
@@ -736,6 +737,7 @@ export async function prepareResponsesRequest(
|
|
|
736
737
|
// recovery or a profile drain fences the physical identity, final auth drops main and preview
|
|
737
738
|
// has to drop it too. A hardcoded `true` would be wrong in the second case and `false` in
|
|
738
739
|
// the first.
|
|
740
|
+
requestOwnedMainCredential: previewRequestScopedMainCredential,
|
|
739
741
|
isMainAccountTokenLive: previewRequestScopedMainCredential
|
|
740
742
|
? () => previewRequestOwnedMainCredentialLive
|
|
741
743
|
: undefined,
|
|
@@ -962,6 +964,7 @@ export async function prepareResponsesRequest(
|
|
|
962
964
|
route,
|
|
963
965
|
options,
|
|
964
966
|
credentialDomainWasRewritten,
|
|
967
|
+
config,
|
|
965
968
|
);
|
|
966
969
|
const recoveryRequestScopedMainCredential = codexRouteCredentialOwnership(
|
|
967
970
|
recoveryAuthHeaders,
|
|
@@ -993,6 +996,7 @@ export async function prepareResponsesRequest(
|
|
|
993
996
|
const recoverySelectionOptions = {
|
|
994
997
|
nativeMainSelectionOnly: !recoveryNativeMainBlocked
|
|
995
998
|
&& recoverySelectionAdmission?.mainProfileDraining === true,
|
|
999
|
+
requestOwnedMainCredential: recoveryRequestScopedMainCredential,
|
|
996
1000
|
isMainAccountTokenLive: recoveryRequestScopedMainCredential
|
|
997
1001
|
? () => recoveryRequestOwnedMainCredentialLive
|
|
998
1002
|
: undefined,
|
|
@@ -1222,6 +1226,7 @@ export async function prepareResponsesRequest(
|
|
|
1222
1226
|
inboundWire,
|
|
1223
1227
|
inboundTransport: options.inboundTransport,
|
|
1224
1228
|
claudeGoAffinity: options.claudeGoAffinity,
|
|
1229
|
+
droidDefaultEffort: options.droidDefaultEffort,
|
|
1225
1230
|
});
|
|
1226
1231
|
try {
|
|
1227
1232
|
assertPolicyNormalizedRoute(policyScope, admittedPolicyRoute, route);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type { RequestSendObserver } from "../../lib/request-execution-budget";
|
|
3
|
-
import { sharedSpendLedger, type SpendReservationLedger } from "../../lib/spend-reservation-ledger";
|
|
3
|
+
import { sharedSpendLedger, type SpendReservationLedger, type SpendScopes } from "../../lib/spend-reservation-ledger";
|
|
4
4
|
import { SpendLedgerOwnerError } from "../../lib/spend-ledger-owner";
|
|
5
5
|
import { markLocalRequestLogRefusal, type RequestLogContext } from "../request-log";
|
|
6
|
-
import { recordWorkflowRefusalEvent, workflowDenialSummary } from "../../lib/workflow-budget";
|
|
6
|
+
import { recordWorkflowRefusalEvent, workflowDenialSummary, type WorkflowDenial } from "../../lib/workflow-budget";
|
|
7
7
|
|
|
8
8
|
/** The terminal usage a request reported, in the only two fields the ledger books. */
|
|
9
9
|
export interface TerminalSpendUsage {
|
|
@@ -81,44 +81,44 @@ export function createRequestSpendTracker(
|
|
|
81
81
|
// short of its limit forever and refuse nothing.
|
|
82
82
|
const alreadySent = options?.alreadySent === true;
|
|
83
83
|
const sendId = randomUUID();
|
|
84
|
-
const
|
|
84
|
+
const bookedLedger = ledger();
|
|
85
|
+
const scopes: SpendScopes = {
|
|
86
|
+
...(rootId !== undefined ? { rootId } : {}),
|
|
87
|
+
// The existing privacy-safe log label is aliased again by the ledger.
|
|
88
|
+
...(logCtx.accountLogLabel !== undefined ? { identityId: logCtx.accountLogLabel } : {}),
|
|
89
|
+
...(logCtx.provider !== undefined ? { poolId: logCtx.provider } : {}),
|
|
90
|
+
};
|
|
91
|
+
const policy = bookedLedger.policy;
|
|
92
|
+
const enforced = (scopes.rootId !== undefined && policy.root.maxTokens !== undefined)
|
|
93
|
+
|| (scopes.identityId !== undefined && policy.identity.maxTokens !== undefined)
|
|
94
|
+
|| (scopes.poolId !== undefined && policy.pool.maxTokens !== undefined);
|
|
95
|
+
const decision = bookedLedger.reserve({
|
|
85
96
|
sendId,
|
|
86
|
-
scopes
|
|
87
|
-
...(rootId !== undefined ? { rootId } : {}),
|
|
88
|
-
// Already the privacy-safe label the request log uses, and the ledger aliases it
|
|
89
|
-
// again on the way to disk. A raw credential never reaches either.
|
|
90
|
-
...(logCtx.accountLogLabel !== undefined ? { identityId: logCtx.accountLogLabel } : {}),
|
|
91
|
-
...(logCtx.provider !== undefined ? { poolId: logCtx.provider } : {}),
|
|
92
|
-
},
|
|
97
|
+
scopes,
|
|
93
98
|
inputTokens: logCtx.spendInputEstimateTokens ?? logCtx.usageLogInputTokens ?? 0,
|
|
94
99
|
outputCeilingTokens: logCtx.spendOutputCeilingTokens ?? 0,
|
|
95
100
|
...(alreadySent ? { alreadySent: true } : {}),
|
|
96
101
|
});
|
|
97
102
|
if (!decision.reserved) {
|
|
103
|
+
// An applicable ceiling cannot authorize an unbooked send, including when tracking
|
|
104
|
+
// capacity is full. Unconfigured/nonapplicable requests remain observe-only, and a
|
|
105
|
+
// physical send reported after dispatch cannot be refused retroactively.
|
|
106
|
+
if (alreadySent || !enforced) return true;
|
|
98
107
|
refusals += 1;
|
|
99
108
|
const denial = decision.denial;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (denial.reason !== "spend-limit-exceeded") return true;
|
|
111
|
-
// This send is refused, and the dispatch path that asked will report an exhausted send
|
|
112
|
-
// budget -- from there, that is all it can see. The row is where an operator actually
|
|
113
|
-
// looks, so the ceiling is named on it here: a locally assigned code wins in
|
|
114
|
-
// addFinalRequestLog, so the request that CROSSED the ceiling reads as a spend refusal
|
|
115
|
-
// rather than as the ordinary budget exhaustion it would otherwise be indistinguishable
|
|
116
|
-
// from. The event ring gets the same pair so /api/workflow-budget agrees with the row.
|
|
117
|
-
const detail = { scope: denial.scope, limit: denial.limit, projected: denial.projected };
|
|
118
|
-
const summary = workflowDenialSummary("workflow-spend-exhausted", detail);
|
|
109
|
+
const reason: WorkflowDenial = denial.reason === "duplicate-send-id"
|
|
110
|
+
? "workflow-send-replayed"
|
|
111
|
+
: denial.reason === "reserve-not-durable" || denial.reason === "journal-corrupt"
|
|
112
|
+
? "workflow-spend-undurable"
|
|
113
|
+
: denial.reason === "tracking-capacity-exhausted"
|
|
114
|
+
? "workflow-tracking-exhausted"
|
|
115
|
+
: "workflow-spend-exhausted";
|
|
116
|
+
const detail = denial.reason === "spend-limit-exceeded"
|
|
117
|
+
? { scope: denial.scope, limit: denial.limit, projected: denial.projected } : undefined;
|
|
118
|
+
const summary = workflowDenialSummary(reason, detail);
|
|
119
119
|
markLocalRequestLogRefusal(logCtx, summary.code);
|
|
120
120
|
logCtx.errorCode = summary.code;
|
|
121
|
-
recordWorkflowRefusalEvent(rootId,
|
|
121
|
+
recordWorkflowRefusalEvent(rootId, reason, Date.now(), detail);
|
|
122
122
|
return false;
|
|
123
123
|
}
|
|
124
124
|
live.push(sendId);
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "../responses/custom-tool-compat";
|
|
14
14
|
import {
|
|
15
15
|
replaceSseDataPayload,
|
|
16
|
+
splitSseBlock,
|
|
16
17
|
sseDataPayload,
|
|
17
18
|
type SseBlockRewrite,
|
|
18
19
|
} from "./sse-payload-rewrite";
|
|
@@ -22,8 +23,7 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
function replaceSseEventName(block: string, type: string): string {
|
|
25
|
-
const newline = block
|
|
26
|
-
const lines = block.split(/\r?\n/);
|
|
26
|
+
const { newline, lines } = splitSseBlock(block);
|
|
27
27
|
let replaced = false;
|
|
28
28
|
const next = lines.map(line => {
|
|
29
29
|
if (!replaced && line.startsWith("event:")) {
|
|
@@ -2,6 +2,8 @@ import { mkdirSync, writeFileSync, unlinkSync } from "node:fs";
|
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { join, posix } from "node:path";
|
|
4
4
|
import { getConfigDir } from "../config";
|
|
5
|
+
import { jsonUtf8Bytes } from "../lib/json-byte-size";
|
|
6
|
+
import { TRANSLATOR_MAX_TURN_BYTES } from "../lib/translator-budget";
|
|
5
7
|
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
6
8
|
import {
|
|
7
9
|
chargeImageBudget, createImageBudget, decodeValidatedImageBase64,
|
|
@@ -12,6 +14,16 @@ import { sseDataPayload, replaceSseDataPayload, type SseBlockRewrite } from "./s
|
|
|
12
14
|
type Row = Record<string, any>;
|
|
13
15
|
const object = (v: unknown): v is Row => !!v && typeof v === "object" && !Array.isArray(v);
|
|
14
16
|
const originators = new Set(["codex_cli_rs", "Codex Desktop", "codex_app", "codex_work_desktop"]);
|
|
17
|
+
const MAX_DISPLAY_ITEMS = 128;
|
|
18
|
+
const MAX_ITEM_METADATA_BYTES = 64 * 1024;
|
|
19
|
+
const MAX_RETAINED_METADATA_BYTES = 1024 * 1024;
|
|
20
|
+
const displayKey = (item: Row, index: number) => typeof item.id === "string" ? "id:" + item.id : "index:" + index;
|
|
21
|
+
|
|
22
|
+
function displayBytes(value: unknown, limit: number): number {
|
|
23
|
+
try { return jsonUtf8Bytes(value, limit); }
|
|
24
|
+
catch { throw new RangeError("hosted image result exceeds local display limits"); }
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
const generatedName = /^img-codex-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\.(png|jpg|webp|gif)$/;
|
|
16
28
|
|
|
17
29
|
/** One spelling per local path: file URL or plain, either separator, dot segments, folded case. */
|
|
@@ -71,13 +83,20 @@ export function createHostedImageDisplayRewrite(): SseBlockRewrite & { json(text
|
|
|
71
83
|
const states = new Map<string, { message: Row; added: boolean; done: boolean }>();
|
|
72
84
|
let extraSequence = 0;
|
|
73
85
|
let wroteArtifact = false;
|
|
86
|
+
let retainedMetadataBytes = 0;
|
|
74
87
|
|
|
75
88
|
function stateFor(item: Row, index: number) {
|
|
76
|
-
|
|
89
|
+
// Validate every occurrence, including repeats whose state is already complete.
|
|
90
|
+
const metadataBytes = displayBytes({ id: item.id,
|
|
91
|
+
metadata: item.internal_chat_message_metadata_passthrough }, MAX_ITEM_METADATA_BYTES);
|
|
92
|
+
const key = displayKey(item, index);
|
|
77
93
|
let state = states.get(key);
|
|
78
94
|
if (state) return state;
|
|
79
|
-
|
|
80
|
-
if (
|
|
95
|
+
if (states.size >= MAX_DISPLAY_ITEMS) throw new RangeError("hosted image result count exceeds local display limit");
|
|
96
|
+
if (metadataBytes > MAX_RETAINED_METADATA_BYTES - retainedMetadataBytes) {
|
|
97
|
+
throw new RangeError("hosted image result exceeds local display limits");
|
|
98
|
+
}
|
|
99
|
+
retainedMetadataBytes += metadataBytes;
|
|
81
100
|
const id = "msg_ocx_img_" + createHash("sha256").update(key).digest("hex").slice(0, 32);
|
|
82
101
|
state = { added: false, done: false, message: {
|
|
83
102
|
// Persist image results outside the client's collapsible progress messages.
|
|
@@ -147,15 +166,51 @@ export function createHostedImageDisplayRewrite(): SseBlockRewrite & { json(text
|
|
|
147
166
|
return events;
|
|
148
167
|
}
|
|
149
168
|
|
|
150
|
-
function
|
|
169
|
+
function projectedMessage(message: Row): Row {
|
|
170
|
+
if (message.status !== "in_progress") return message;
|
|
171
|
+
// Longest possible local filename and JSON text size, without decoding or writing an image.
|
|
172
|
+
const pathText = ", "img-codex-" + "0".repeat(36) + ".webp") + ">)";
|
|
173
|
+
const fallback = "The completed image result has no supported image data for local display.";
|
|
174
|
+
const text = displayBytes(pathText, TRANSLATOR_MAX_TURN_BYTES) > displayBytes(fallback, TRANSLATOR_MAX_TURN_BYTES)
|
|
175
|
+
? pathText : fallback;
|
|
176
|
+
return { ...message, status: "incomplete", content: [{ type: "output_text", text, annotations: [] }] };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function lifecycleBytes(message: Row): number {
|
|
180
|
+
// At most two item copies and four text copies, plus fixed event fields, ids and numeric indices.
|
|
181
|
+
return 2 * displayBytes(message, TRANSLATOR_MAX_TURN_BYTES)
|
|
182
|
+
+ 4 * displayBytes(message.content[0].text, TRANSLATOR_MAX_TURN_BYTES) + 2048;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function snapshot(response: Row, terminal: boolean, events?: Row[], envelope?: Row): Row {
|
|
151
186
|
if (!Array.isArray(response.output)) return response;
|
|
152
|
-
//
|
|
153
|
-
const keys = new Set(
|
|
187
|
+
// Preflight the complete snapshot before any artifact writes or expanded serialization.
|
|
188
|
+
const keys = new Set<string>();
|
|
189
|
+
const ids = new Set<string>();
|
|
154
190
|
for (const [index, item] of response.output.entries()) {
|
|
155
191
|
if (!object(item) || item.type !== "image_generation_call") continue;
|
|
156
|
-
|
|
157
|
-
if (keys.
|
|
192
|
+
const key = displayKey(item, index);
|
|
193
|
+
if (keys.has(key)) throw new RangeError("duplicate hosted image identity in local display snapshot");
|
|
194
|
+
keys.add(key);
|
|
195
|
+
if (typeof item.id === "string") ids.add(item.id);
|
|
196
|
+
stateFor(item, index);
|
|
197
|
+
}
|
|
198
|
+
if (keys.size === 0) return response;
|
|
199
|
+
const seen = new Set<string>();
|
|
200
|
+
for (const item of response.output) {
|
|
201
|
+
if (!object(item) || !ids.has(item.id)) continue;
|
|
202
|
+
if (seen.has(item.id)) throw new RangeError("conflicting hosted image identity in local display snapshot");
|
|
203
|
+
seen.add(item.id);
|
|
158
204
|
}
|
|
205
|
+
let extraBytes = 32; // Sequence-number growth on a rewritten SSE envelope.
|
|
206
|
+
const preview = { ...response, output: response.output.map((item: unknown, index: number) => {
|
|
207
|
+
if (!object(item) || item.type !== "image_generation_call") return item;
|
|
208
|
+
const state = stateFor(item, index);
|
|
209
|
+
const message = terminal ? projectedMessage(state.message) : state.message;
|
|
210
|
+
if (events && terminal && !state.done) extraBytes += lifecycleBytes(message);
|
|
211
|
+
return message;
|
|
212
|
+
}) };
|
|
213
|
+
displayBytes(envelope ? { ...envelope, response: preview } : preview, TRANSLATOR_MAX_TURN_BYTES - extraBytes);
|
|
159
214
|
let changed = false;
|
|
160
215
|
const output = response.output.map((item: unknown, index: number) => {
|
|
161
216
|
if (!object(item) || item.type !== "image_generation_call") return item;
|
|
@@ -182,9 +237,14 @@ export function createHostedImageDisplayRewrite(): SseBlockRewrite & { json(text
|
|
|
182
237
|
&& (hasIndex || typeof event.item.id === "string")) {
|
|
183
238
|
if (event.type === "response.output_item.added") {
|
|
184
239
|
const state = stateFor(event.item, index);
|
|
240
|
+
if (!state.added) displayBytes({ ...event, item: state.message }, TRANSLATOR_MAX_TURN_BYTES - 32);
|
|
185
241
|
result = state.added ? [] : [{ ...event, item: state.message }];
|
|
186
242
|
state.added = true;
|
|
187
243
|
} else if (event.type === "response.output_item.done") {
|
|
244
|
+
const state = stateFor(event.item, index);
|
|
245
|
+
if (!state.done && lifecycleBytes(projectedMessage(state.message)) > TRANSLATOR_MAX_TURN_BYTES) {
|
|
246
|
+
throw new RangeError("hosted image result exceeds local display limits");
|
|
247
|
+
}
|
|
188
248
|
result = doneEvents(event.item, index);
|
|
189
249
|
}
|
|
190
250
|
} else if (typeof event.type === "string" && event.type.startsWith("response.image_generation_call.")) {
|
|
@@ -193,7 +253,7 @@ export function createHostedImageDisplayRewrite(): SseBlockRewrite & { json(text
|
|
|
193
253
|
} else if (object(event.response)) {
|
|
194
254
|
const terminal = ["response.completed", "response.incomplete", "response.failed"].includes(event.type);
|
|
195
255
|
const injected: Row[] = [];
|
|
196
|
-
const response = snapshot(event.response, terminal, injected);
|
|
256
|
+
const response = snapshot(event.response, terminal, injected, event);
|
|
197
257
|
if (response !== event.response) result = [...injected, { ...event, response }];
|
|
198
258
|
}
|
|
199
259
|
const sequence = Number.isSafeInteger(event.sequence_number) ? event.sequence_number + extraSequence : undefined;
|
|
@@ -216,6 +276,7 @@ export function createHostedImageDisplayRewrite(): SseBlockRewrite & { json(text
|
|
|
216
276
|
};
|
|
217
277
|
rewrite.dispose = () => {
|
|
218
278
|
states.clear();
|
|
279
|
+
retainedMetadataBytes = 0;
|
|
219
280
|
if (wroteArtifact) {
|
|
220
281
|
wroteArtifact = false;
|
|
221
282
|
pruneArtifacts();
|
|
@@ -91,6 +91,7 @@ export function relayResponsesSseWithTerminalRepair(
|
|
|
91
91
|
let maxSequence = -1;
|
|
92
92
|
let buffer = "";
|
|
93
93
|
let bufferBytes = 0;
|
|
94
|
+
let pendingLineFeed = false;
|
|
94
95
|
let timer: unknown;
|
|
95
96
|
let timerGeneration = 0;
|
|
96
97
|
let realTerminalSeen = false;
|
|
@@ -284,6 +285,7 @@ export function relayResponsesSseWithTerminalRepair(
|
|
|
284
285
|
let emitted = false;
|
|
285
286
|
while ((next = nextSseBlock(buffer))) {
|
|
286
287
|
replaceBuffer(next.rest);
|
|
288
|
+
pendingLineFeed = next.delimiter.endsWith("\r") && buffer.length === 0;
|
|
287
289
|
const kind = inspectPayload(sseDataPayload(next.block));
|
|
288
290
|
if (kind === "done" && !realTerminalSeen) {
|
|
289
291
|
emitSynthetic(completeCandidate() ? "completed" : "incomplete", controller);
|
|
@@ -327,9 +329,20 @@ export function relayResponsesSseWithTerminalRepair(
|
|
|
327
329
|
controller.close();
|
|
328
330
|
return;
|
|
329
331
|
}
|
|
330
|
-
|
|
332
|
+
let fragment = decoder.decode(value, { stream: true });
|
|
333
|
+
let continuedDelimiter = false;
|
|
334
|
+
if (pendingLineFeed && fragment.length > 0) {
|
|
335
|
+
pendingLineFeed = false;
|
|
336
|
+
if (fragment.startsWith("\n")) {
|
|
337
|
+
// This byte completes an already-forwarded CR delimiter, not a new EOF suffix.
|
|
338
|
+
controller.enqueue(encoder.encode("\n"));
|
|
339
|
+
fragment = fragment.slice(1);
|
|
340
|
+
continuedDelimiter = true;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
appendBuffer(fragment);
|
|
331
344
|
const result = emitBlocks(controller);
|
|
332
|
-
if (result.closed || result.emitted) return;
|
|
345
|
+
if (result.closed || result.emitted || continuedDelimiter) return;
|
|
333
346
|
}
|
|
334
347
|
} catch (error) {
|
|
335
348
|
if (disposed) return;
|