@bitkyc08/opencodex 2.55.0 → 2.57.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/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +4 -3
- package/src/adapters/base.ts +21 -0
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
- package/src/adapters/command-code.ts +1 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +495 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +40 -1452
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +642 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +58 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +630 -0
- package/src/bridge/sse.ts +1462 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +18 -0
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/index.ts +48 -5
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +4 -4
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +184 -35
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +566 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +431 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +252 -35
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +305 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +714 -0
- package/src/codex/catalog/routed-gather.ts +895 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +567 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +89 -1444
- package/src/codex/lineage.ts +458 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/pool-refresh-backoff.ts +161 -0
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +285 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +780 -0
- package/src/codex/routing/thread-affinity.ts +586 -0
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +370 -2271
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +649 -121
- package/src/images/loop.ts +1 -1
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +198 -23
- package/src/lib/spend-reservation-ledger.ts +958 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +132 -21
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/workflow-budget.ts +553 -30
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +16 -0
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1228 -0
- package/src/providers/registry/entries-extended.ts +1213 -0
- package/src/providers/registry/model-seeds.ts +912 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +456 -0
- package/src/routing/probe-lease.ts +613 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +601 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +339 -0
- package/src/server/index.ts +45 -2552
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +372 -4
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +307 -0
- package/src/server/responses/adapter-continuation.ts +540 -0
- package/src/server/responses/adapter-delivery.ts +208 -0
- package/src/server/responses/adapter-dispatch.ts +1042 -0
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/compact.ts +113 -17
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +529 -0
- package/src/server/responses/core-codex-account.ts +907 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +787 -0
- package/src/server/responses/core-errors.ts +170 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +298 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/passthrough-delivery.ts +869 -0
- package/src/server/responses/passthrough-dispatch.ts +1494 -0
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +1080 -0
- package/src/server/responses/request-send-budget.ts +259 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +803 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +476 -0
- package/src/server/responses/sidecar-execution.ts +463 -0
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/workflow-refusal.ts +84 -0
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +34 -1
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +147 -1
- package/src/usage/summary.ts +171 -21
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import type { ResponsesRequestContext } from "./core-options";
|
|
2
|
+
import type { PreparedResponsesRequest } from "./request-prepare";
|
|
3
|
+
import type { ResponsesTransport } from "./request-transport";
|
|
4
|
+
import type { ResponsesSidecarAuth } from "./request-sidecar-auth";
|
|
5
|
+
import type { ResponsesEffects } from "./response-effects";
|
|
6
|
+
import type { ResponsesSendBudget } from "./request-send-budget";
|
|
7
|
+
import { formatErrorResponse } from "../../bridge";
|
|
8
|
+
import { planWebSearch, buildWebSearchTool, runWithWebSearch } from "../../web-search";
|
|
9
|
+
import {
|
|
10
|
+
planImageBridge,
|
|
11
|
+
planVideoBridge,
|
|
12
|
+
IMAGE_GEN_TOOL_NAME,
|
|
13
|
+
buildImageTool,
|
|
14
|
+
VIDEO_GEN_TOOL_NAME,
|
|
15
|
+
buildVideoTool,
|
|
16
|
+
runWithImageBridge,
|
|
17
|
+
clampImageMaxRounds,
|
|
18
|
+
} from "../../images";
|
|
19
|
+
import type { ProviderAdapter } from "../../adapters/base";
|
|
20
|
+
import { rotateProviderTransportOn429, rateLimitRetryPolicyFor } from "../../providers/key-failover";
|
|
21
|
+
import {
|
|
22
|
+
GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
|
|
23
|
+
isGenericOAuthFailoverEnabled,
|
|
24
|
+
rotateGenericOAuthAccountOn429,
|
|
25
|
+
failoverAccountSnapshot,
|
|
26
|
+
} from "../../oauth/generic-account-failover";
|
|
27
|
+
import {
|
|
28
|
+
ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
|
|
29
|
+
rotateAnthropicAccountOn429,
|
|
30
|
+
getAnthropicPoolAccessSnapshot,
|
|
31
|
+
formatAnthropicProviderForLog,
|
|
32
|
+
} from "../../oauth/anthropic-routing";
|
|
33
|
+
import { resolveWireProtocolOverride } from "../adapter-resolve";
|
|
34
|
+
import { bindRouteReasoningReplayScope, adapterNeedsForcedContinuation } from "./core-replay";
|
|
35
|
+
import { namespacedToolName } from "../../types";
|
|
36
|
+
import { providerFetch } from "./fetch-helpers";
|
|
37
|
+
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
38
|
+
import { recordAdapterReasoning, recordAdapterTier } from "../request-log";
|
|
39
|
+
import { normalizeLogConversationId } from "../request-log-conversation";
|
|
40
|
+
import { rememberResponseState } from "../../responses/state";
|
|
41
|
+
import { trackStreamLifetime } from "../lifecycle";
|
|
42
|
+
|
|
43
|
+
/** One responsibility of the Responses request pipeline; state owners are explicit. */
|
|
44
|
+
export async function executeResponsesSidecars(
|
|
45
|
+
requestContext: Pick<ResponsesRequestContext, "config" | "options" | "logCtx">,
|
|
46
|
+
requestState: Pick<
|
|
47
|
+
PreparedResponsesRequest,
|
|
48
|
+
| "parsed"
|
|
49
|
+
| "route"
|
|
50
|
+
| "inboundWire"
|
|
51
|
+
| "selectedForwardHeaders"
|
|
52
|
+
| "translatorBudget"
|
|
53
|
+
| "rememberKiroDeliveredFinalAnswer"
|
|
54
|
+
| "responseStateOptions"
|
|
55
|
+
>,
|
|
56
|
+
transportState: Pick<
|
|
57
|
+
ResponsesTransport,
|
|
58
|
+
| "adapter"
|
|
59
|
+
| "genericFailoverAccountId"
|
|
60
|
+
| "genericFailovers"
|
|
61
|
+
| "applyFailoverSnapshot"
|
|
62
|
+
| "anthropicPoolAccountId"
|
|
63
|
+
| "anthropicPoolFailovers"
|
|
64
|
+
| "anthropicSessionKey"
|
|
65
|
+
| "commitResolvedOAuthSelection"
|
|
66
|
+
| "resolveSelectionAdapter"
|
|
67
|
+
| "oauthDispatch"
|
|
68
|
+
| "noteRoutedAttemptSend"
|
|
69
|
+
| "bindKeyUsageFromBridge"
|
|
70
|
+
>,
|
|
71
|
+
sidecarState: Pick<ResponsesSidecarAuth, "routedCompaction" | "openAiSidecar">,
|
|
72
|
+
responseEffects: Pick<
|
|
73
|
+
ResponsesEffects,
|
|
74
|
+
| "commitReasoningReplayServingRoute"
|
|
75
|
+
| "continuationStateForResponse"
|
|
76
|
+
| "notifyResponseComplete"
|
|
77
|
+
| "cancelResponseCompletion"
|
|
78
|
+
>,
|
|
79
|
+
sendBudgetState: Pick<ResponsesSendBudget, "reserveCredentialHop">,
|
|
80
|
+
) {
|
|
81
|
+
const { config, options, logCtx } = requestContext;
|
|
82
|
+
const {
|
|
83
|
+
applyFailoverSnapshot,
|
|
84
|
+
anthropicSessionKey,
|
|
85
|
+
commitResolvedOAuthSelection,
|
|
86
|
+
resolveSelectionAdapter,
|
|
87
|
+
oauthDispatch,
|
|
88
|
+
} = transportState;
|
|
89
|
+
const {
|
|
90
|
+
parsed,
|
|
91
|
+
route,
|
|
92
|
+
inboundWire,
|
|
93
|
+
translatorBudget,
|
|
94
|
+
rememberKiroDeliveredFinalAnswer,
|
|
95
|
+
responseStateOptions,
|
|
96
|
+
} = requestState;
|
|
97
|
+
const { routedCompaction, openAiSidecar } = sidecarState;
|
|
98
|
+
const { reserveCredentialHop } = sendBudgetState;
|
|
99
|
+
const {
|
|
100
|
+
commitReasoningReplayServingRoute,
|
|
101
|
+
continuationStateForResponse,
|
|
102
|
+
notifyResponseComplete,
|
|
103
|
+
cancelResponseCompletion,
|
|
104
|
+
} = responseEffects;
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
// Tool results are PAIRED by call_id. parseRequest writes it into OcxToolResultMessage.toolCallId
|
|
108
|
+
// (parser.ts:738/752) without validating it, because inputItemSchema's permissive catch-all
|
|
109
|
+
// (schema.ts:106) accepts a tool item whose strict schema failed only for a missing call_id. A
|
|
110
|
+
// translating adapter then consumes `toolCallId: string` holding undefined: kiro-wire.ts:32
|
|
111
|
+
// TypeErrors, ollama-native.ts:334 throws, and anthropic.ts:775 sends
|
|
112
|
+
// "[tool_result without adjacent tool_use: undefined]" upstream (issue #3259).
|
|
113
|
+
//
|
|
114
|
+
// This CANNOT move into the schema. parseRequest (:2812) runs before the passthrough branch
|
|
115
|
+
// (:3719), so a parse-time rejection would also kill forward/key passthrough and routed
|
|
116
|
+
// compaction — paths that never read context.messages, build from _rawBody, and already
|
|
117
|
+
// degrade an unpaired output to "[tool output for unknown call]" on their own.
|
|
118
|
+
//
|
|
119
|
+
// Keyed on the adapter, not on position: routedCompaction skips the passthrough branch above
|
|
120
|
+
// yet still builds from _rawBody (see the :3703 comment).
|
|
121
|
+
if (!("passthrough" in transportState.adapter && transportState.adapter.passthrough)) {
|
|
122
|
+
const unpaired = parsed.context.messages.find(
|
|
123
|
+
message => message.role === "toolResult"
|
|
124
|
+
&& (typeof (message as { toolCallId?: unknown }).toolCallId !== "string"
|
|
125
|
+
|| (message as { toolCallId: string }).toolCallId.length === 0),
|
|
126
|
+
);
|
|
127
|
+
if (unpaired) {
|
|
128
|
+
// Never interpolate the tool output: this message reaches the client and the logs.
|
|
129
|
+
return formatErrorResponse(
|
|
130
|
+
400,
|
|
131
|
+
"invalid_request_error",
|
|
132
|
+
"tool result requires a non-empty string call_id",
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Image / web-search sidecars: plan once, then dispatch with runTurn-aware priority.
|
|
138
|
+
// Routed-compaction turns must NOT hit the image bridge: compaction clears tools/_webSearch but
|
|
139
|
+
// leaves _imageGeneration, so planImageBridge would activate and return a normal Responses
|
|
140
|
+
// completion instead of the synthetic compaction item Codex expects (#424).
|
|
141
|
+
//
|
|
142
|
+
// Web-search's loop only supports buildRequest/fetch/parseStream — NOT adapter.runTurn. Sending
|
|
143
|
+
// Cursor/runTurn requests into runWithWebSearch produces empty HTTP failures. So:
|
|
144
|
+
// - non-runTurn: web-search wins over image when both eligible (documented priority)
|
|
145
|
+
// - runTurn: image bridge may run (it supports runTurn); web-search is skipped so runTurn
|
|
146
|
+
// can proceed for web-search-only turns
|
|
147
|
+
const wsPlan = !routedCompaction
|
|
148
|
+
? planWebSearch(config, parsed, false, route.provider, route.modelId, openAiSidecar, {
|
|
149
|
+
admission: options.admission, codexAuthPolicy: options.codexAuthPolicy, providerName: route.providerName,
|
|
150
|
+
})
|
|
151
|
+
: undefined;
|
|
152
|
+
const imgPlan = !routedCompaction ? await planImageBridge(config, parsed, route.provider) : undefined;
|
|
153
|
+
const vidPlan = !routedCompaction ? await planVideoBridge(config, parsed, route.provider) : undefined;
|
|
154
|
+
const canRunWebSearch = !!wsPlan && !transportState.adapter.runTurn;
|
|
155
|
+
const rotateSidecarProviderOn429 = async (
|
|
156
|
+
retryAfter: string | null,
|
|
157
|
+
responseHeaders?: Headers,
|
|
158
|
+
): Promise<ProviderAdapter | null> => {
|
|
159
|
+
const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
|
|
160
|
+
retryAfter,
|
|
161
|
+
now: Date.now(),
|
|
162
|
+
attemptedKey: route.provider.apiKey,
|
|
163
|
+
promptCacheKey: parsed.options.promptCacheKey,
|
|
164
|
+
});
|
|
165
|
+
if (rotated) {
|
|
166
|
+
route.provider = rotated;
|
|
167
|
+
} else if (
|
|
168
|
+
// A POSITIVE gate, not an early return. An early `return null` here made every later arm
|
|
169
|
+
// unreachable: Anthropic never has a genericFailoverAccountId (isGenericFailoverProvider
|
|
170
|
+
// excludes it), so its sidecar 429s died on this guard before the Anthropic arm below
|
|
171
|
+
// could ever be considered.
|
|
172
|
+
transportState.genericFailoverAccountId
|
|
173
|
+
&& transportState.genericFailovers < GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
|
|
174
|
+
&& isGenericOAuthFailoverEnabled(config, route.providerName)
|
|
175
|
+
) {
|
|
176
|
+
// Intersection with the request's shared budget. The sidecar replay is dispatched by the
|
|
177
|
+
// web-search/image loop and never reaches `onSendsConsumed`, so this reservation is the
|
|
178
|
+
// charge; a refusal returns null and the caller keeps the real 429 it already has.
|
|
179
|
+
const hop = reserveCredentialHop(
|
|
180
|
+
"auth-recovery",
|
|
181
|
+
`${route.providerName}|${route.modelId}|sidecar-oauth-429`,
|
|
182
|
+
);
|
|
183
|
+
if (!hop.allowed) return null;
|
|
184
|
+
const nextAccountId = rotateGenericOAuthAccountOn429(
|
|
185
|
+
config,
|
|
186
|
+
route.providerName,
|
|
187
|
+
transportState.genericFailoverAccountId,
|
|
188
|
+
retryAfter,
|
|
189
|
+
);
|
|
190
|
+
if (!nextAccountId) {
|
|
191
|
+
hop.permit?.release();
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
|
|
196
|
+
transportState.genericFailovers += 1;
|
|
197
|
+
if (!await applyFailoverSnapshot(snapshot)) {
|
|
198
|
+
hop.permit?.release();
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
hop.permit?.release();
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
hop.permit?.use();
|
|
206
|
+
} else if (
|
|
207
|
+
// Anthropic's pool is excluded from generic failover, so without this arm a 429 inside a
|
|
208
|
+
// web-search or image-bridge turn was terminal even with the pool fully enabled -- while
|
|
209
|
+
// the very same 429 on the main response path rotated.
|
|
210
|
+
transportState.anthropicPoolAccountId
|
|
211
|
+
&& transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST
|
|
212
|
+
) {
|
|
213
|
+
// Same intersection for the Anthropic roster: its own per-request bound still applies,
|
|
214
|
+
// and the shared budget decides whether this request may spend another send at all.
|
|
215
|
+
const hop = reserveCredentialHop(
|
|
216
|
+
"auth-recovery",
|
|
217
|
+
`${route.providerName}|${route.modelId}|sidecar-anthropic-429`,
|
|
218
|
+
);
|
|
219
|
+
if (!hop.allowed) return null;
|
|
220
|
+
const nextAccountId = rotateAnthropicAccountOn429(
|
|
221
|
+
config,
|
|
222
|
+
transportState.anthropicPoolAccountId,
|
|
223
|
+
retryAfter,
|
|
224
|
+
anthropicSessionKey,
|
|
225
|
+
Date.now(),
|
|
226
|
+
responseHeaders,
|
|
227
|
+
);
|
|
228
|
+
if (!nextAccountId) {
|
|
229
|
+
hop.permit?.release();
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
// Deliberately NOT applyFailoverSnapshot: that helper exists to pair per-account routing
|
|
234
|
+
// metadata (Copilot origin, Antigravity project, Kiro context) with its bearer. Anthropic
|
|
235
|
+
// carries none, and getAnthropicPoolAccessToken is what enforces its fail-closed
|
|
236
|
+
// local-cli credential rule. Both existing Anthropic rotation sites apply the token the
|
|
237
|
+
// same way.
|
|
238
|
+
const admitted = await commitResolvedOAuthSelection(await getAnthropicPoolAccessSnapshot(nextAccountId));
|
|
239
|
+
if (!admitted) throw new Error("OAuth selection changed during recovery");
|
|
240
|
+
transportState.anthropicPoolAccountId = admitted.accountId;
|
|
241
|
+
transportState.anthropicPoolFailovers += 1;
|
|
242
|
+
route.provider = { ...route.provider, apiKey: admitted.accessToken };
|
|
243
|
+
logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
|
|
244
|
+
} catch {
|
|
245
|
+
hop.permit?.release();
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
hop.permit?.use();
|
|
249
|
+
} else {
|
|
250
|
+
// No key pool, no generic OAuth roster, no Anthropic pool could produce a replacement
|
|
251
|
+
// credential. The 429 is terminal for this sidecar turn.
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
const rotatedAdapter = resolveSelectionAdapter(
|
|
255
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
256
|
+
config.cacheRetention,
|
|
257
|
+
);
|
|
258
|
+
bindRouteReasoningReplayScope({
|
|
259
|
+
parsed,
|
|
260
|
+
providerName: route.providerName,
|
|
261
|
+
provider: route.provider,
|
|
262
|
+
adapterName: rotatedAdapter.name,
|
|
263
|
+
});
|
|
264
|
+
return rotatedAdapter;
|
|
265
|
+
};
|
|
266
|
+
if ((imgPlan || vidPlan) && canRunWebSearch) {
|
|
267
|
+
// Web search takes priority when both are active — the media bridge cannot run
|
|
268
|
+
// alongside runWithWebSearch. Surface a runtime signal so the user knows their
|
|
269
|
+
// configured video/image bridge was skipped for this turn, rather than silently
|
|
270
|
+
// dropping a paid capability.
|
|
271
|
+
if (vidPlan) console.warn("[videos] video bridge skipped: web search is active for this turn");
|
|
272
|
+
if (imgPlan) console.warn("[images] image bridge skipped: web search is active for this turn");
|
|
273
|
+
}
|
|
274
|
+
if ((imgPlan || vidPlan) && (!wsPlan || transportState.adapter.runTurn)) {
|
|
275
|
+
// The image bridge detects a hosted image_generation tool and requires streaming.
|
|
276
|
+
// The video bridge activates from config and injects a tool — it also needs streaming
|
|
277
|
+
// (the loop returns SSE). For video-only (no imgPlan) on a non-streaming request, skip
|
|
278
|
+
// the bridge entirely so enabling the feature doesn't break ordinary non-streaming traffic.
|
|
279
|
+
if (!parsed.stream) {
|
|
280
|
+
if (imgPlan) {
|
|
281
|
+
return formatErrorResponse(400, "invalid_request_error", "image bridge requires stream=true");
|
|
282
|
+
}
|
|
283
|
+
// Video-only: skip bridge for non-streaming requests
|
|
284
|
+
} else {
|
|
285
|
+
// Replace any pre-existing image_gen/video_gen aliases instead of appending duplicate wire names.
|
|
286
|
+
const priorTools = parsed.context.tools ?? [];
|
|
287
|
+
const bridgeTools = [...priorTools.filter(t => {
|
|
288
|
+
if (t.imageGeneration) return false;
|
|
289
|
+
if (t.videoGeneration) return false;
|
|
290
|
+
if (imgPlan && imgPlan.toolNames.has(t.name)) return false;
|
|
291
|
+
if (imgPlan && t.namespace && imgPlan.toolNames.has(namespacedToolName(t.namespace, t.name))) return false;
|
|
292
|
+
// Only strip unnamespaced video_gen aliases — a namespaced MCP video_gen is left alone.
|
|
293
|
+
if (vidPlan && !t.namespace && vidPlan.toolNames.has(t.name)) return false;
|
|
294
|
+
return true;
|
|
295
|
+
})];
|
|
296
|
+
const existingNames = new Set(bridgeTools.map(t => t.name));
|
|
297
|
+
if (imgPlan && !existingNames.has(IMAGE_GEN_TOOL_NAME)) bridgeTools.push(buildImageTool());
|
|
298
|
+
if (vidPlan && !existingNames.has(VIDEO_GEN_TOOL_NAME)) bridgeTools.push(buildVideoTool());
|
|
299
|
+
parsed.context.tools = bridgeTools;
|
|
300
|
+
// Hosted image_generation tool_choice / allowed_tools must target the synthetic function name.
|
|
301
|
+
// Gate on imgPlan — in a video-only turn buildImageTool() was never injected, so rewriting
|
|
302
|
+
// image_generation/image_gen aliases would add an undeclared tool that strict upstreams reject.
|
|
303
|
+
const tc = parsed.options.toolChoice;
|
|
304
|
+
if (imgPlan && tc && typeof tc === "object" && "allowedTools" in tc && Array.isArray(tc.allowedTools)) {
|
|
305
|
+
const mapped = tc.allowedTools.map(name =>
|
|
306
|
+
name === "image_generation" || name === "image_gen" || (imgPlan.toolNames.has(name) ?? false)
|
|
307
|
+
? IMAGE_GEN_TOOL_NAME
|
|
308
|
+
: name,
|
|
309
|
+
);
|
|
310
|
+
parsed.options.toolChoice = { ...tc, allowedTools: [...new Set(mapped)] };
|
|
311
|
+
} else if (imgPlan && tc && typeof tc === "object" && "name" in tc && typeof tc.name === "string"
|
|
312
|
+
&& (tc.name === "image_generation" || imgPlan.toolNames.has(tc.name))) {
|
|
313
|
+
parsed.options.toolChoice = { ...tc, name: IMAGE_GEN_TOOL_NAME };
|
|
314
|
+
}
|
|
315
|
+
const imageProviderFetch = providerFetch(
|
|
316
|
+
route.provider,
|
|
317
|
+
options.codexWsRuntimeIdentity,
|
|
318
|
+
{ providerName: route.providerName, modelId: route.modelId },
|
|
319
|
+
);
|
|
320
|
+
const imgResponse = await runWithImageBridge({
|
|
321
|
+
parsed, adapter: transportState.adapter,
|
|
322
|
+
incomingMeta: { headers: requestState.selectedForwardHeaders, abortSignal: options.abortSignal, translatorBudget },
|
|
323
|
+
...(imgPlan ? { plan: imgPlan } : {}),
|
|
324
|
+
...(vidPlan ? { videoPlan: vidPlan } : {}),
|
|
325
|
+
forwardHeaders: requestState.selectedForwardHeaders,
|
|
326
|
+
onAttemptSend: (recovery?: AttemptRecoveryKind) =>
|
|
327
|
+
transportState.noteRoutedAttemptSend(logCtx.usageLogInputTokens, recovery),
|
|
328
|
+
abortSignal: options.abortSignal,
|
|
329
|
+
maxRounds: imgPlan && vidPlan
|
|
330
|
+
? clampImageMaxRounds(Math.min(config.images?.maxRounds ?? 3, config.images?.videoMaxRounds ?? 2))
|
|
331
|
+
: imgPlan
|
|
332
|
+
? clampImageMaxRounds(config.images?.maxRounds)
|
|
333
|
+
: clampImageMaxRounds(config.images?.videoMaxRounds ?? 2),
|
|
334
|
+
connectTimeoutMs: config.connectTimeoutMs ?? 200_000,
|
|
335
|
+
stallTimeoutSec: config.stallTimeoutSec,
|
|
336
|
+
waitForRequestSlot: imageProviderFetch.waitForPacing,
|
|
337
|
+
fetchImpl: imageProviderFetch.unpacedFetch ?? imageProviderFetch,
|
|
338
|
+
fetchForRequest: (request, iterParsed) => {
|
|
339
|
+
const fetch = providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
340
|
+
dispatchOverride: oauthDispatch(request, iterParsed),
|
|
341
|
+
providerName: route.providerName, modelId: route.modelId,
|
|
342
|
+
});
|
|
343
|
+
return fetch.unpacedFetch ?? fetch;
|
|
344
|
+
},
|
|
345
|
+
onRequestBuilt: request => {
|
|
346
|
+
recordAdapterReasoning(logCtx, request);
|
|
347
|
+
recordAdapterTier(logCtx, request);
|
|
348
|
+
},
|
|
349
|
+
...(vidPlan?.timeoutMs ? { videoTimeoutMs: vidPlan.timeoutMs } : {}),
|
|
350
|
+
onUsage: usage => {
|
|
351
|
+
// Cursor may assign _cursorConversationId inside the image loop's first runTurn;
|
|
352
|
+
// backfill so Logs can filter/total that opening request (parity with the normal
|
|
353
|
+
// runTurn branch).
|
|
354
|
+
if (!logCtx.conversationId && parsed._cursorConversationId) {
|
|
355
|
+
logCtx.conversationId = normalizeLogConversationId(parsed._cursorConversationId);
|
|
356
|
+
}
|
|
357
|
+
transportState.bindKeyUsageFromBridge(usage);
|
|
358
|
+
},
|
|
359
|
+
on429: rotateSidecarProviderOn429,
|
|
360
|
+
retryOn429Policy: rateLimitRetryPolicyFor(route.provider),
|
|
361
|
+
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
362
|
+
...(options.forceEmptyResponseId ? { forceEmptyResponseId: true } : {}),
|
|
363
|
+
onCompletedResponse: (response, providerState) => {
|
|
364
|
+
commitReasoningReplayServingRoute();
|
|
365
|
+
rememberKiroDeliveredFinalAnswer(transportState.adapter.name, response);
|
|
366
|
+
rememberResponseState(
|
|
367
|
+
parsed._rawBody,
|
|
368
|
+
response,
|
|
369
|
+
continuationStateForResponse(providerState),
|
|
370
|
+
responseStateOptions(adapterNeedsForcedContinuation(transportState.adapter.name)),
|
|
371
|
+
);
|
|
372
|
+
notifyResponseComplete(response);
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
if (imgResponse.body) {
|
|
376
|
+
const imgTurnAc = new AbortController();
|
|
377
|
+
imgTurnAc.signal.addEventListener("abort", cancelResponseCompletion, { once: true });
|
|
378
|
+
return new Response(trackStreamLifetime(imgResponse.body, imgTurnAc, undefined, options.turnAdmissionLease), {
|
|
379
|
+
status: imgResponse.status,
|
|
380
|
+
headers: imgResponse.headers,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
return imgResponse;
|
|
384
|
+
} // end else (streaming bridge)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Web-search sidecar: Codex enabled web_search but this is a routed (non-OpenAI) model that can't
|
|
388
|
+
// run it server-side. Expose web_search as a function tool and run searches via the gpt-mini sidecar
|
|
389
|
+
// through the ChatGPT passthrough, looping until the model answers. Otherwise take the normal path.
|
|
390
|
+
// Placed BEFORE the runTurn early-return for non-runTurn adapters so dual-tool turns dispatch
|
|
391
|
+
// through web-search instead of being swallowed. runTurn adapters never enter this branch.
|
|
392
|
+
if (canRunWebSearch && wsPlan) {
|
|
393
|
+
parsed.context.tools = [...(parsed.context.tools ?? []), buildWebSearchTool()];
|
|
394
|
+
// Resolve the mutable route at send time: a 429 rotation replaces route.provider, so retaining
|
|
395
|
+
// one pre-rotation providerFetch would keep the old credential and transport pin.
|
|
396
|
+
const routedProviderFetch = ((input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) =>
|
|
397
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
398
|
+
providerName: route.providerName,
|
|
399
|
+
modelId: route.modelId,
|
|
400
|
+
})(input, init)) as typeof globalThis.fetch;
|
|
401
|
+
const wsResponse = await runWithWebSearch({
|
|
402
|
+
parsed, adapter: transportState.adapter,
|
|
403
|
+
fetchForRequest: (request, iterParsed) => providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
404
|
+
dispatchOverride: oauthDispatch(request, iterParsed),
|
|
405
|
+
providerName: route.providerName, modelId: route.modelId,
|
|
406
|
+
}),
|
|
407
|
+
incomingMeta: {
|
|
408
|
+
headers: requestState.selectedForwardHeaders,
|
|
409
|
+
abortSignal: options.abortSignal,
|
|
410
|
+
translatorBudget,
|
|
411
|
+
providerFetch: routedProviderFetch,
|
|
412
|
+
},
|
|
413
|
+
backend: wsPlan.backend,
|
|
414
|
+
forwardProvider: wsPlan.forwardSidecar?.provider,
|
|
415
|
+
anthropicSidecar: wsPlan.anthropicSidecar,
|
|
416
|
+
xaiSidecar: wsPlan.xaiSidecar,
|
|
417
|
+
geminiSidecar: wsPlan.geminiSidecar,
|
|
418
|
+
xaiSearchOptions: wsPlan.xaiSearchOptions,
|
|
419
|
+
// The exa key never rides the plan: read it from config at unpack time (L9).
|
|
420
|
+
...(wsPlan.exaConfigured ? { exaApiKey: config.webSearchSidecar?.exaApiKey } : {}),
|
|
421
|
+
hostedTool: wsPlan.hostedTool,
|
|
422
|
+
selectedForwardHeaders: wsPlan.forwardSidecar?.headers ?? requestState.selectedForwardHeaders,
|
|
423
|
+
settings: wsPlan.settings,
|
|
424
|
+
maxSearches: wsPlan.maxSearches,
|
|
425
|
+
forceEmptyResponseId: true,
|
|
426
|
+
abortSignal: options.abortSignal,
|
|
427
|
+
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
428
|
+
onRequestBuilt: request => {
|
|
429
|
+
recordAdapterReasoning(logCtx, request);
|
|
430
|
+
recordAdapterTier(logCtx, request);
|
|
431
|
+
},
|
|
432
|
+
onAttemptSend: (recovery?: AttemptRecoveryKind) =>
|
|
433
|
+
transportState.noteRoutedAttemptSend(logCtx.usageLogInputTokens, recovery),
|
|
434
|
+
onUsage: usage => {
|
|
435
|
+
transportState.bindKeyUsageFromBridge(usage);
|
|
436
|
+
},
|
|
437
|
+
recordSidecarOutcome: wsPlan.forwardSidecar?.recordOutcome,
|
|
438
|
+
connectTimeoutMs: config.connectTimeoutMs ?? 200_000,
|
|
439
|
+
routedModelStallTimeoutMs: wsPlan.routedModelStallTimeoutMs,
|
|
440
|
+
stallTimeoutSec: wsPlan.stallTimeoutSec,
|
|
441
|
+
streamRoutedModelOutput: wsPlan.streamRoutedModelOutput,
|
|
442
|
+
on429: rotateSidecarProviderOn429,
|
|
443
|
+
retryOn429Policy: rateLimitRetryPolicyFor(route.provider),
|
|
444
|
+
onCompletedResponse: response => {
|
|
445
|
+
commitReasoningReplayServingRoute();
|
|
446
|
+
notifyResponseComplete(response);
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
// Register the sidecar stream as an active turn so drainAndShutdown waits for (or aborts)
|
|
450
|
+
// in-flight web-search turns instead of skipping them during graceful shutdown.
|
|
451
|
+
if (wsResponse.body) {
|
|
452
|
+
const wsTurnAc = new AbortController();
|
|
453
|
+
wsTurnAc.signal.addEventListener("abort", cancelResponseCompletion, { once: true });
|
|
454
|
+
return new Response(trackStreamLifetime(wsResponse.body, wsTurnAc, undefined, options.turnAdmissionLease), {
|
|
455
|
+
status: wsResponse.status,
|
|
456
|
+
headers: wsResponse.headers,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
return wsResponse;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
@@ -13,6 +13,9 @@ const PLAN_OR_COMPLETION_RE = /(?:\b(?:i(?:'|’)m going to|i will|i(?:'|’)ll|
|
|
|
13
13
|
const WAITING_FOR_USER_RE = /(?:[??]\s*$|需要我|请(?:确认|选择|提供)|是否|要不要|可以吗|\b(?:do you want|should i|which file|please confirm|please provide)\b)/iu;
|
|
14
14
|
const EXPLICIT_CONTINUE_RE = /^(?:继续|接着|往下|go on|continue|proceed|keep going)\s*[.!。!]?$/iu;
|
|
15
15
|
const MAX_ANNOUNCEMENT_CHARS = 280;
|
|
16
|
+
const MAX_RETAINED_EVENTS = 1_024;
|
|
17
|
+
// JavaScript string code units, not UTF-8 bytes or a process-wide memory limit.
|
|
18
|
+
const MAX_RETAINED_CONTENT_CHARS = 64 * 1_024;
|
|
16
19
|
|
|
17
20
|
export const TERMINAL_GUARD_NUDGE =
|
|
18
21
|
"你刚才只描述了计划,没有执行任何工具。不要再次解释计划,现在立即调用必要工具执行用户任务。" +
|
|
@@ -195,7 +198,17 @@ function mergeUsage(first: OcxUsage | undefined, second: OcxUsage | undefined):
|
|
|
195
198
|
};
|
|
196
199
|
}
|
|
197
200
|
|
|
198
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* Forward adapter events and re-ask only short, suspicious no-tool completions.
|
|
203
|
+
* Retention is bounded per turn; tools or overflow disable analysis without truncating output.
|
|
204
|
+
* Reported usage from completed legs survives a continuation-factory failure. Unreported
|
|
205
|
+
* usage stays absent, and source-iteration failures propagate to the caller's transport handler.
|
|
206
|
+
*
|
|
207
|
+
* @param options Initial stream, parsed history, and continuation callback. The caller owns
|
|
208
|
+
* provider opt-in; the continuation limit defaults to one and is clamped to at most two.
|
|
209
|
+
* @yields Unchanged content events, internal assistant boundaries, and terminal events with
|
|
210
|
+
* accumulated reported usage when available. Returning the iterator closes its active source.
|
|
211
|
+
*/
|
|
199
212
|
export async function* guardTerminalEventStream(options: GuardedEventStreamOptions): AsyncGenerator<AdapterEvent> {
|
|
200
213
|
const maxContinuations = Math.max(0, Math.min(2, Math.floor(options.maxAutoContinuations ?? 1)));
|
|
201
214
|
let parsed = options.parsed;
|
|
@@ -205,6 +218,10 @@ export async function* guardTerminalEventStream(options: GuardedEventStreamOptio
|
|
|
205
218
|
|
|
206
219
|
while (true) {
|
|
207
220
|
const seen: AdapterEvent[] = [];
|
|
221
|
+
let retainedContentChars = 0;
|
|
222
|
+
let retainedText = "";
|
|
223
|
+
let analysisEnabled = (options.adapterName === "anthropic" || options.adapterName === "openai-chat")
|
|
224
|
+
&& continuations < maxContinuations;
|
|
208
225
|
let terminalSeen = false;
|
|
209
226
|
for await (const event of source) {
|
|
210
227
|
// Liveness markers and tool argument fragments are passed through to the bridge, but
|
|
@@ -216,7 +233,7 @@ export async function* guardTerminalEventStream(options: GuardedEventStreamOptio
|
|
|
216
233
|
}
|
|
217
234
|
if (event.type === "done") {
|
|
218
235
|
terminalSeen = true;
|
|
219
|
-
const analysis =
|
|
236
|
+
const analysis = analysisEnabled
|
|
220
237
|
? analyzeTerminalTurn(parsed, seen)
|
|
221
238
|
: { decision: "pass" as const };
|
|
222
239
|
const normalStop = event.stopReason !== "max_tokens" && event.stopReason !== "content_filter";
|
|
@@ -224,11 +241,17 @@ export async function* guardTerminalEventStream(options: GuardedEventStreamOptio
|
|
|
224
241
|
accumulatedUsage = mergeUsage(accumulatedUsage, event.usage);
|
|
225
242
|
continuations += 1;
|
|
226
243
|
parsed = buildContinuationRequest(parsed, seen);
|
|
244
|
+
seen.length = 0;
|
|
245
|
+
retainedText = "";
|
|
227
246
|
yield { type: "assistant_boundary" };
|
|
228
247
|
try {
|
|
229
248
|
source = await options.continuation(parsed);
|
|
230
249
|
} catch (error) {
|
|
231
|
-
yield {
|
|
250
|
+
yield {
|
|
251
|
+
type: "error",
|
|
252
|
+
message: error instanceof Error ? error.message : String(error),
|
|
253
|
+
...(accumulatedUsage ? { usage: accumulatedUsage } : {}),
|
|
254
|
+
};
|
|
232
255
|
return;
|
|
233
256
|
}
|
|
234
257
|
break;
|
|
@@ -243,7 +266,45 @@ export async function* guardTerminalEventStream(options: GuardedEventStreamOptio
|
|
|
243
266
|
yield usage ? { ...event, usage } : event;
|
|
244
267
|
return;
|
|
245
268
|
}
|
|
246
|
-
|
|
269
|
+
if (analysisEnabled) {
|
|
270
|
+
if (event.type === "tool_call_start") {
|
|
271
|
+
// A real tool call permanently rules out a no-tool continuation for this turn.
|
|
272
|
+
analysisEnabled = false;
|
|
273
|
+
} else if (
|
|
274
|
+
event.type === "text_delta"
|
|
275
|
+
|| event.type === "thinking_delta"
|
|
276
|
+
|| event.type === "thinking_signature"
|
|
277
|
+
|| event.type === "redacted_thinking"
|
|
278
|
+
) {
|
|
279
|
+
const content = event.type === "text_delta"
|
|
280
|
+
? event.text
|
|
281
|
+
: event.type === "thinking_delta"
|
|
282
|
+
? event.thinking
|
|
283
|
+
: event.type === "thinking_signature"
|
|
284
|
+
? event.signature
|
|
285
|
+
: event.data;
|
|
286
|
+
if (
|
|
287
|
+
seen.length >= MAX_RETAINED_EVENTS
|
|
288
|
+
|| content.length > MAX_RETAINED_CONTENT_CHARS - retainedContentChars
|
|
289
|
+
) {
|
|
290
|
+
analysisEnabled = false;
|
|
291
|
+
} else {
|
|
292
|
+
retainedContentChars += content.length;
|
|
293
|
+
if (event.type === "text_delta") retainedText += content;
|
|
294
|
+
// Match analyzeTerminalTurn's trimmed-text semantics, including split padding.
|
|
295
|
+
if (retainedText.trim().length > MAX_ANNOUNCEMENT_CHARS) {
|
|
296
|
+
analysisEnabled = false;
|
|
297
|
+
} else {
|
|
298
|
+
seen.push(event);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (!analysisEnabled) {
|
|
303
|
+
// Never rebuild a continuation from truncated thinking or a partial turn.
|
|
304
|
+
seen.length = 0;
|
|
305
|
+
retainedText = "";
|
|
306
|
+
}
|
|
307
|
+
}
|
|
247
308
|
yield event;
|
|
248
309
|
}
|
|
249
310
|
if (!terminalSeen) return;
|
|
@@ -2,7 +2,7 @@ import { collectResponsesToolGroups } from "../responses/tool-groups";
|
|
|
2
2
|
import { relaySseWithPayloadRewrite, type SsePayloadRewrite } from "./sse-payload-rewrite";
|
|
3
3
|
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
4
4
|
|
|
5
|
-
interface NamespacedTool {
|
|
5
|
+
export interface NamespacedTool {
|
|
6
6
|
namespace: string;
|
|
7
7
|
name: string;
|
|
8
8
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { collectAmbiguousDottedAliases, dottedAliasIsUnambiguous, wireToolInnerName } from "../responses/tool-name-aliases";
|
|
2
2
|
import {
|
|
3
|
-
CODE_MODE_EXEC_TOOL_NAME,
|
|
4
3
|
dottedToolName,
|
|
4
|
+
NAMESPACED_BARE_ALIAS_EXCLUDED_NAMES,
|
|
5
5
|
namespacedToolName,
|
|
6
6
|
normalizeDeclaredToolName,
|
|
7
7
|
} from "../types";
|
|
@@ -104,10 +104,14 @@ function addWireToolName(
|
|
|
104
104
|
if (dottedAliasIsUnambiguous(namespace, name) && !ambiguousDottedAliases?.has(dotted)) {
|
|
105
105
|
names.add(dotted);
|
|
106
106
|
}
|
|
107
|
-
//
|
|
108
|
-
// for a
|
|
109
|
-
// `
|
|
110
|
-
|
|
107
|
+
// The code-mode helper spellings do not get a bare alias for a namespaced tool. Bare `exec`
|
|
108
|
+
// switches nested-helper normalization on for a catalog that never declared the shell; bare
|
|
109
|
+
// `exec_command`/`shell_command` switch it off for one that did; bare `write_stdin`/
|
|
110
|
+
// `apply_patch`/`view_image` are simply accepted as declared under a name the caller only ever
|
|
111
|
+
// authorized inside a namespace. This guard named only `exec` and let the other five through,
|
|
112
|
+
// which is the same drift the bridge-side copy had; both now read one list
|
|
113
|
+
// (src/types/tools.ts). Every other inner name keeps the bare alias.
|
|
114
|
+
if (!NAMESPACED_BARE_ALIAS_EXCLUDED_NAMES.has(name)) names.add(name);
|
|
111
115
|
}
|
|
112
116
|
|
|
113
117
|
/**
|