@bitkyc08/opencodex 2.65.0 → 2.66.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/README.md +14 -0
- package/gui/dist/assets/App-DyHlFeNB.js +51 -0
- package/gui/dist/assets/App-gkoJIEOV.css +1 -0
- package/gui/dist/assets/{Tray-D4bA1vff.js → Tray-CI8cVKWu.js} +1 -1
- package/gui/dist/assets/index-BiwnVAUm.js +86 -0
- package/gui/dist/assets/index-ComVsdSk.css +1 -0
- package/gui/dist/assets/{tray-data-BZXhB3qz.js → tray-data-_WyYzbMt.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +80 -0
- package/src/adapters/anthropic/passthrough.ts +221 -0
- package/src/adapters/anthropic-image-codec.ts +56 -19
- package/src/adapters/anthropic-image-normalize.ts +20 -10
- package/src/adapters/anthropic.ts +55 -24
- package/src/adapters/coding-agent/protocol.ts +29 -19
- package/src/adapters/cursor/live-transport.ts +18 -3
- package/src/adapters/cursor/native-exec-shell.ts +18 -63
- package/src/adapters/cursor/native-exec.ts +5 -2
- package/src/adapters/cursor/native-foreground-shell.ts +44 -0
- package/src/adapters/devin/cloud-direct/chat.ts +7 -1
- package/src/adapters/exec-tool-result-normalize.ts +4 -1
- package/src/adapters/google.ts +12 -0
- package/src/adapters/ollama-native.ts +32 -1
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +131 -34
- package/src/adapters/openai-chat.ts +6 -4
- package/src/adapters/openai-responses/passthrough.ts +1 -0
- package/src/adapters/openai-responses/reasoning.ts +45 -1
- package/src/bridge/response-json.ts +10 -2
- package/src/chat/outbound.ts +104 -52
- package/src/claude/claude-code-block.ts +16 -0
- package/src/claude/context-windows.ts +52 -4
- package/src/claude/desktop-first-party.ts +25 -8
- package/src/claude/inbound.ts +22 -0
- package/src/claude/intercept/connect-proxy.ts +37 -4
- package/src/claude/intercept/proxy-auth.ts +166 -0
- package/src/claude/intercept/runtime.ts +21 -0
- package/src/claude/intercept/settings.ts +33 -12
- package/src/claude/outbound.ts +45 -27
- package/src/cli/account-extended.ts +47 -15
- package/src/cli/account-target.ts +115 -0
- package/src/cli/account.ts +20 -28
- package/src/cli/agent.ts +10 -1
- package/src/cli/api-protocols.ts +206 -0
- package/src/cli/capabilities.ts +86 -0
- package/src/cli/combo.ts +2 -2
- package/src/cli/connect.ts +127 -3
- package/src/cli/dispatch.ts +8 -0
- package/src/cli/doctor.ts +33 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/link.ts +288 -0
- package/src/cli/registry.ts +23 -0
- package/src/cli/runtime-api.ts +79 -0
- package/src/cli/system-command.ts +21 -2
- package/src/client/connect.ts +125 -23
- package/src/client/hub-relay.ts +12 -8
- package/src/client/link-join.ts +323 -0
- package/src/client/link-relay.ts +241 -0
- package/src/client/link-state.ts +110 -0
- package/src/client/link-teardown.ts +63 -0
- package/src/client/link-tunnel.ts +465 -0
- package/src/client/machine-listener.ts +14 -5
- package/src/client/runtime.ts +91 -42
- package/src/client/state.ts +4 -0
- package/src/codex/catalog/build-entries.ts +4 -0
- package/src/codex/catalog/derive-entry.ts +5 -0
- package/src/codex/catalog/parsing.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +4 -1
- package/src/codex/desktop-switches.ts +88 -8
- package/src/codex/inject/plan.ts +16 -3
- package/src/codex/inject.ts +3 -0
- package/src/codex/internal/catalog-writer.ts +13 -2
- package/src/combos/failover.ts +3 -2
- package/src/combos/index.ts +12 -0
- package/src/combos/jev.ts +646 -0
- package/src/combos/request.ts +5 -0
- package/src/combos/resolve.ts +17 -15
- package/src/combos/types.ts +47 -3
- package/src/config/atomic-write.ts +3 -0
- package/src/config/live-reconcile.ts +20 -4
- package/src/config/persist-unlocked.ts +20 -7
- package/src/config/schema/config-schema.ts +16 -0
- package/src/config/schema/leaf-validators.ts +38 -1
- package/src/generated/compatibility-version.json +416 -128
- package/src/integrations/omp-yaml-source.ts +109 -4
- package/src/lab/automation/orchestrator.ts +60 -4
- package/src/lab/events/limits.ts +1 -1
- package/src/lib/bounded-body.ts +55 -38
- package/src/lib/config-ownership.ts +11 -22
- package/src/lib/local-desktop-snapshot-capability.ts +56 -0
- package/src/lib/local-management-capability.ts +25 -3
- package/src/lib/optional-shutdown-hooks.ts +17 -0
- package/src/lib/state-store-registrations.ts +2 -2
- package/src/lib/windows-elevation.ts +62 -25
- package/src/lib/windows-secret-acl.ts +114 -23
- package/src/link/admission-wait.ts +73 -0
- package/src/link/compensation.ts +100 -0
- package/src/link/fingerprint.ts +21 -0
- package/src/link/paths.ts +19 -0
- package/src/link/ports.ts +12 -0
- package/src/link/routes.ts +32 -0
- package/src/link/ssh-argv.ts +170 -0
- package/src/link/ssh-config.ts +152 -0
- package/src/link/ssh-runner.ts +161 -0
- package/src/link/status-projection.ts +96 -0
- package/src/link/store.ts +139 -0
- package/src/link/supervisor.ts +404 -0
- package/src/link/tunnel-state.ts +91 -0
- package/src/protocols/baseline.ts +88 -0
- package/src/protocols/codecs/chat.ts +17 -0
- package/src/protocols/codecs/messages.ts +18 -0
- package/src/protocols/codecs/responses.ts +17 -0
- package/src/protocols/contract.ts +185 -0
- package/src/protocols/dto.ts +267 -0
- package/src/protocols/encoders/adapter-events.ts +876 -0
- package/src/protocols/encoders/chat.ts +243 -0
- package/src/protocols/encoders/messages.ts +462 -0
- package/src/protocols/envelope.ts +43 -0
- package/src/protocols/features.ts +295 -0
- package/src/protocols/guard.ts +33 -0
- package/src/protocols/opaque-state.ts +132 -0
- package/src/protocols/path.ts +39 -0
- package/src/protocols/plan-snapshot.ts +226 -0
- package/src/protocols/plan.ts +192 -0
- package/src/protocols/provider-summary.ts +101 -0
- package/src/protocols/settings.ts +118 -0
- package/src/protocols/shadow-plan.ts +36 -0
- package/src/protocols/shadow.ts +62 -0
- package/src/protocols/trace.ts +335 -0
- package/src/providers/model-rename-migration.ts +3 -3
- package/src/providers/registry/entries-core.ts +6 -1
- package/src/providers/registry/entries-extended.ts +15 -0
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/types.ts +6 -0
- package/src/providers/registry.ts +1 -1
- package/src/remote-control/workspace-hub.ts +5 -4
- package/src/responses/apply-patch-envelope.ts +6 -1
- package/src/responses/code-mode-shell-input.ts +4 -2
- package/src/responses/freeform-wrapper-scan.ts +51 -24
- package/src/responses/progressive-freeform-input.ts +14 -20
- package/src/responses/spill-store.ts +187 -33
- package/src/responses/state/snapshot-select.ts +49 -0
- package/src/responses/state/spill-inspect.ts +46 -0
- package/src/responses/state/spill-queue.ts +16 -0
- package/src/responses/state.ts +51 -27
- package/src/server/audio-client.ts +8 -3
- package/src/server/audio-upstream.ts +9 -4
- package/src/server/auth-cors.ts +25 -8
- package/src/server/chat-completions.ts +71 -11
- package/src/server/chat-native-eligibility.ts +74 -0
- package/src/server/chat-native.ts +139 -74
- package/src/server/claude-messages.ts +140 -22
- package/src/server/hub-usage.ts +4 -3
- package/src/server/index/link-listener.ts +195 -0
- package/src/server/index/optional-listeners.ts +115 -0
- package/src/server/index/serve-options.ts +44 -10
- package/src/server/index.ts +10 -9
- package/src/server/inference/attempt.ts +51 -0
- package/src/server/inference/client-encoder-delivery.ts +235 -0
- package/src/server/inference/client-wire-log.ts +49 -0
- package/src/server/inference/client-wire.ts +69 -0
- package/src/server/inference/context.ts +15 -0
- package/src/server/inference/final-log.ts +37 -0
- package/src/server/local-desktop-snapshot-auth.ts +57 -0
- package/src/server/management/agent-settings-routes.ts +15 -13
- package/src/server/management/api-access.ts +11 -1
- package/src/server/management/config-routes.ts +3 -6
- package/src/server/management/context.ts +25 -0
- package/src/server/management/link-routes.ts +535 -0
- package/src/server/management/logs-usage-routes.ts +31 -1
- package/src/server/management/native-integration-routes.ts +6 -11
- package/src/server/management/oauth-account-routes.ts +42 -16
- package/src/server/management/protocol-routes.ts +159 -0
- package/src/server/management/protocol-settings-patch.ts +145 -0
- package/src/server/management/provider-patch-transaction.ts +61 -0
- package/src/server/management/provider-routes.ts +39 -18
- package/src/server/management/route-registry.ts +12 -0
- package/src/server/management/sidebar-routes.ts +8 -3
- package/src/server/management/system-routes.ts +18 -1
- package/src/server/management/usage-aggregate-cache.ts +206 -6
- package/src/server/management-api.ts +26 -1
- package/src/server/management-auth.ts +19 -5
- package/src/server/messages-native-eligibility.ts +161 -0
- package/src/server/messages-native-oauth.ts +149 -0
- package/src/server/messages-native.ts +790 -0
- package/src/server/relay.ts +9 -0
- package/src/server/request-log-filter.ts +92 -0
- package/src/server/request-log.ts +31 -70
- package/src/server/responses/adapter-delivery.ts +43 -16
- package/src/server/responses/agent-task-recovery.ts +90 -22
- package/src/server/responses/codex-ws-exchange.ts +4 -0
- package/src/server/responses/codex-ws-wire.ts +10 -1
- package/src/server/responses/core-combo-native.ts +323 -0
- package/src/server/responses/core-combo.ts +224 -13
- package/src/server/responses/core-options.ts +22 -0
- package/src/server/responses/core.ts +14 -26
- package/src/server/responses/request-sidecar-auth.ts +8 -3
- package/src/server/responses/run-turn-execution.ts +257 -63
- package/src/server/responses/sidecar-execution.ts +7 -4
- package/src/server/system-env.ts +7 -0
- package/src/service/diagnostics.ts +7 -1
- package/src/service/launchd.ts +26 -37
- package/src/service/windows-ops.ts +24 -20
- package/src/types/config.ts +58 -1
- package/src/types.ts +2 -0
- package/src/update/transactional-install.d.mts +0 -1
- package/src/update/transactional-install.mjs +17 -17
- package/src/usage/jev-stats.ts +495 -0
- package/src/usage/log.ts +24 -3
- package/src/web-search/run-turn-loop.ts +568 -0
- package/gui/dist/assets/App-8NMiZxT0.css +0 -1
- package/gui/dist/assets/App-DGLte6IR.js +0 -50
- package/gui/dist/assets/index--EWgGQvZ.css +0 -1
- package/gui/dist/assets/index-Bi1K37Wl.js +0 -86
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AdapterEvent -> Anthropic Messages, without the internal Responses SSE (PF-09).
|
|
3
|
+
*
|
|
4
|
+
* The writer reproduces `responsesSseToAnthropicSse` (src/claude/outbound.ts) for the frames the
|
|
5
|
+
* bridge produced: `message_start` + `ping` on the first semantic output (never before an initial
|
|
6
|
+
* error), text blocks per assistant message, thinking blocks buffered until their item closes
|
|
7
|
+
* and then emitted with the genuine signature or the bounded ocxr1 fallback, redacted blocks
|
|
8
|
+
* ahead of the thinking block they came with, tool_use blocks with streamed `input_json_delta`
|
|
9
|
+
* (WebSearch input buffered and sanitized), the server_tool_use / web_search_tool_result pair,
|
|
10
|
+
* `message_delta` + `message_stop`, and mid-stream `error` frames. Pings follow the bridge's
|
|
11
|
+
* wire-silence heartbeat and the converter's 20 s keepalive, only while the client has demand.
|
|
12
|
+
*/
|
|
13
|
+
import type { AdapterEvent } from "../../types";
|
|
14
|
+
import {
|
|
15
|
+
isTranslatorBudgetExceededError,
|
|
16
|
+
type TranslatorBudget,
|
|
17
|
+
} from "../../lib/translator-budget";
|
|
18
|
+
import { isTransientUpstreamStatus } from "../../lib/upstream-retry";
|
|
19
|
+
import { encodeReasoningEnvelope } from "../../responses/reasoning-envelope";
|
|
20
|
+
import { responsesUsage, webSearchAction } from "../../bridge/internal";
|
|
21
|
+
import {
|
|
22
|
+
anthropicErrorBody,
|
|
23
|
+
anthropicErrorResponse,
|
|
24
|
+
anthropicFailedStatus,
|
|
25
|
+
anthropicIncompleteOutcome,
|
|
26
|
+
anthropicUsage,
|
|
27
|
+
boundedReasoningIdentity,
|
|
28
|
+
collectAnthropicMessage,
|
|
29
|
+
isClaudeWebSearchToolName,
|
|
30
|
+
messageSnapshot,
|
|
31
|
+
sanitizeWebSearchInput,
|
|
32
|
+
sseFrame,
|
|
33
|
+
webSearchPairFromItem,
|
|
34
|
+
} from "../../claude/outbound";
|
|
35
|
+
import {
|
|
36
|
+
encodeAdapterEventStream,
|
|
37
|
+
type AdapterEventEncodeOptions,
|
|
38
|
+
type ClientFrameSink,
|
|
39
|
+
type ClientWireWriter,
|
|
40
|
+
} from "./adapter-events";
|
|
41
|
+
import type { RelayedEventObservation } from "../../usage/attempt-delivery";
|
|
42
|
+
|
|
43
|
+
type Rec = Record<string, unknown>;
|
|
44
|
+
|
|
45
|
+
/** The converter's transport keepalive interval. */
|
|
46
|
+
const MESSAGES_KEEPALIVE_MS = 20_000;
|
|
47
|
+
|
|
48
|
+
export interface AnthropicMessageEncodeOptions extends AdapterEventEncodeOptions {
|
|
49
|
+
/** The model string the client asked for; `message_start` and the folded message carry it. */
|
|
50
|
+
model: string;
|
|
51
|
+
/** This proxy's prompt estimate for `message_start` when no usage arrived first (#4857). */
|
|
52
|
+
inputTokenFloor?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface OpenBlock {
|
|
56
|
+
kind: "text" | "thinking" | "tool_use";
|
|
57
|
+
index: number;
|
|
58
|
+
bufferWebSearchArgs?: boolean;
|
|
59
|
+
argsBuf?: string;
|
|
60
|
+
argsBufBytes?: number;
|
|
61
|
+
webSearchArgsEmitted?: boolean;
|
|
62
|
+
toolArgsEmitted?: boolean;
|
|
63
|
+
callId?: string;
|
|
64
|
+
reasoningPartKey?: string;
|
|
65
|
+
reasoningItemKey?: string;
|
|
66
|
+
thinkingBuf?: string;
|
|
67
|
+
thinkingBufBytes?: number;
|
|
68
|
+
reasoningSig?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function appendedUtf8Bytes(previous: string, previousBytes: number, fragment: string): number {
|
|
72
|
+
let nextBytes = previousBytes + Buffer.byteLength(fragment);
|
|
73
|
+
const previousLast = previous.charCodeAt(previous.length - 1);
|
|
74
|
+
const fragmentFirst = fragment.charCodeAt(0);
|
|
75
|
+
if (previousLast >= 0xd800 && previousLast <= 0xdbff
|
|
76
|
+
&& fragmentFirst >= 0xdc00 && fragmentFirst <= 0xdfff) {
|
|
77
|
+
nextBytes -= 2;
|
|
78
|
+
}
|
|
79
|
+
return nextBytes;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function createAnthropicMessageWriter(
|
|
83
|
+
sink: ClientFrameSink,
|
|
84
|
+
model: string,
|
|
85
|
+
inputTokenFloor: number | undefined,
|
|
86
|
+
): ClientWireWriter {
|
|
87
|
+
const budget = sink.budget;
|
|
88
|
+
let started = false;
|
|
89
|
+
let terminated = false;
|
|
90
|
+
// Only a delivered terminal forbids the bounded overflow error.
|
|
91
|
+
let terminalDelivered = false;
|
|
92
|
+
let blockIndex = 0;
|
|
93
|
+
let open: OpenBlock | null = null;
|
|
94
|
+
let sawToolUse = false;
|
|
95
|
+
let webSearchRequests = 0;
|
|
96
|
+
|
|
97
|
+
const emit = (name: string, data: Rec, observation?: RelayedEventObservation) => sink.emit(sseFrame(name, data), observation);
|
|
98
|
+
const releaseThinkingBuffer = (block: OpenBlock | null) => {
|
|
99
|
+
if (block?.kind !== "thinking") return;
|
|
100
|
+
budget.releaseRetained(block.thinkingBufBytes ?? 0, { kind: "reasoning" });
|
|
101
|
+
block.thinkingBufBytes = 0;
|
|
102
|
+
};
|
|
103
|
+
const appendRetained = (previous: string, previousBytes: number, fragment: string, scope: { kind: "reasoning" | "tool_args"; callId?: string }) => {
|
|
104
|
+
const nextBytes = appendedUtf8Bytes(previous, previousBytes, fragment);
|
|
105
|
+
const reservation = budget.reserveTransient(nextBytes, scope);
|
|
106
|
+
try {
|
|
107
|
+
const value = previous + fragment;
|
|
108
|
+
reservation.commitRetained();
|
|
109
|
+
budget.releaseRetained(previousBytes, scope);
|
|
110
|
+
return { value, bytes: nextBytes };
|
|
111
|
+
} catch (error) {
|
|
112
|
+
reservation.release();
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const ensureStarted = () => {
|
|
117
|
+
if (started) return;
|
|
118
|
+
started = true;
|
|
119
|
+
emit("message_start", { type: "message_start", message: messageSnapshot(model, undefined, inputTokenFloor) });
|
|
120
|
+
emit("ping", { type: "ping" });
|
|
121
|
+
};
|
|
122
|
+
const closeOpenBlock = () => {
|
|
123
|
+
if (!open) return;
|
|
124
|
+
if (open.kind === "tool_use" && open.bufferWebSearchArgs && !open.webSearchArgsEmitted) {
|
|
125
|
+
let parsed: unknown = {};
|
|
126
|
+
const rawArgs = open.argsBuf ?? "";
|
|
127
|
+
try { parsed = rawArgs.length > 0 ? JSON.parse(rawArgs) : {}; } catch { parsed = {}; }
|
|
128
|
+
emit("content_block_delta", {
|
|
129
|
+
type: "content_block_delta",
|
|
130
|
+
index: open.index,
|
|
131
|
+
delta: { type: "input_json_delta", partial_json: JSON.stringify(sanitizeWebSearchInput(parsed)) },
|
|
132
|
+
});
|
|
133
|
+
open.webSearchArgsEmitted = true;
|
|
134
|
+
}
|
|
135
|
+
if (open.kind === "thinking") {
|
|
136
|
+
// The index and every thinking frame wait for closure, so redacted blocks from the
|
|
137
|
+
// same item can go first.
|
|
138
|
+
open.index = blockIndex++;
|
|
139
|
+
emit("content_block_start", {
|
|
140
|
+
type: "content_block_start", index: open.index,
|
|
141
|
+
content_block: { type: "thinking", thinking: "", signature: "" },
|
|
142
|
+
});
|
|
143
|
+
if (open.thinkingBuf) {
|
|
144
|
+
emit("content_block_delta", {
|
|
145
|
+
type: "content_block_delta", index: open.index,
|
|
146
|
+
delta: { type: "thinking_delta", thinking: open.thinkingBuf },
|
|
147
|
+
}, { semanticBytes: Buffer.byteLength(open.thinkingBuf) });
|
|
148
|
+
}
|
|
149
|
+
const signature = open.reasoningSig ?? encodeReasoningEnvelope({ txt: open.thinkingBuf ?? "" }, budget);
|
|
150
|
+
emit("content_block_delta", {
|
|
151
|
+
type: "content_block_delta", index: open.index,
|
|
152
|
+
delta: { type: "signature_delta", signature },
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
emit("content_block_stop", { type: "content_block_stop", index: open.index });
|
|
156
|
+
releaseThinkingBuffer(open);
|
|
157
|
+
if (open.callId) budget.closeCall(open.callId);
|
|
158
|
+
open = null;
|
|
159
|
+
};
|
|
160
|
+
const ensureBlock = (kind: "text" | "thinking") => {
|
|
161
|
+
ensureStarted();
|
|
162
|
+
if (open && open.kind === kind) return;
|
|
163
|
+
closeOpenBlock();
|
|
164
|
+
if (kind === "thinking") {
|
|
165
|
+
open = { kind, index: -1, thinkingBuf: "", thinkingBufBytes: 0 };
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const index = blockIndex++;
|
|
169
|
+
emit("content_block_start", { type: "content_block_start", index, content_block: { type: "text", text: "" } });
|
|
170
|
+
open = { kind, index };
|
|
171
|
+
};
|
|
172
|
+
const finish = (stopReason: string, usage: unknown) => {
|
|
173
|
+
if (terminated) return;
|
|
174
|
+
terminated = true;
|
|
175
|
+
ensureStarted();
|
|
176
|
+
closeOpenBlock();
|
|
177
|
+
emit("message_delta", {
|
|
178
|
+
type: "message_delta",
|
|
179
|
+
delta: { stop_reason: stopReason, stop_sequence: null },
|
|
180
|
+
usage: anthropicUsage(usage, webSearchRequests),
|
|
181
|
+
});
|
|
182
|
+
emit("message_stop", { type: "message_stop" }, { terminal: true });
|
|
183
|
+
terminalDelivered = true;
|
|
184
|
+
};
|
|
185
|
+
// Transient upstream statuses become overloaded_error so Anthropic SDK clients back off.
|
|
186
|
+
const fail = (status: number, message: string, upstreamDerived = false, code?: string) => {
|
|
187
|
+
if (terminated && (code !== "translation_buffer_limit" || terminalDelivered)) return;
|
|
188
|
+
terminated = true;
|
|
189
|
+
if (code === "translation_buffer_limit") {
|
|
190
|
+
releaseThinkingBuffer(open);
|
|
191
|
+
if (open?.callId) budget.closeCall(open.callId);
|
|
192
|
+
open = null;
|
|
193
|
+
terminalDelivered = true;
|
|
194
|
+
// No normal close frames are valid after overflow: one bounded typed terminal.
|
|
195
|
+
sink.emitBounded(sseFrame("error", anthropicErrorBody(413, message, "request_too_large", "translation_buffer_limit")));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const type = upstreamDerived && isTransientUpstreamStatus(status) ? "overloaded_error" : undefined;
|
|
199
|
+
// An initial failure is an error stream, not a partial message: no message_start first.
|
|
200
|
+
if (started) closeOpenBlock();
|
|
201
|
+
emit("error", anthropicErrorBody(status, message, type, code), { terminal: true });
|
|
202
|
+
terminalDelivered = true;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
start() { /* the bridge's lifecycle preludes carry no usage, so nothing starts here */ },
|
|
207
|
+
heartbeat() {
|
|
208
|
+
if (terminated || sink.desiredSize() <= 0) return;
|
|
209
|
+
sink.emitKeepalive(sseFrame("ping", { type: "ping" }));
|
|
210
|
+
},
|
|
211
|
+
text(delta) {
|
|
212
|
+
if (!delta) return;
|
|
213
|
+
ensureBlock("text");
|
|
214
|
+
const active = open;
|
|
215
|
+
if (!active || active.kind !== "text") return;
|
|
216
|
+
emit("content_block_delta", {
|
|
217
|
+
type: "content_block_delta", index: active.index,
|
|
218
|
+
delta: { type: "text_delta", text: delta },
|
|
219
|
+
}, { semanticBytes: Buffer.byteLength(delta) });
|
|
220
|
+
},
|
|
221
|
+
messageDone() {
|
|
222
|
+
if (open && open.kind === "text") closeOpenBlock();
|
|
223
|
+
},
|
|
224
|
+
reasoning(delta, itemId, channel) {
|
|
225
|
+
if (!delta) return;
|
|
226
|
+
const itemKey = boundedReasoningIdentity(itemId);
|
|
227
|
+
if (open?.kind === "thinking" && open.reasoningItemKey !== itemKey) closeOpenBlock();
|
|
228
|
+
ensureBlock("thinking");
|
|
229
|
+
const active = open as OpenBlock | null;
|
|
230
|
+
if (!active || active.kind !== "thinking") return;
|
|
231
|
+
// The bridge writes summary deltas at summary_index 0 and raw deltas at content_index 0.
|
|
232
|
+
const slot = channel === "summary" ? `s${boundedReasoningIdentity(0)}` : `c${boundedReasoningIdentity(0)}`;
|
|
233
|
+
const partKey = `${itemKey}:${slot}`;
|
|
234
|
+
const needsPartSeparator = active.reasoningPartKey !== undefined && active.reasoningPartKey !== partKey;
|
|
235
|
+
const next = appendRetained(
|
|
236
|
+
active.thinkingBuf ?? "",
|
|
237
|
+
active.thinkingBufBytes ?? 0,
|
|
238
|
+
`${needsPartSeparator ? "\n\n" : ""}${delta}`,
|
|
239
|
+
{ kind: "reasoning" },
|
|
240
|
+
);
|
|
241
|
+
active.thinkingBuf = next.value;
|
|
242
|
+
active.thinkingBufBytes = next.bytes;
|
|
243
|
+
active.reasoningItemKey = itemKey;
|
|
244
|
+
active.reasoningPartKey = partKey;
|
|
245
|
+
},
|
|
246
|
+
reasoningDone({ itemId, signature, redacted }) {
|
|
247
|
+
const red = redacted ?? [];
|
|
248
|
+
const itemKey = boundedReasoningIdentity(itemId);
|
|
249
|
+
// A late or unrelated item cannot reorder or sign another item's text.
|
|
250
|
+
if (open?.kind === "thinking" && open.reasoningItemKey !== itemKey) closeOpenBlock();
|
|
251
|
+
if (red.length > 0) {
|
|
252
|
+
ensureStarted();
|
|
253
|
+
if (open?.kind !== "thinking") closeOpenBlock();
|
|
254
|
+
}
|
|
255
|
+
for (const data of red) {
|
|
256
|
+
const index = blockIndex++;
|
|
257
|
+
emit("content_block_start", { type: "content_block_start", index, content_block: { type: "redacted_thinking", data } });
|
|
258
|
+
emit("content_block_stop", { type: "content_block_stop", index });
|
|
259
|
+
}
|
|
260
|
+
if (signature && open?.kind !== "thinking") ensureBlock("thinking");
|
|
261
|
+
const active = open as OpenBlock | null;
|
|
262
|
+
if (active?.kind === "thinking") {
|
|
263
|
+
if (signature) active.reasoningSig = signature;
|
|
264
|
+
closeOpenBlock();
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
toolStart(call) {
|
|
268
|
+
// Custom and tool-search calls have no Anthropic representation; the converter skips them.
|
|
269
|
+
if (call.kind !== "function") return;
|
|
270
|
+
ensureStarted();
|
|
271
|
+
closeOpenBlock();
|
|
272
|
+
sawToolUse = true;
|
|
273
|
+
const index = blockIndex++;
|
|
274
|
+
emit("content_block_start", {
|
|
275
|
+
type: "content_block_start", index,
|
|
276
|
+
content_block: { type: "tool_use", id: call.callId, name: call.name, input: {} },
|
|
277
|
+
}, { sideEffect: true });
|
|
278
|
+
budget.openCall(call.callId);
|
|
279
|
+
open = {
|
|
280
|
+
kind: "tool_use",
|
|
281
|
+
index,
|
|
282
|
+
callId: call.callId,
|
|
283
|
+
bufferWebSearchArgs: isClaudeWebSearchToolName(call.name),
|
|
284
|
+
argsBuf: "",
|
|
285
|
+
argsBufBytes: 0,
|
|
286
|
+
webSearchArgsEmitted: false,
|
|
287
|
+
toolArgsEmitted: false,
|
|
288
|
+
};
|
|
289
|
+
},
|
|
290
|
+
toolArgsDelta(_itemId, delta) {
|
|
291
|
+
if (!delta) return;
|
|
292
|
+
if (!open || open.kind !== "tool_use") return;
|
|
293
|
+
if (open.bufferWebSearchArgs) {
|
|
294
|
+
const next = appendRetained(open.argsBuf ?? "", open.argsBufBytes ?? 0, delta, {
|
|
295
|
+
kind: "tool_args",
|
|
296
|
+
...(open.callId ? { callId: open.callId } : {}),
|
|
297
|
+
});
|
|
298
|
+
open.argsBuf = next.value;
|
|
299
|
+
open.argsBufBytes = next.bytes;
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
emit("content_block_delta", {
|
|
303
|
+
type: "content_block_delta", index: open.index,
|
|
304
|
+
delta: { type: "input_json_delta", partial_json: delta },
|
|
305
|
+
}, { semanticBytes: Buffer.byteLength(delta) });
|
|
306
|
+
open.toolArgsEmitted = true;
|
|
307
|
+
},
|
|
308
|
+
toolArgsDone(_itemId, args) {
|
|
309
|
+
if (!open || open.kind !== "tool_use" || open.bufferWebSearchArgs || open.toolArgsEmitted) return;
|
|
310
|
+
if (args.length === 0) return;
|
|
311
|
+
emit("content_block_delta", {
|
|
312
|
+
type: "content_block_delta", index: open.index,
|
|
313
|
+
delta: { type: "input_json_delta", partial_json: args },
|
|
314
|
+
}, { semanticBytes: Buffer.byteLength(args) });
|
|
315
|
+
open.toolArgsEmitted = true;
|
|
316
|
+
},
|
|
317
|
+
toolDone(call) {
|
|
318
|
+
if (call.kind !== "function" || !open || open.kind !== "tool_use") return;
|
|
319
|
+
if (open.bufferWebSearchArgs && !open.webSearchArgsEmitted) {
|
|
320
|
+
const rawArgs = call.arguments.length > 0 ? call.arguments : (open.argsBuf ?? "");
|
|
321
|
+
let parsed: unknown = {};
|
|
322
|
+
try { parsed = rawArgs.length > 0 ? JSON.parse(rawArgs) : {}; } catch { parsed = {}; }
|
|
323
|
+
emit("content_block_delta", {
|
|
324
|
+
type: "content_block_delta",
|
|
325
|
+
index: open.index,
|
|
326
|
+
delta: { type: "input_json_delta", partial_json: JSON.stringify(sanitizeWebSearchInput(parsed)) },
|
|
327
|
+
});
|
|
328
|
+
open.webSearchArgsEmitted = true;
|
|
329
|
+
} else if (!open.bufferWebSearchArgs && !open.toolArgsEmitted && call.arguments.length > 0) {
|
|
330
|
+
emit("content_block_delta", {
|
|
331
|
+
type: "content_block_delta", index: open.index,
|
|
332
|
+
delta: { type: "input_json_delta", partial_json: call.arguments },
|
|
333
|
+
}, { semanticBytes: Buffer.byteLength(call.arguments) });
|
|
334
|
+
open.toolArgsEmitted = true;
|
|
335
|
+
}
|
|
336
|
+
closeOpenBlock();
|
|
337
|
+
},
|
|
338
|
+
webSearchDone(search) {
|
|
339
|
+
// Server-side search becomes the pair Claude Code parses natively. It never marks a
|
|
340
|
+
// tool use, so the stop reason stays end_turn unless a real tool ran.
|
|
341
|
+
ensureStarted();
|
|
342
|
+
closeOpenBlock();
|
|
343
|
+
const pair = webSearchPairFromItem({
|
|
344
|
+
type: "web_search_call",
|
|
345
|
+
id: search.itemId,
|
|
346
|
+
status: search.status,
|
|
347
|
+
action: webSearchAction(search.queries),
|
|
348
|
+
...(search.sources.length > 0 ? { sources: search.sources } : {}),
|
|
349
|
+
});
|
|
350
|
+
const toolIndex = blockIndex++;
|
|
351
|
+
emit("content_block_start", {
|
|
352
|
+
type: "content_block_start", index: toolIndex,
|
|
353
|
+
content_block: { type: "server_tool_use", id: pair.id, name: "web_search" },
|
|
354
|
+
}, { sideEffect: true });
|
|
355
|
+
emit("content_block_delta", {
|
|
356
|
+
type: "content_block_delta", index: toolIndex,
|
|
357
|
+
delta: { type: "input_json_delta", partial_json: JSON.stringify(pair.input) },
|
|
358
|
+
});
|
|
359
|
+
emit("content_block_stop", { type: "content_block_stop", index: toolIndex });
|
|
360
|
+
const resultIndex = blockIndex++;
|
|
361
|
+
emit("content_block_start", {
|
|
362
|
+
type: "content_block_start", index: resultIndex,
|
|
363
|
+
content_block: { type: "web_search_tool_result", tool_use_id: pair.id, content: pair.resultContent },
|
|
364
|
+
});
|
|
365
|
+
emit("content_block_stop", { type: "content_block_stop", index: resultIndex });
|
|
366
|
+
if (pair.completed) webSearchRequests++;
|
|
367
|
+
},
|
|
368
|
+
terminal(terminal) {
|
|
369
|
+
if (terminal.status === "completed") {
|
|
370
|
+
if (terminal.endTurn === false && !sawToolUse) {
|
|
371
|
+
fail(529, "upstream turn ended without a final answer", true);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
finish(sawToolUse ? "tool_use" : "end_turn", responsesUsage(terminal.usage));
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (terminal.status === "incomplete") {
|
|
378
|
+
const outcome = anthropicIncompleteOutcome(terminal.incomplete?.reason, terminal.incomplete?.message);
|
|
379
|
+
if ("stopReason" in outcome) finish(outcome.stopReason, terminal.usageOnWire ? responsesUsage(terminal.usage) : null);
|
|
380
|
+
else fail(529, outcome.failMessage, true);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const error = (terminal.error ?? {}) as unknown as Rec;
|
|
384
|
+
if (error.code === "translation_buffer_limit") {
|
|
385
|
+
fail(413, "upstream translation buffer exceeded the safe limit", false, "translation_buffer_limit");
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const message = typeof error.message === "string" ? error.message : "upstream request failed";
|
|
389
|
+
fail(anthropicFailedStatus(error, message), message, true);
|
|
390
|
+
},
|
|
391
|
+
overflow() {
|
|
392
|
+
fail(413, "upstream translation buffer exceeded the safe limit", false, "translation_buffer_limit");
|
|
393
|
+
},
|
|
394
|
+
dispose() {
|
|
395
|
+
releaseThinkingBuffer(open);
|
|
396
|
+
if (open?.callId) budget.closeCall(open.callId);
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Stream adapter events as Anthropic Messages SSE bytes. */
|
|
402
|
+
export function encodeAnthropicMessageSse(
|
|
403
|
+
events: AsyncIterable<AdapterEvent>,
|
|
404
|
+
options: AnthropicMessageEncodeOptions,
|
|
405
|
+
): ReadableStream<Uint8Array> {
|
|
406
|
+
return encodeAdapterEventStream(
|
|
407
|
+
events,
|
|
408
|
+
sink => createAnthropicMessageWriter(sink, options.model, options.inputTokenFloor),
|
|
409
|
+
{ ...options, keepaliveMs: MESSAGES_KEEPALIVE_MS },
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Fold an encoded Messages stream into the non-streaming client response, with the status
|
|
415
|
+
* mapping the Messages ingress applied to its collected message: a translator overflow is 413,
|
|
416
|
+
* a stream that ended in an error frame is 502, anything else unexpected a 502 api_error.
|
|
417
|
+
*/
|
|
418
|
+
export async function collectAnthropicMessageResponse(
|
|
419
|
+
stream: ReadableStream<Uint8Array>,
|
|
420
|
+
model: string,
|
|
421
|
+
translatorBudget: TranslatorBudget,
|
|
422
|
+
): Promise<Response> {
|
|
423
|
+
let message: Rec;
|
|
424
|
+
try {
|
|
425
|
+
message = await collectAnthropicMessage(stream, model, translatorBudget);
|
|
426
|
+
} catch (error) {
|
|
427
|
+
if (isTranslatorBudgetExceededError(error)) {
|
|
428
|
+
return anthropicErrorResponse(413, error.message, "request_too_large", error.code);
|
|
429
|
+
}
|
|
430
|
+
return anthropicErrorResponse(502, error instanceof Error ? error.message : String(error), "api_error");
|
|
431
|
+
}
|
|
432
|
+
const isError = message.type === "error";
|
|
433
|
+
const translatedError = isError && typeof message.error === "object"
|
|
434
|
+
? (message as { error: { code?: unknown; message?: unknown } }).error
|
|
435
|
+
: undefined;
|
|
436
|
+
if (translatedError?.code === "translation_buffer_limit") {
|
|
437
|
+
return anthropicErrorResponse(
|
|
438
|
+
413,
|
|
439
|
+
typeof translatedError.message === "string"
|
|
440
|
+
? translatedError.message
|
|
441
|
+
: "upstream translation buffer exceeded the safe limit",
|
|
442
|
+
"request_too_large",
|
|
443
|
+
"translation_buffer_limit",
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
return new Response(JSON.stringify(message), {
|
|
447
|
+
status: isError ? 502 : 200,
|
|
448
|
+
headers: { "Content-Type": "application/json" },
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Non-streaming client: the encoded stream folded exactly as the Messages collector folds it. */
|
|
453
|
+
export function foldAnthropicMessage(
|
|
454
|
+
events: AsyncIterable<AdapterEvent>,
|
|
455
|
+
options: AnthropicMessageEncodeOptions,
|
|
456
|
+
): Promise<Response> {
|
|
457
|
+
return collectAnthropicMessageResponse(
|
|
458
|
+
encodeAnthropicMessageSse(events, options),
|
|
459
|
+
options.model,
|
|
460
|
+
options.translatorBudget,
|
|
461
|
+
);
|
|
462
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The source envelope of one inference request (PF-06).
|
|
3
|
+
*
|
|
4
|
+
* SERVER SIDE, not a leaf: it charges the request's translator budget. The envelope keeps the
|
|
5
|
+
* body the ingress parsed, by reference, for the lifetime of the request and nothing longer —
|
|
6
|
+
* it is a local of the ingress, never stored on a shared or long-lived object. Features are
|
|
7
|
+
* scanned once, on first use, so a caller that never asks pays nothing. `freshBody()` hands out
|
|
8
|
+
* an independent copy, charged under `request_copies`, so a consumer that rewrites its body in
|
|
9
|
+
* place can never leak that rewrite into another consumer's input.
|
|
10
|
+
*/
|
|
11
|
+
import { jsonUtf8Bytes } from "../lib/json-byte-size";
|
|
12
|
+
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
13
|
+
import type { Protocol } from "./contract";
|
|
14
|
+
import { featuresFromBody, type ProtocolFeature } from "./features";
|
|
15
|
+
|
|
16
|
+
export interface ProtocolEnvelope {
|
|
17
|
+
readonly inbound: Protocol;
|
|
18
|
+
/** The request features, scanned from the source body on the first call and cached. */
|
|
19
|
+
features(): ReadonlySet<ProtocolFeature>;
|
|
20
|
+
/** A structured clone of the source body, charged to the translator budget. */
|
|
21
|
+
freshBody(): Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createProtocolEnvelope(input: {
|
|
25
|
+
inbound: Protocol;
|
|
26
|
+
body: Record<string, unknown>;
|
|
27
|
+
translatorBudget: TranslatorBudget;
|
|
28
|
+
}): ProtocolEnvelope {
|
|
29
|
+
const { inbound, body, translatorBudget } = input;
|
|
30
|
+
let features: ReadonlySet<ProtocolFeature> | undefined;
|
|
31
|
+
return {
|
|
32
|
+
inbound,
|
|
33
|
+
features() {
|
|
34
|
+
features ??= featuresFromBody(inbound, body);
|
|
35
|
+
return features;
|
|
36
|
+
},
|
|
37
|
+
freshBody() {
|
|
38
|
+
// Charged before the copy exists, so an over-budget request fails without allocating it.
|
|
39
|
+
translatorBudget.chargeRetained(jsonUtf8Bytes(body), { kind: "request_copies" });
|
|
40
|
+
return structuredClone(body);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|