@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,630 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AdapterEvent,
|
|
3
|
+
OcxMessagePhase,
|
|
4
|
+
OcxProviderContinuationState,
|
|
5
|
+
OcxProviderOpaqueToolCallMetadata,
|
|
6
|
+
OcxReasoningReplayScopeRef,
|
|
7
|
+
OcxUsage,
|
|
8
|
+
} from "../types";
|
|
9
|
+
import { coerceIntegerToolArguments } from "../lib/tool-argument-integers";
|
|
10
|
+
import {
|
|
11
|
+
adapterFailureFromMessage,
|
|
12
|
+
classifyError,
|
|
13
|
+
cyberPolicyErrorType,
|
|
14
|
+
CYBER_POLICY_ERROR_CODE,
|
|
15
|
+
isCyberPolicyCode,
|
|
16
|
+
type OcxErrorPayload,
|
|
17
|
+
} from "../lib/errors";
|
|
18
|
+
import { mayBecomePatchEnvelope, repairFreeformToolInput } from "../responses/apply-patch-envelope";
|
|
19
|
+
import { encodeCompactionSummary } from "../responses/compaction";
|
|
20
|
+
import { compileCodeModeHelperInput, resolveCodeModeHelperName } from "../responses/code-mode-helper-compat";
|
|
21
|
+
import { isTruncatedStopReason, truncationReasonFor } from "../responses/truncated-stop-reason";
|
|
22
|
+
import { encodeReasoningEnvelope, type ReasoningEnvelope } from "../responses/reasoning-envelope";
|
|
23
|
+
import { rememberReasoningForCall } from "../responses/reasoning-replay-cache";
|
|
24
|
+
import {
|
|
25
|
+
rememberAndSerializeExtraContent,
|
|
26
|
+
rememberExtraContentForReplay,
|
|
27
|
+
awaitThoughtSignatureDurability,
|
|
28
|
+
} from "../responses/thought-signature-replay";
|
|
29
|
+
import {
|
|
30
|
+
createCitationMarkerFilter,
|
|
31
|
+
stripCitationMarkers,
|
|
32
|
+
type CitationMarkerFilter,
|
|
33
|
+
} from "../responses/citation-markers";
|
|
34
|
+
import { declaresCodeModeExec, normalizeDeclaredToolName } from "../types";
|
|
35
|
+
import { appendSafeWebSearchSource, safeWebSearchSources } from "../web-search/sources";
|
|
36
|
+
import {
|
|
37
|
+
isTranslatorBudgetExceededError,
|
|
38
|
+
releaseTranslatedEvent,
|
|
39
|
+
createTranslatorBudget,
|
|
40
|
+
type TranslatorBudget,
|
|
41
|
+
type TranslatorBufferKind,
|
|
42
|
+
} from "../lib/translator-budget";
|
|
43
|
+
import { adapterFailureFromEvent, emptyChunks, joinChunks, responsesUsage, toolCallArgumentsUsable, uuid, webSearchAction } from "./internal";
|
|
44
|
+
import type { OutputItem, StringChunks } from "./internal";
|
|
45
|
+
import { bridgeToResponsesSSE } from "./sse";
|
|
46
|
+
|
|
47
|
+
export function buildResponseJSON(
|
|
48
|
+
events: AdapterEvent[],
|
|
49
|
+
modelId: string,
|
|
50
|
+
options?: Parameters<typeof buildResponseJSONWithBudget>[2],
|
|
51
|
+
): Record<string, unknown> {
|
|
52
|
+
// Default-budget safety net: a caller that omits the budget gets a bounded
|
|
53
|
+
// default (disposed with the call), never the unbounded append path.
|
|
54
|
+
if (options?.translatorBudget) return buildResponseJSONWithBudget(events, modelId, options);
|
|
55
|
+
const budget = createTranslatorBudget();
|
|
56
|
+
try {
|
|
57
|
+
return buildResponseJSONWithBudget(events, modelId, { ...options, translatorBudget: budget });
|
|
58
|
+
} finally {
|
|
59
|
+
budget.dispose();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildResponseJSONWithBudget(
|
|
64
|
+
events: AdapterEvent[],
|
|
65
|
+
modelId: string,
|
|
66
|
+
options?: {
|
|
67
|
+
hideThinkingSummary?: boolean;
|
|
68
|
+
toolNsMap?: Map<string, { namespace: string; name: string; freeform?: true }>;
|
|
69
|
+
/** Request-visible tool names. When present, an upstream call outside this set fails closed. */
|
|
70
|
+
declaredToolNames?: ReadonlySet<string>;
|
|
71
|
+
/** See `bridgeToResponsesSSE`: enforcement is separate from normalization (#4735). */
|
|
72
|
+
enforceDeclaredToolNames?: boolean;
|
|
73
|
+
/** Declared parameter schema per tool name; repairs integral-float integer args (#1611). */
|
|
74
|
+
toolParameterSchemas?: ReadonlyMap<string, Record<string, unknown>>;
|
|
75
|
+
freeformToolNames?: Set<string>;
|
|
76
|
+
toolSearchToolNames?: Set<string>;
|
|
77
|
+
/** Remote compaction v2 turn — append one synthetic compaction output item (see bridgeToResponsesSSE). */
|
|
78
|
+
compaction?: boolean;
|
|
79
|
+
onProviderState?: (state: OcxProviderContinuationState) => void;
|
|
80
|
+
/** Raw adapter-reported usage before wire normalization (see bridgeToResponsesSSE onUsage). */
|
|
81
|
+
onUsage?: (usage: OcxUsage | undefined) => void;
|
|
82
|
+
translatorBudget?: TranslatorBudget;
|
|
83
|
+
/** Conversation identity for the reasoning replay cache (issue #950). */
|
|
84
|
+
replayCacheScope?: OcxReasoningReplayScopeRef;
|
|
85
|
+
},
|
|
86
|
+
): Record<string, unknown> {
|
|
87
|
+
const responseId = `resp_${uuid()}`;
|
|
88
|
+
const replayCacheScope = options?.replayCacheScope;
|
|
89
|
+
const output: OutputItem[] = [];
|
|
90
|
+
const budget = options?.translatorBudget;
|
|
91
|
+
const encoder = new TextEncoder();
|
|
92
|
+
const bytesOf = (value: string): number => Buffer.byteLength(value);
|
|
93
|
+
const appendBatchString = (
|
|
94
|
+
previous: StringChunks,
|
|
95
|
+
fragment: string,
|
|
96
|
+
kind: TranslatorBufferKind,
|
|
97
|
+
callId?: string,
|
|
98
|
+
): StringChunks => {
|
|
99
|
+
const fragmentBytes = bytesOf(fragment);
|
|
100
|
+
if (fragmentBytes === 0) return previous;
|
|
101
|
+
const nextBytes = previous.bytes + fragmentBytes;
|
|
102
|
+
if (!budget) {
|
|
103
|
+
previous.chunks.push(fragment);
|
|
104
|
+
return { chunks: previous.chunks, bytes: nextBytes };
|
|
105
|
+
}
|
|
106
|
+
const scope = { kind, ...(callId ? { callId } : {}) };
|
|
107
|
+
const reservation = budget.reserveTransient(nextBytes, scope);
|
|
108
|
+
try {
|
|
109
|
+
previous.chunks.push(fragment);
|
|
110
|
+
const result: StringChunks = { chunks: previous.chunks, bytes: nextBytes };
|
|
111
|
+
reservation.commitRetained();
|
|
112
|
+
budget.releaseRetained(previous.bytes, scope);
|
|
113
|
+
return result;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
reservation.release();
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
// Batch counterpart: tool-call arguments require direct string representation for immediate
|
|
120
|
+
// JSON serialization compatibility.
|
|
121
|
+
const appendBatchStringDirect = (
|
|
122
|
+
previous: string,
|
|
123
|
+
previousBytes: number,
|
|
124
|
+
fragment: string,
|
|
125
|
+
kind: TranslatorBufferKind,
|
|
126
|
+
callId?: string,
|
|
127
|
+
): { value: string; bytes: number } => {
|
|
128
|
+
const nextBytes = previousBytes + bytesOf(fragment);
|
|
129
|
+
if (!budget) return { value: previous + fragment, bytes: nextBytes };
|
|
130
|
+
const scope = { kind, ...(callId ? { callId } : {}) };
|
|
131
|
+
const reservation = budget.reserveTransient(nextBytes, scope);
|
|
132
|
+
try {
|
|
133
|
+
const value = previous + fragment;
|
|
134
|
+
reservation.commitRetained();
|
|
135
|
+
budget.releaseRetained(previousBytes, scope);
|
|
136
|
+
return { value, bytes: nextBytes };
|
|
137
|
+
} catch (error) {
|
|
138
|
+
reservation.release();
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const replaceBatchRetainedString = (previousBytes: number, next: string, kind: TranslatorBufferKind): number => {
|
|
143
|
+
const nextBytes = bytesOf(next);
|
|
144
|
+
if (!budget) return nextBytes;
|
|
145
|
+
const reservation = budget.reserveTransient(nextBytes, { kind });
|
|
146
|
+
reservation.commitRetained();
|
|
147
|
+
budget.releaseRetained(previousBytes, { kind });
|
|
148
|
+
return nextBytes;
|
|
149
|
+
};
|
|
150
|
+
const pushOutput = (item: OutputItem, replacedBytes = 0, kind: TranslatorBufferKind = "retained_collectors") => {
|
|
151
|
+
const reservation = budget?.reserveTransient(bytesOf(JSON.stringify(item)), { kind });
|
|
152
|
+
output.push(item);
|
|
153
|
+
reservation?.commitRetained();
|
|
154
|
+
if (replacedBytes > 0) budget?.releaseRetained(replacedBytes, { kind });
|
|
155
|
+
};
|
|
156
|
+
let usage: OcxUsage | undefined;
|
|
157
|
+
let errorEvent: Extract<AdapterEvent, { type: "error" }> | undefined;
|
|
158
|
+
let incompleteEvent: Extract<AdapterEvent, { type: "incomplete" }> | undefined;
|
|
159
|
+
let endTurn: boolean | undefined;
|
|
160
|
+
let stopReason: string | undefined;
|
|
161
|
+
// The adapter's stop reason exactly as it arrived. `stopReason` above is deliberately narrowed
|
|
162
|
+
// to the two reasons that map onto a Responses `incomplete_details`; the raw value is what the
|
|
163
|
+
// truncation guard needs, because adapters disagree on vocabulary (`length`, `refusal`, ...).
|
|
164
|
+
let rawStopReason: string | undefined;
|
|
165
|
+
let cleanDone = false;
|
|
166
|
+
// Whether the adapter emitted ANY terminal (done/error/incomplete). Distinct from `cleanDone`,
|
|
167
|
+
// which is only true for a `done` without a stop reason. A buffered turn whose adapter simply
|
|
168
|
+
// stopped emitting has no terminal at all, and must not be reported as a success.
|
|
169
|
+
let sawTerminal = false;
|
|
170
|
+
let batchCompaction = emptyChunks();
|
|
171
|
+
let compactionEncryptedContent: string | undefined;
|
|
172
|
+
|
|
173
|
+
let currentText = emptyChunks();
|
|
174
|
+
let currentTextPhase: OcxMessagePhase | undefined;
|
|
175
|
+
let currentSummaryReasoning = emptyChunks();
|
|
176
|
+
let currentRawReasoning = emptyChunks();
|
|
177
|
+
// Same replay-cache handoff as the streaming path (issue #950): the most
|
|
178
|
+
// recently flushed raw reasoning waits for the tool call it preceded.
|
|
179
|
+
let rawReasoningForNextToolCall = "";
|
|
180
|
+
// Anthropic extended-thinking round-trip (batch): see bridgeToResponsesSSE counterpart.
|
|
181
|
+
let batchSignature: string | undefined;
|
|
182
|
+
let batchSignatureBytes = 0;
|
|
183
|
+
let batchRedacted: string[] = [];
|
|
184
|
+
let batchRedactedBytes = 0;
|
|
185
|
+
// Kiro reasoning blob, held until after the trailing flushes so it lands AFTER the assistant
|
|
186
|
+
// message (see the streaming path). Retained because it outlives releaseTranslatedEvent.
|
|
187
|
+
let batchKiroRedacted: string | undefined;
|
|
188
|
+
let batchKiroRedactedBytes = 0;
|
|
189
|
+
let currentToolCallId = "";
|
|
190
|
+
let currentToolCallName = "";
|
|
191
|
+
let currentToolCallCodeModeHelperName: string | undefined;
|
|
192
|
+
let currentToolCallArgs = "";
|
|
193
|
+
let currentToolCallProviderMetadata: OcxProviderOpaqueToolCallMetadata | undefined;
|
|
194
|
+
let currentToolCallArgsBytes = 0;
|
|
195
|
+
// Web-search citations awaiting the next assistant message (attached as url_citation annotations).
|
|
196
|
+
let pendingWebSources: { url: string; title?: string }[] = [];
|
|
197
|
+
|
|
198
|
+
const freeformInput = (
|
|
199
|
+
args: string,
|
|
200
|
+
toolName: string,
|
|
201
|
+
namespace?: string,
|
|
202
|
+
codeModeHelperName?: string,
|
|
203
|
+
): string => {
|
|
204
|
+
const helper = resolveCodeModeHelperName(codeModeHelperName, toolName, args, namespace, options?.declaredToolNames);
|
|
205
|
+
return helper
|
|
206
|
+
? compileCodeModeHelperInput(args, helper)
|
|
207
|
+
: repairFreeformToolInput(args, toolName, namespace);
|
|
208
|
+
};
|
|
209
|
+
const parseArgsObj = (args: string): Record<string, unknown> => {
|
|
210
|
+
try { const o = JSON.parse(args); return o && typeof o === "object" ? o : {}; } catch { return {}; }
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const flushText = (inferredPhase?: OcxMessagePhase) => {
|
|
214
|
+
const currentTextStr = joinChunks(currentText);
|
|
215
|
+
if (!currentTextStr) return;
|
|
216
|
+
const phase = currentTextPhase ?? inferredPhase;
|
|
217
|
+
// ChatGPT-backend citation markers arrive as literal private-use characters that the
|
|
218
|
+
// Codex TUI prints verbatim (#3150). Strip them here rather than at the accumulator so
|
|
219
|
+
// the retained byte accounting above still describes what the upstream actually sent.
|
|
220
|
+
const text = stripCitationMarkers(currentTextStr);
|
|
221
|
+
const sourceBytes = pendingWebSources.reduce((sum, source) => sum + bytesOf(JSON.stringify(source)), 0);
|
|
222
|
+
const annotations = pendingWebSources.map(s => ({
|
|
223
|
+
type: "url_citation", url: s.url, ...(s.title ? { title: s.title } : {}), start_index: 0, end_index: 0,
|
|
224
|
+
}));
|
|
225
|
+
pendingWebSources = [];
|
|
226
|
+
const item = {
|
|
227
|
+
type: "message", id: `msg_${uuid()}`, role: "assistant", status: "completed",
|
|
228
|
+
content: [{ type: "output_text", text, annotations }],
|
|
229
|
+
...(phase ? { phase } : {}),
|
|
230
|
+
} as OutputItem;
|
|
231
|
+
pushOutput(item, currentText.bytes);
|
|
232
|
+
budget?.releaseRetained(sourceBytes, { kind: "tool_search_sources" });
|
|
233
|
+
currentText = emptyChunks();
|
|
234
|
+
currentTextPhase = undefined;
|
|
235
|
+
};
|
|
236
|
+
const flushSummaryReasoning = () => {
|
|
237
|
+
const summaryText = joinChunks(currentSummaryReasoning);
|
|
238
|
+
if (!summaryText && !batchSignature && batchRedacted.length === 0) return;
|
|
239
|
+
const envelope: ReasoningEnvelope = {};
|
|
240
|
+
if (batchSignature) envelope.sig = batchSignature;
|
|
241
|
+
if (batchRedacted.length > 0) envelope.red = batchRedacted;
|
|
242
|
+
const hidden = options?.hideThinkingSummary === true;
|
|
243
|
+
if (hidden && summaryText && (envelope.sig || envelope.red)) envelope.txt = summaryText;
|
|
244
|
+
const encrypted = envelope.sig || envelope.red || envelope.txt ? encodeReasoningEnvelope(envelope, budget) : undefined;
|
|
245
|
+
const sourceBytes = currentSummaryReasoning.bytes + batchSignatureBytes + batchRedactedBytes;
|
|
246
|
+
batchSignature = undefined;
|
|
247
|
+
batchSignatureBytes = 0;
|
|
248
|
+
batchRedacted = [];
|
|
249
|
+
batchRedactedBytes = 0;
|
|
250
|
+
if (hidden && !encrypted) {
|
|
251
|
+
budget?.releaseRetained(sourceBytes, { kind: "reasoning" });
|
|
252
|
+
currentSummaryReasoning = emptyChunks();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const item = {
|
|
256
|
+
type: "reasoning", id: `rs_${uuid()}`,
|
|
257
|
+
summary: !hidden && summaryText ? [{ type: "summary_text", text: summaryText }] : [],
|
|
258
|
+
...(encrypted ? { encrypted_content: encrypted } : {}),
|
|
259
|
+
} as OutputItem;
|
|
260
|
+
pushOutput(item, sourceBytes, "reasoning");
|
|
261
|
+
currentSummaryReasoning = emptyChunks();
|
|
262
|
+
};
|
|
263
|
+
const flushRawReasoning = () => {
|
|
264
|
+
const rawText = joinChunks(currentRawReasoning);
|
|
265
|
+
if (!rawText) return;
|
|
266
|
+
rawReasoningForNextToolCall = rawText;
|
|
267
|
+
if (options?.hideThinkingSummary === true) {
|
|
268
|
+
// Same contract as the streaming path: no visible reasoning, txt-only envelope round-trip.
|
|
269
|
+
pushOutput({
|
|
270
|
+
type: "reasoning", id: `rs_${uuid()}`, summary: [],
|
|
271
|
+
encrypted_content: encodeReasoningEnvelope({ txt: rawText }, budget),
|
|
272
|
+
}, currentRawReasoning.bytes, "reasoning");
|
|
273
|
+
currentRawReasoning = emptyChunks();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
pushOutput({
|
|
277
|
+
type: "reasoning", id: `rs_${uuid()}`,
|
|
278
|
+
summary: [],
|
|
279
|
+
content: [{ type: "reasoning_text", text: rawText }],
|
|
280
|
+
}, currentRawReasoning.bytes, "reasoning");
|
|
281
|
+
currentRawReasoning = emptyChunks();
|
|
282
|
+
};
|
|
283
|
+
const flushToolCall = (status: "completed" | "incomplete" = "completed") => {
|
|
284
|
+
if (!currentToolCallId) return;
|
|
285
|
+
const mapped = options?.toolNsMap?.get(currentToolCallName);
|
|
286
|
+
const realName = mapped?.name ?? currentToolCallName;
|
|
287
|
+
const ns = mapped?.namespace;
|
|
288
|
+
const toolSearch = options?.toolSearchToolNames?.has(realName) ?? false;
|
|
289
|
+
const freeform = !toolSearch && (mapped
|
|
290
|
+
? mapped.freeform === true
|
|
291
|
+
: (options?.freeformToolNames?.has(realName) ?? false));
|
|
292
|
+
// #1611: same integral-float repair as the streaming path. Keyed by the wire name
|
|
293
|
+
// the request declared, which is the pre-namespace-mapping `currentToolCallName`.
|
|
294
|
+
const coercedArgs = coerceIntegerToolArguments(
|
|
295
|
+
currentToolCallArgs,
|
|
296
|
+
options?.toolParameterSchemas?.get(currentToolCallName),
|
|
297
|
+
ns === undefined ? realName : undefined,
|
|
298
|
+
);
|
|
299
|
+
// Freeform tools serialize as custom_tool_call without extra_content; remember the
|
|
300
|
+
// signature server-side regardless so the replayed call can be re-signed (#1735).
|
|
301
|
+
void rememberExtraContentForReplay(currentToolCallId, currentToolCallProviderMetadata, replayCacheScope);
|
|
302
|
+
if (toolSearch) {
|
|
303
|
+
pushOutput({
|
|
304
|
+
type: "tool_search_call", id: `tsc_${uuid()}`,
|
|
305
|
+
call_id: currentToolCallId, execution: "client",
|
|
306
|
+
arguments: parseArgsObj(coercedArgs), status,
|
|
307
|
+
});
|
|
308
|
+
} else if (freeform) {
|
|
309
|
+
pushOutput({
|
|
310
|
+
type: "custom_tool_call", id: `ctc_${uuid()}`,
|
|
311
|
+
call_id: currentToolCallId, name: realName,
|
|
312
|
+
...(ns ? { namespace: ns } : {}),
|
|
313
|
+
input: freeformInput(currentToolCallArgs, realName, ns, currentToolCallCodeModeHelperName), status,
|
|
314
|
+
});
|
|
315
|
+
} else {
|
|
316
|
+
pushOutput({
|
|
317
|
+
type: "function_call", id: `fc_${uuid()}`,
|
|
318
|
+
call_id: currentToolCallId, name: realName,
|
|
319
|
+
arguments: coercedArgs || "{}", status,
|
|
320
|
+
...(ns ? { namespace: ns } : {}),
|
|
321
|
+
...(rememberAndSerializeExtraContent(currentToolCallId, currentToolCallProviderMetadata, replayCacheScope).extra ?? {}),
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
budget?.closeCall(currentToolCallId);
|
|
325
|
+
currentToolCallId = "";
|
|
326
|
+
currentToolCallName = "";
|
|
327
|
+
currentToolCallCodeModeHelperName = undefined;
|
|
328
|
+
currentToolCallProviderMetadata = undefined;
|
|
329
|
+
currentToolCallArgs = "";
|
|
330
|
+
currentToolCallArgsBytes = 0;
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
for (const e of events) {
|
|
334
|
+
if (errorEvent) {
|
|
335
|
+
// Match streaming: once the turn fails, later parallel calls must not become executable
|
|
336
|
+
// completed output. Still release every retained event in order and preserve terminal usage.
|
|
337
|
+
if (e.type === "error" || e.type === "incomplete" || e.type === "done") {
|
|
338
|
+
usage = e.usage ?? usage;
|
|
339
|
+
}
|
|
340
|
+
if (budget) releaseTranslatedEvent(e, budget);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (batchSignature !== undefined && e.type !== "thinking_signature" && e.type !== "heartbeat") {
|
|
344
|
+
flushSummaryReasoning();
|
|
345
|
+
}
|
|
346
|
+
switch (e.type) {
|
|
347
|
+
case "assistant_boundary":
|
|
348
|
+
flushText("commentary");
|
|
349
|
+
flushSummaryReasoning();
|
|
350
|
+
flushRawReasoning();
|
|
351
|
+
rawReasoningForNextToolCall = "";
|
|
352
|
+
flushToolCall();
|
|
353
|
+
break;
|
|
354
|
+
case "text_delta":
|
|
355
|
+
// Only flush on an explicit phase change. A later delta that omits `phase` must keep
|
|
356
|
+
// appending under the previously established phase.
|
|
357
|
+
if (currentText.bytes > 0 && e.phase !== undefined && currentTextPhase !== e.phase) flushText("commentary");
|
|
358
|
+
if (currentSummaryReasoning.bytes > 0) flushSummaryReasoning();
|
|
359
|
+
if (currentRawReasoning.bytes > 0) flushRawReasoning();
|
|
360
|
+
// Empty text deltas (batch chat responses always carry content, often "") must
|
|
361
|
+
// not wipe reasoning that precedes a tool call (#950 non-streaming path).
|
|
362
|
+
if (e.text.length > 0) rawReasoningForNextToolCall = "";
|
|
363
|
+
if (currentToolCallId) flushToolCall();
|
|
364
|
+
// Compaction turns keep the summary out of normal message output (replay dedup — see
|
|
365
|
+
// bridgeToResponsesSSE); it ships only inside the synthetic compaction item below.
|
|
366
|
+
if (options?.compaction) {
|
|
367
|
+
batchCompaction = appendBatchString(
|
|
368
|
+
batchCompaction, e.text, "retained_collectors",
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
if (e.phase !== undefined) currentTextPhase = e.phase;
|
|
373
|
+
currentText = appendBatchString(
|
|
374
|
+
currentText, e.text, "retained_collectors",
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
break;
|
|
378
|
+
case "thinking_delta":
|
|
379
|
+
if (currentText.bytes > 0) flushText("commentary");
|
|
380
|
+
if (currentRawReasoning.bytes > 0) flushRawReasoning();
|
|
381
|
+
if (e.thinking.length > 0) rawReasoningForNextToolCall = "";
|
|
382
|
+
if (currentToolCallId) flushToolCall();
|
|
383
|
+
{
|
|
384
|
+
currentSummaryReasoning = appendBatchString(
|
|
385
|
+
currentSummaryReasoning, e.thinking, "reasoning",
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
break;
|
|
389
|
+
case "thinking_signature":
|
|
390
|
+
// Like streaming, retain the latest signature update until the next semantic
|
|
391
|
+
// event. Flushing every update would manufacture signature-only siblings.
|
|
392
|
+
batchSignatureBytes = replaceBatchRetainedString(batchSignatureBytes, e.signature, "reasoning");
|
|
393
|
+
batchSignature = e.signature;
|
|
394
|
+
break;
|
|
395
|
+
case "redacted_thinking":
|
|
396
|
+
flushText("commentary");
|
|
397
|
+
flushSummaryReasoning();
|
|
398
|
+
flushRawReasoning();
|
|
399
|
+
flushToolCall();
|
|
400
|
+
{
|
|
401
|
+
const dataBytes = bytesOf(e.data);
|
|
402
|
+
budget?.chargeRetained(dataBytes, { kind: "reasoning" });
|
|
403
|
+
batchRedactedBytes += dataBytes;
|
|
404
|
+
}
|
|
405
|
+
batchRedacted.push(e.data);
|
|
406
|
+
flushSummaryReasoning();
|
|
407
|
+
break;
|
|
408
|
+
case "kiro_redacted_reasoning":
|
|
409
|
+
// Stash only — pushed after the trailing flushes. One blob per turn, so last wins.
|
|
410
|
+
{
|
|
411
|
+
const dataBytes = bytesOf(e.data);
|
|
412
|
+
budget?.chargeRetained(dataBytes, { kind: "reasoning" });
|
|
413
|
+
if (batchKiroRedactedBytes > 0) budget?.releaseRetained(batchKiroRedactedBytes, { kind: "reasoning" });
|
|
414
|
+
batchKiroRedactedBytes = dataBytes;
|
|
415
|
+
}
|
|
416
|
+
batchKiroRedacted = e.data;
|
|
417
|
+
break;
|
|
418
|
+
case "reasoning_raw_delta":
|
|
419
|
+
if (currentText.bytes > 0) flushText("commentary");
|
|
420
|
+
if (currentSummaryReasoning.bytes > 0) flushSummaryReasoning();
|
|
421
|
+
if (currentToolCallId) flushToolCall();
|
|
422
|
+
{
|
|
423
|
+
currentRawReasoning = appendBatchString(
|
|
424
|
+
currentRawReasoning, e.text, "reasoning",
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
break;
|
|
428
|
+
case "tool_call_start": {
|
|
429
|
+
if (currentText.bytes > 0) flushText("commentary");
|
|
430
|
+
if (currentSummaryReasoning.bytes > 0) flushSummaryReasoning();
|
|
431
|
+
if (currentRawReasoning.bytes > 0) flushRawReasoning();
|
|
432
|
+
if (rawReasoningForNextToolCall) {
|
|
433
|
+
rememberReasoningForCall(e.id, rawReasoningForNextToolCall, replayCacheScope);
|
|
434
|
+
}
|
|
435
|
+
flushToolCall();
|
|
436
|
+
const effectiveName = normalizeDeclaredToolName(e.name, options?.declaredToolNames);
|
|
437
|
+
if (
|
|
438
|
+
options?.declaredToolNames
|
|
439
|
+
&& options.enforceDeclaredToolNames !== false
|
|
440
|
+
&& !options.declaredToolNames.has(effectiveName)
|
|
441
|
+
) {
|
|
442
|
+
errorEvent = {
|
|
443
|
+
type: "error",
|
|
444
|
+
message: `routed provider emitted undeclared client tool "${effectiveName}"; only request-declared tools may be called`,
|
|
445
|
+
status: 502,
|
|
446
|
+
errorType: "upstream_error",
|
|
447
|
+
};
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
currentToolCallId = e.id;
|
|
451
|
+
budget?.openCall(e.id);
|
|
452
|
+
currentToolCallName = effectiveName;
|
|
453
|
+
currentToolCallCodeModeHelperName = effectiveName === "exec" && e.name !== effectiveName
|
|
454
|
+
? e.name
|
|
455
|
+
: undefined;
|
|
456
|
+
currentToolCallArgs = "";
|
|
457
|
+
currentToolCallArgsBytes = 0;
|
|
458
|
+
currentToolCallProviderMetadata = e.providerMetadata;
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
case "tool_call_delta":
|
|
462
|
+
{
|
|
463
|
+
({ value: currentToolCallArgs, bytes: currentToolCallArgsBytes } = appendBatchStringDirect(
|
|
464
|
+
currentToolCallArgs, currentToolCallArgsBytes, e.arguments, "tool_args", currentToolCallId,
|
|
465
|
+
));
|
|
466
|
+
}
|
|
467
|
+
break;
|
|
468
|
+
case "tool_call_end":
|
|
469
|
+
if (!toolCallArgumentsUsable(currentToolCallArgs) && currentToolCallId) {
|
|
470
|
+
// Mirror the streaming path: refuse to complete unusable arguments.
|
|
471
|
+
const mapped = options?.toolNsMap?.get(currentToolCallName);
|
|
472
|
+
const realName = mapped?.name ?? currentToolCallName;
|
|
473
|
+
const toolSearch = options?.toolSearchToolNames?.has(realName) ?? false;
|
|
474
|
+
const freeform = !toolSearch && (mapped
|
|
475
|
+
? mapped.freeform === true
|
|
476
|
+
: (options?.freeformToolNames?.has(realName) ?? false));
|
|
477
|
+
if (!freeform && !toolSearch) {
|
|
478
|
+
flushToolCall("incomplete");
|
|
479
|
+
errorEvent = {
|
|
480
|
+
type: "error",
|
|
481
|
+
message: "upstream stream produced malformed tool call arguments",
|
|
482
|
+
status: 502,
|
|
483
|
+
errorType: "upstream_error",
|
|
484
|
+
};
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
flushToolCall();
|
|
489
|
+
break;
|
|
490
|
+
case "web_search_call_begin":
|
|
491
|
+
// Batch/non-streaming output has no in_progress phase to animate — the search cell is a
|
|
492
|
+
// single finalized item, emitted on `end`. Begin is a no-op here.
|
|
493
|
+
break;
|
|
494
|
+
case "web_search_call_end": {
|
|
495
|
+
if (currentText.bytes > 0) flushText("commentary");
|
|
496
|
+
if (currentSummaryReasoning.bytes > 0) flushSummaryReasoning();
|
|
497
|
+
if (currentRawReasoning.bytes > 0) flushRawReasoning();
|
|
498
|
+
flushToolCall();
|
|
499
|
+
const safeSources = safeWebSearchSources(e.sources);
|
|
500
|
+
pushOutput({
|
|
501
|
+
type: "web_search_call", id: `ws_${uuid()}`, status: e.status ?? "completed",
|
|
502
|
+
action: webSearchAction(e.queries),
|
|
503
|
+
...(safeSources.length > 0 ? { sources: safeSources } : {}),
|
|
504
|
+
});
|
|
505
|
+
if (safeSources.length > 0) {
|
|
506
|
+
for (const source of safeSources) {
|
|
507
|
+
if (appendSafeWebSearchSource(pendingWebSources, source)) {
|
|
508
|
+
budget?.chargeRetained(bytesOf(JSON.stringify(source)), { kind: "tool_search_sources" });
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
case "error":
|
|
515
|
+
errorEvent = e;
|
|
516
|
+
sawTerminal = true;
|
|
517
|
+
usage = e.usage ?? usage;
|
|
518
|
+
break;
|
|
519
|
+
case "incomplete":
|
|
520
|
+
incompleteEvent = e;
|
|
521
|
+
sawTerminal = true;
|
|
522
|
+
endTurn = e.endTurn;
|
|
523
|
+
if (e.providerState) options?.onProviderState?.(e.providerState);
|
|
524
|
+
break;
|
|
525
|
+
case "done":
|
|
526
|
+
usage = e.usage;
|
|
527
|
+
compactionEncryptedContent = e.compactionEncryptedContent;
|
|
528
|
+
sawTerminal = true;
|
|
529
|
+
endTurn = e.endTurn;
|
|
530
|
+
cleanDone = e.stopReason === undefined;
|
|
531
|
+
rawStopReason = e.stopReason;
|
|
532
|
+
if (e.providerState) options?.onProviderState?.(e.providerState);
|
|
533
|
+
// Match streaming: max_tokens and content_filter both terminate as incomplete.
|
|
534
|
+
// Normalize every adapter's truncation vocabulary to the canonical pair, so a raw
|
|
535
|
+
// `length` or `refusal` reaches the status/incomplete_details logic below instead of
|
|
536
|
+
// silently reading as a clean stop.
|
|
537
|
+
{
|
|
538
|
+
const truncation = truncationReasonFor(e.stopReason);
|
|
539
|
+
if (truncation) stopReason = truncation === "max_output_tokens" ? "max_tokens" : "content_filter";
|
|
540
|
+
}
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
if (budget) releaseTranslatedEvent(e, budget);
|
|
544
|
+
}
|
|
545
|
+
flushText(cleanDone && !errorEvent && !incompleteEvent ? "final_answer" : undefined);
|
|
546
|
+
if (pendingWebSources.length > 0) {
|
|
547
|
+
const sourceBytes = pendingWebSources.reduce((sum, source) => sum + bytesOf(JSON.stringify(source)), 0);
|
|
548
|
+
pendingWebSources = [];
|
|
549
|
+
budget?.releaseRetained(sourceBytes, { kind: "tool_search_sources" });
|
|
550
|
+
}
|
|
551
|
+
flushSummaryReasoning();
|
|
552
|
+
flushRawReasoning();
|
|
553
|
+
// Open tool call on a failed/incomplete turn must not land as status:"completed" — and neither
|
|
554
|
+
// must one left open by a stream that stopped without any terminal at all. That case previously
|
|
555
|
+
// fell through to "completed", handing back a function_call whose arguments were half-written
|
|
556
|
+
// JSON, inside a turn also marked completed.
|
|
557
|
+
if (currentToolCallId) {
|
|
558
|
+
flushToolCall(errorEvent || incompleteEvent || !sawTerminal || isTruncatedStopReason(rawStopReason)
|
|
559
|
+
? "incomplete" : "completed");
|
|
560
|
+
}
|
|
561
|
+
if (batchKiroRedacted) {
|
|
562
|
+
// pushOutput reserves the item itself and releases the retained raw blob it replaces.
|
|
563
|
+
pushOutput({
|
|
564
|
+
type: "reasoning", id: `rs_${uuid()}`, summary: [],
|
|
565
|
+
encrypted_content: encodeReasoningEnvelope({ krc: batchKiroRedacted }, budget),
|
|
566
|
+
}, batchKiroRedactedBytes, "reasoning");
|
|
567
|
+
batchKiroRedacted = undefined;
|
|
568
|
+
batchKiroRedactedBytes = 0;
|
|
569
|
+
}
|
|
570
|
+
// A truncated turn must never be installed as replacement history: emit the
|
|
571
|
+
// compaction item only when the turn actually completed (#422).
|
|
572
|
+
if (
|
|
573
|
+
options?.compaction
|
|
574
|
+
&& !errorEvent
|
|
575
|
+
&& !incompleteEvent
|
|
576
|
+
// A stream that stopped without any terminal did not complete either. The original guard
|
|
577
|
+
// could only see explicit failure events, so an adapter EOF slipped past it and installed a
|
|
578
|
+
// truncated summary as replacement history — the exact #422 hazard, reached by a route that
|
|
579
|
+
// did not exist when the guard was written.
|
|
580
|
+
&& sawTerminal
|
|
581
|
+
&& !isTruncatedStopReason(rawStopReason)
|
|
582
|
+
) {
|
|
583
|
+
const item = {
|
|
584
|
+
type: "compaction", id: `cmp_${uuid()}`,
|
|
585
|
+
encrypted_content: compactionEncryptedContent ?? encodeCompactionSummary(joinChunks(batchCompaction)),
|
|
586
|
+
};
|
|
587
|
+
pushOutput(item, compactionEncryptedContent ? bytesOf(compactionEncryptedContent) : batchCompaction.bytes);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const failure = errorEvent ? adapterFailureFromEvent(errorEvent) : undefined;
|
|
591
|
+
const status = errorEvent
|
|
592
|
+
? "failed"
|
|
593
|
+
: incompleteEvent || stopReason === "max_tokens" || stopReason === "content_filter"
|
|
594
|
+
? "incomplete"
|
|
595
|
+
: sawTerminal
|
|
596
|
+
? "completed"
|
|
597
|
+
// The adapter stopped emitting without any terminal, so the turn was cut short. Streaming
|
|
598
|
+
// already reports this as response.incomplete / adapter_eof (see the !terminated branch);
|
|
599
|
+
// defaulting the buffered path to "completed" handed callers a truncated turn — including
|
|
600
|
+
// one carrying a never-closed tool call with half-written JSON arguments — as a success.
|
|
601
|
+
: "incomplete";
|
|
602
|
+
options?.onUsage?.(incompleteEvent?.usage ?? usage);
|
|
603
|
+
return {
|
|
604
|
+
id: responseId, object: "response",
|
|
605
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
606
|
+
status,
|
|
607
|
+
model: modelId, output,
|
|
608
|
+
...(endTurn !== undefined ? { end_turn: endTurn } : {}),
|
|
609
|
+
...(failure ? { error: failure.error, last_error: failure.error } : {}),
|
|
610
|
+
...(failure && isCyberPolicyCode(failure.error.code)
|
|
611
|
+
? { retryable: false }
|
|
612
|
+
: errorEvent?.retryable !== undefined ? { retryable: errorEvent.retryable } : {}),
|
|
613
|
+
...(incompleteEvent ? {
|
|
614
|
+
incomplete_details: {
|
|
615
|
+
reason: incompleteEvent.reason,
|
|
616
|
+
...(incompleteEvent.message ? { message: incompleteEvent.message } : {}),
|
|
617
|
+
...(incompleteEvent.retryable !== undefined ? { retryable: incompleteEvent.retryable } : {}),
|
|
618
|
+
},
|
|
619
|
+
} : stopReason === "max_tokens" ? {
|
|
620
|
+
incomplete_details: { reason: "max_output_tokens" },
|
|
621
|
+
} : stopReason === "content_filter" ? {
|
|
622
|
+
incomplete_details: { reason: "content_filter" },
|
|
623
|
+
} : !sawTerminal ? {
|
|
624
|
+
// Same reason string the streaming path uses, so a caller sees one signal for one condition
|
|
625
|
+
// regardless of which surface it asked for.
|
|
626
|
+
incomplete_details: { reason: "adapter_eof" },
|
|
627
|
+
} : {}),
|
|
628
|
+
usage: responsesUsage(incompleteEvent?.usage ?? usage),
|
|
629
|
+
};
|
|
630
|
+
}
|