@yanzc/claude-pro 0.0.0-stage → 0.4.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/.env.example +41 -0
- package/LICENSE +21 -0
- package/README.md +314 -3
- package/bin/claude-pro.js +32 -0
- package/package.json +40 -6
- package/src/agent.js +752 -0
- package/src/agents.js +186 -0
- package/src/api.js +488 -0
- package/src/cache.js +150 -0
- package/src/checkpoints.js +184 -0
- package/src/claude-code.js +60 -0
- package/src/codex/client.js +300 -0
- package/src/codex/translate-request.js +239 -0
- package/src/codex/translate-stream.js +328 -0
- package/src/commands.js +803 -0
- package/src/config.js +453 -0
- package/src/context.js +109 -0
- package/src/filestate.js +57 -0
- package/src/gateway.js +110 -0
- package/src/hooks.js +81 -0
- package/src/main.js +193 -0
- package/src/mcp.js +290 -0
- package/src/modes.js +154 -0
- package/src/oauth.js +287 -0
- package/src/peers.js +156 -0
- package/src/print.js +106 -0
- package/src/prompts.js +179 -0
- package/src/ratelimit.js +244 -0
- package/src/restart.js +115 -0
- package/src/runtime.js +162 -0
- package/src/selfcheck.js +71 -0
- package/src/session.js +139 -0
- package/src/supervisor.js +110 -0
- package/src/tasks.js +76 -0
- package/src/tools/bash.js +227 -0
- package/src/tools/effort.js +41 -0
- package/src/tools/files.js +352 -0
- package/src/tools/index.js +52 -0
- package/src/tools/interaction.js +157 -0
- package/src/tools/notebook.js +91 -0
- package/src/tools/peers.js +45 -0
- package/src/tools/restart.js +50 -0
- package/src/tools/search.js +458 -0
- package/src/tools/skill.js +32 -0
- package/src/tools/task.js +139 -0
- package/src/tools/web.js +283 -0
- package/src/ui/ansi.js +228 -0
- package/src/ui/dialogs.js +122 -0
- package/src/ui/editor.js +389 -0
- package/src/ui/markdown.js +249 -0
- package/src/ui/render.js +155 -0
- package/src/ui/repl.js +1450 -0
- package/src/ui/screen.js +259 -0
- package/src/updater.js +93 -0
- package/src/utils/clipboard.js +197 -0
- package/src/utils/diff.js +95 -0
- package/src/utils/paths.js +37 -0
- package/src/utils/shell.js +251 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// Copied from codex-gateway (src/translate-request.mjs): Anthropic Messages request -> OpenAI Responses request.
|
|
2
|
+
// Anthropic Messages request -> OpenAI Responses request (Codex backend flavour).
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
|
|
5
|
+
// Thinking blocks we emit carry Codex's encrypted reasoning in `signature`, prefixed with this
|
|
6
|
+
// marker, so the reasoning can be replayed on the next turn (store=false has no server state).
|
|
7
|
+
export const SIGNATURE_PREFIX = "cgw1:";
|
|
8
|
+
|
|
9
|
+
const hash = (s, n = 8) => crypto.createHash("sha256").update(s).digest("hex").slice(0, n);
|
|
10
|
+
|
|
11
|
+
/** OpenAI function names: ^[a-zA-Z0-9_-]{1,64}$. Long MCP names get a stable hashed suffix. */
|
|
12
|
+
export function upstreamToolName(name) {
|
|
13
|
+
let n = String(name).replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
14
|
+
if (n.length > 64) n = `${n.slice(0, 55)}_${hash(name)}`;
|
|
15
|
+
return n || "tool";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function systemText(system) {
|
|
19
|
+
if (!system) return "";
|
|
20
|
+
if (typeof system === "string") return system;
|
|
21
|
+
return system
|
|
22
|
+
.filter((b) => b?.type === "text" && b.text && !b.text.startsWith("x-anthropic-billing-header"))
|
|
23
|
+
.map((b) => b.text)
|
|
24
|
+
.join("\n\n");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const asBlocks = (content) => (typeof content === "string" ? [{ type: "text", text: content }] : content || []);
|
|
28
|
+
|
|
29
|
+
function imagePart(block) {
|
|
30
|
+
const s = block.source || {};
|
|
31
|
+
if (s.type === "base64") return { type: "input_image", image_url: `data:${s.media_type};base64,${s.data}` };
|
|
32
|
+
if (s.type === "url") return { type: "input_image", image_url: s.url };
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function documentPart(block) {
|
|
37
|
+
const s = block.source || {};
|
|
38
|
+
if (s.type === "text") return { type: "input_text", text: s.data || "" };
|
|
39
|
+
if (s.type === "base64") {
|
|
40
|
+
return {
|
|
41
|
+
type: "input_file",
|
|
42
|
+
filename: block.title || (s.media_type === "application/pdf" ? "document.pdf" : "document"),
|
|
43
|
+
file_data: `data:${s.media_type};base64,${s.data}`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (s.type === "content") return { type: "input_text", text: asBlocks(s.content).map((b) => b.text || "").join("\n") };
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function toolResultItem(block) {
|
|
51
|
+
const parts = asBlocks(block.content);
|
|
52
|
+
const errorPrefix = (text) => (block.is_error && !/^\s*<tool_use_error>/.test(text) ? `ERROR: ${text}` : text);
|
|
53
|
+
let output;
|
|
54
|
+
if (parts.some((p) => p.type === "image")) {
|
|
55
|
+
output = parts
|
|
56
|
+
.map((p) => (p.type === "image" ? imagePart(p) : p.type === "text" ? { type: "input_text", text: p.text } : null))
|
|
57
|
+
.filter(Boolean);
|
|
58
|
+
if (block.is_error) output.unshift({ type: "input_text", text: "ERROR:" });
|
|
59
|
+
} else {
|
|
60
|
+
const text = (p) =>
|
|
61
|
+
p.type === "text" ? p.text
|
|
62
|
+
: p.type === "document" ? documentPart(p)?.text || ""
|
|
63
|
+
: p.type === "tool_reference" ? `Tool ${p.tool_name} is now available.`
|
|
64
|
+
: "";
|
|
65
|
+
output = errorPrefix(parts.map(text).join("\n"));
|
|
66
|
+
}
|
|
67
|
+
return { type: "function_call_output", call_id: block.tool_use_id, output };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function reasoningItem(block) {
|
|
71
|
+
const sig = block.signature;
|
|
72
|
+
if (typeof sig !== "string" || !sig.startsWith(SIGNATURE_PREFIX)) return null;
|
|
73
|
+
return { type: "reasoning", summary: [], encrypted_content: sig.slice(SIGNATURE_PREFIX.length) };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function convertMessages(messages, { dropReasoning }) {
|
|
77
|
+
const input = [];
|
|
78
|
+
for (const msg of messages || []) {
|
|
79
|
+
const blocks = asBlocks(msg.content);
|
|
80
|
+
if (msg.role === "assistant") {
|
|
81
|
+
let parts = [];
|
|
82
|
+
const flush = () => {
|
|
83
|
+
if (parts.length) input.push({ type: "message", role: "assistant", content: parts });
|
|
84
|
+
parts = [];
|
|
85
|
+
};
|
|
86
|
+
for (const b of blocks) {
|
|
87
|
+
if (b.type === "text") {
|
|
88
|
+
if (b.text) parts.push({ type: "output_text", text: b.text });
|
|
89
|
+
} else if (b.type === "thinking") {
|
|
90
|
+
const r = !dropReasoning && reasoningItem(b);
|
|
91
|
+
if (r) {
|
|
92
|
+
flush();
|
|
93
|
+
input.push(r);
|
|
94
|
+
}
|
|
95
|
+
} else if (b.type === "tool_use") {
|
|
96
|
+
flush();
|
|
97
|
+
input.push({
|
|
98
|
+
type: "function_call",
|
|
99
|
+
call_id: b.id,
|
|
100
|
+
name: upstreamToolName(b.name),
|
|
101
|
+
arguments: JSON.stringify(b.input ?? {}),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
flush();
|
|
106
|
+
} else {
|
|
107
|
+
let parts = [];
|
|
108
|
+
const flush = () => {
|
|
109
|
+
if (parts.length) input.push({ type: "message", role: "user", content: parts });
|
|
110
|
+
parts = [];
|
|
111
|
+
};
|
|
112
|
+
for (const b of blocks) {
|
|
113
|
+
if (b.type === "text") {
|
|
114
|
+
if (b.text) parts.push({ type: "input_text", text: b.text });
|
|
115
|
+
} else if (b.type === "image") {
|
|
116
|
+
const p = imagePart(b);
|
|
117
|
+
if (p) parts.push(p);
|
|
118
|
+
} else if (b.type === "document") {
|
|
119
|
+
const p = documentPart(b);
|
|
120
|
+
if (p) parts.push(p);
|
|
121
|
+
} else if (b.type === "tool_result") {
|
|
122
|
+
flush();
|
|
123
|
+
input.push(toolResultItem(b));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
flush();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return input;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function sanitizeSchema(schema) {
|
|
133
|
+
const s = schema && typeof schema === "object" ? { ...schema } : {};
|
|
134
|
+
delete s.$schema;
|
|
135
|
+
if (s.type !== "object") s.type = "object";
|
|
136
|
+
if (!s.properties || typeof s.properties !== "object") s.properties = {};
|
|
137
|
+
return s;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function convertTools(tools, { webSearch }) {
|
|
141
|
+
const out = [];
|
|
142
|
+
for (const t of tools || []) {
|
|
143
|
+
if (typeof t.type === "string" && t.type.startsWith("web_search")) {
|
|
144
|
+
if (webSearch) out.push({ type: "web_search" });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (t.type && t.type !== "custom" && !t.input_schema) continue; // other Anthropic server tools
|
|
148
|
+
out.push({
|
|
149
|
+
type: "function",
|
|
150
|
+
name: upstreamToolName(t.name),
|
|
151
|
+
description: t.description || "",
|
|
152
|
+
parameters: sanitizeSchema(t.input_schema),
|
|
153
|
+
strict: false,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function convertToolChoice(tc) {
|
|
160
|
+
if (!tc) return "auto";
|
|
161
|
+
switch (tc.type) {
|
|
162
|
+
case "any":
|
|
163
|
+
return "required";
|
|
164
|
+
case "none":
|
|
165
|
+
return "none";
|
|
166
|
+
case "tool":
|
|
167
|
+
return { type: "function", name: upstreamToolName(tc.name) };
|
|
168
|
+
default:
|
|
169
|
+
return "auto";
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Maps upstream (possibly shortened) tool names back to the client's names. */
|
|
174
|
+
export function toolNameMap(anth) {
|
|
175
|
+
const map = new Map();
|
|
176
|
+
const add = (n) => n && map.set(upstreamToolName(n), n);
|
|
177
|
+
for (const t of anth.tools || []) add(t.name);
|
|
178
|
+
for (const m of anth.messages || []) for (const b of asBlocks(m.content)) if (b.type === "tool_use") add(b.name);
|
|
179
|
+
return map;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Stable per-conversation id, used as session_id header and prompt_cache_key. */
|
|
183
|
+
export function sessionIdFor(anth) {
|
|
184
|
+
const uid = anth.metadata?.user_id;
|
|
185
|
+
let sid = null;
|
|
186
|
+
if (typeof uid === "string") {
|
|
187
|
+
try {
|
|
188
|
+
sid = JSON.parse(uid).session_id || null;
|
|
189
|
+
} catch {}
|
|
190
|
+
sid ||= uid.match(/session_([0-9a-fA-F-]{36})/)?.[1] || null;
|
|
191
|
+
}
|
|
192
|
+
const seed = `${sid || "anon"}|${systemText(anth.system).slice(0, 300)}`;
|
|
193
|
+
const h = crypto.createHash("sha256").update(seed).digest("hex");
|
|
194
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-a${h.slice(17, 20)}-${h.slice(20, 32)}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* @param anth Anthropic request body
|
|
199
|
+
* @param o { model, effort, summary, sessionId, instructionsMode: "custom"|"codex", codexInstructions,
|
|
200
|
+
* dropReasoning, removedParams:Set<string>, webSearch, parallelToolCalls }
|
|
201
|
+
*/
|
|
202
|
+
export function buildResponsesRequest(anth, o) {
|
|
203
|
+
const system = systemText(anth.system);
|
|
204
|
+
const input = convertMessages(anth.messages, o);
|
|
205
|
+
let instructions = system || "You are a helpful assistant.";
|
|
206
|
+
if (o.instructionsMode === "codex" && o.codexInstructions) {
|
|
207
|
+
instructions = o.codexInstructions;
|
|
208
|
+
const note =
|
|
209
|
+
"The instructions above describe the Codex CLI harness. In this session you are driven by a different " +
|
|
210
|
+
"harness: follow the developer instructions below and use only the tools defined in this request.";
|
|
211
|
+
input.unshift({ type: "message", role: "developer", content: [{ type: "input_text", text: `${note}\n\n${system}` }] });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const payload = {
|
|
215
|
+
model: o.model,
|
|
216
|
+
instructions,
|
|
217
|
+
input,
|
|
218
|
+
store: false,
|
|
219
|
+
stream: true,
|
|
220
|
+
include: ["reasoning.encrypted_content"],
|
|
221
|
+
prompt_cache_key: o.sessionId,
|
|
222
|
+
};
|
|
223
|
+
if (o.effort) {
|
|
224
|
+
payload.reasoning = { effort: o.effort };
|
|
225
|
+
if (o.summary && o.summary !== "none") payload.reasoning.summary = o.summary;
|
|
226
|
+
}
|
|
227
|
+
const tools = convertTools(anth.tools, o);
|
|
228
|
+
if (tools.length) {
|
|
229
|
+
payload.tools = tools;
|
|
230
|
+
payload.tool_choice = convertToolChoice(anth.tool_choice);
|
|
231
|
+
payload.parallel_tool_calls = o.parallelToolCalls && !anth.tool_choice?.disable_parallel_tool_use;
|
|
232
|
+
}
|
|
233
|
+
for (const p of o.removedParams || []) {
|
|
234
|
+
const [a, b] = p.split(".");
|
|
235
|
+
if (b && payload[a] && typeof payload[a] === "object") delete payload[a][b];
|
|
236
|
+
else delete payload[a];
|
|
237
|
+
}
|
|
238
|
+
return payload;
|
|
239
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
// Copied from codex-gateway (src/translate-stream.mjs): OpenAI Responses SSE -> Anthropic Messages SSE events.
|
|
2
|
+
// OpenAI Responses SSE events -> Anthropic Messages SSE events (and a non-streaming aggregator).
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import { SIGNATURE_PREFIX } from "./translate-request.js";
|
|
5
|
+
|
|
6
|
+
/** Async iterator over parsed SSE `data:` JSON payloads from a Node readable stream. */
|
|
7
|
+
export async function* parseSSE(stream) {
|
|
8
|
+
let buf = "";
|
|
9
|
+
const decoder = new TextDecoder();
|
|
10
|
+
const parseFrame = (frame) => {
|
|
11
|
+
const data = frame
|
|
12
|
+
.split(/\r?\n/)
|
|
13
|
+
.filter((l) => l.startsWith("data:"))
|
|
14
|
+
.map((l) => l.slice(5).replace(/^ /, ""))
|
|
15
|
+
.join("\n");
|
|
16
|
+
if (!data || data === "[DONE]") return null;
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(data);
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
for await (const chunk of stream) {
|
|
24
|
+
buf += decoder.decode(chunk, { stream: true });
|
|
25
|
+
let m;
|
|
26
|
+
while ((m = buf.match(/\r?\n\r?\n/))) {
|
|
27
|
+
const frame = buf.slice(0, m.index);
|
|
28
|
+
buf = buf.slice(m.index + m[0].length);
|
|
29
|
+
const ev = parseFrame(frame);
|
|
30
|
+
if (ev) yield ev;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const ev = parseFrame(buf);
|
|
34
|
+
if (ev) yield ev;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Merges usage objects, skipping null/undefined fields so a partial update cannot erase counts. */
|
|
38
|
+
export function mergeUsage(base, update) {
|
|
39
|
+
const merged = { ...(base || {}) };
|
|
40
|
+
for (const [key, value] of Object.entries(update || {})) {
|
|
41
|
+
if (value !== null && value !== undefined) merged[key] = value;
|
|
42
|
+
}
|
|
43
|
+
return merged;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function anthropicUsage(u) {
|
|
47
|
+
if (!u) return { input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 };
|
|
48
|
+
const cached = u.input_tokens_details?.cached_tokens || 0;
|
|
49
|
+
return {
|
|
50
|
+
input_tokens: Math.max(0, (u.input_tokens || 0) - cached),
|
|
51
|
+
output_tokens: u.output_tokens || 0,
|
|
52
|
+
cache_read_input_tokens: cached,
|
|
53
|
+
cache_creation_input_tokens: 0,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Feed upstream events through handle(); Anthropic events come out through onEvent(name, data).
|
|
59
|
+
* Content blocks are emitted strictly one after another, as the Anthropic protocol requires.
|
|
60
|
+
*/
|
|
61
|
+
export class ResponsesToAnthropic {
|
|
62
|
+
constructor({ model, toolNames, emitThinking = true, onEvent }) {
|
|
63
|
+
this.model = model;
|
|
64
|
+
this.toolNames = toolNames || new Map();
|
|
65
|
+
this.emitThinking = emitThinking;
|
|
66
|
+
this.onEvent = onEvent;
|
|
67
|
+
this.messageId = "msg_" + crypto.randomBytes(12).toString("hex");
|
|
68
|
+
this.index = -1;
|
|
69
|
+
this.open = null; // { key, type, sawDelta, summaryIndex, citations }
|
|
70
|
+
this.closedKeys = new Set();
|
|
71
|
+
this.sawToolUse = false;
|
|
72
|
+
this.started = false;
|
|
73
|
+
this.done = false;
|
|
74
|
+
this.usage = null;
|
|
75
|
+
this.error = null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
emit(name, data) {
|
|
79
|
+
this.onEvent(name, { type: name, ...data });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
start() {
|
|
83
|
+
if (this.started) return;
|
|
84
|
+
this.started = true;
|
|
85
|
+
this.emit("message_start", {
|
|
86
|
+
message: {
|
|
87
|
+
id: this.messageId,
|
|
88
|
+
type: "message",
|
|
89
|
+
role: "assistant",
|
|
90
|
+
model: this.model,
|
|
91
|
+
content: [],
|
|
92
|
+
stop_reason: null,
|
|
93
|
+
stop_sequence: null,
|
|
94
|
+
usage: { input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 },
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
openBlock(key, contentBlock, extra = {}) {
|
|
100
|
+
if (this.open?.key === key) return this.open;
|
|
101
|
+
this.closeBlock();
|
|
102
|
+
this.index += 1;
|
|
103
|
+
this.open = { key, type: contentBlock.type, sawDelta: false, ...extra };
|
|
104
|
+
this.emit("content_block_start", { index: this.index, content_block: contentBlock });
|
|
105
|
+
return this.open;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
closeBlock() {
|
|
109
|
+
if (!this.open) return;
|
|
110
|
+
this.emit("content_block_stop", { index: this.index });
|
|
111
|
+
this.closedKeys.add(this.open.key);
|
|
112
|
+
this.open = null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
delta(delta) {
|
|
116
|
+
this.emit("content_block_delta", { index: this.index, delta });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
textBlock(key) {
|
|
120
|
+
return this.openBlock(key, { type: "text", text: "" }, { citations: [] });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
thinkingBlock(key) {
|
|
124
|
+
return this.openBlock(key, { type: "thinking", thinking: "", signature: "" }, { summaryIndex: null });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
handle(ev) {
|
|
128
|
+
if (this.done) return;
|
|
129
|
+
this.start();
|
|
130
|
+
const item = ev.item;
|
|
131
|
+
switch (ev.type) {
|
|
132
|
+
case "response.output_item.added":
|
|
133
|
+
// Reasoning blocks open lazily (first summary delta or item done) to avoid empty blocks.
|
|
134
|
+
if (item?.type === "function_call") this.toolBlock(ev.output_index, item);
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
case "response.reasoning_summary_part.added":
|
|
138
|
+
case "response.reasoning_summary_text.delta":
|
|
139
|
+
case "response.reasoning_text.delta": {
|
|
140
|
+
if (!this.emitThinking) break;
|
|
141
|
+
const key = `rs:${ev.output_index}`;
|
|
142
|
+
if (this.closedKeys.has(key) || (!ev.delta && this.open?.key !== key)) break;
|
|
143
|
+
const blk = this.thinkingBlock(key);
|
|
144
|
+
const part = ev.summary_index ?? ev.content_index ?? 0;
|
|
145
|
+
if (blk.summaryIndex !== null && blk.summaryIndex !== part && blk.sawDelta) {
|
|
146
|
+
this.delta({ type: "thinking_delta", thinking: "\n\n" });
|
|
147
|
+
}
|
|
148
|
+
blk.summaryIndex = part;
|
|
149
|
+
if (ev.delta) {
|
|
150
|
+
blk.sawDelta = true;
|
|
151
|
+
this.delta({ type: "thinking_delta", thinking: ev.delta });
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
case "response.output_text.delta":
|
|
157
|
+
case "response.refusal.delta": {
|
|
158
|
+
const key = `msg:${ev.output_index}:${ev.content_index ?? 0}`;
|
|
159
|
+
if (this.closedKeys.has(key)) break;
|
|
160
|
+
const blk = this.textBlock(key);
|
|
161
|
+
if (ev.delta) {
|
|
162
|
+
blk.sawDelta = true;
|
|
163
|
+
this.delta({ type: "text_delta", text: ev.delta });
|
|
164
|
+
}
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
case "response.output_text.annotation.added": {
|
|
169
|
+
const a = ev.annotation;
|
|
170
|
+
if (a?.type === "url_citation" && this.open?.type === "text") this.open.citations.push(a);
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
case "response.function_call_arguments.delta": {
|
|
175
|
+
const key = `fc:${ev.output_index}`;
|
|
176
|
+
if (this.open?.key !== key || !ev.delta) break;
|
|
177
|
+
this.open.sawDelta = true;
|
|
178
|
+
this.delta({ type: "input_json_delta", partial_json: ev.delta });
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
case "response.output_item.done":
|
|
183
|
+
this.itemDone(ev.output_index, item);
|
|
184
|
+
break;
|
|
185
|
+
|
|
186
|
+
case "response.completed":
|
|
187
|
+
case "response.incomplete":
|
|
188
|
+
this.finish(ev.response);
|
|
189
|
+
break;
|
|
190
|
+
|
|
191
|
+
case "response.failed":
|
|
192
|
+
this.fail(ev.response?.error?.message || "Upstream response failed", ev.response?.error?.code);
|
|
193
|
+
break;
|
|
194
|
+
|
|
195
|
+
case "error":
|
|
196
|
+
this.fail(ev.message || ev.error?.message || "Upstream error", ev.code || ev.error?.code || ev.error?.type);
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
toolBlock(outputIndex, item) {
|
|
202
|
+
this.sawToolUse = true;
|
|
203
|
+
return this.openBlock(`fc:${outputIndex}`, {
|
|
204
|
+
type: "tool_use",
|
|
205
|
+
id: item.call_id || item.id,
|
|
206
|
+
name: this.toolNames.get(item.name) || item.name,
|
|
207
|
+
input: {},
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
itemDone(outputIndex, item) {
|
|
212
|
+
if (!item) return;
|
|
213
|
+
if (item.type === "reasoning") {
|
|
214
|
+
if (!this.emitThinking) return;
|
|
215
|
+
const key = `rs:${outputIndex}`;
|
|
216
|
+
if (this.closedKeys.has(key)) return;
|
|
217
|
+
const text = (item.summary || []).map((s) => s.text).filter(Boolean).join("\n\n");
|
|
218
|
+
if (this.open?.key !== key && !text && !item.encrypted_content) return;
|
|
219
|
+
const blk = this.thinkingBlock(key);
|
|
220
|
+
if (!blk.sawDelta && text) this.delta({ type: "thinking_delta", thinking: text });
|
|
221
|
+
if (item.encrypted_content) {
|
|
222
|
+
this.delta({ type: "signature_delta", signature: SIGNATURE_PREFIX + item.encrypted_content });
|
|
223
|
+
}
|
|
224
|
+
this.closeBlock();
|
|
225
|
+
} else if (item.type === "function_call") {
|
|
226
|
+
const key = `fc:${outputIndex}`;
|
|
227
|
+
if (this.closedKeys.has(key)) return;
|
|
228
|
+
const blk = this.open?.key === key ? this.open : this.toolBlock(outputIndex, item);
|
|
229
|
+
if (!blk.sawDelta && item.arguments) this.delta({ type: "input_json_delta", partial_json: item.arguments });
|
|
230
|
+
this.closeBlock();
|
|
231
|
+
} else if (item.type === "message") {
|
|
232
|
+
(item.content || []).forEach((part, ci) => {
|
|
233
|
+
const key = `msg:${outputIndex}:${ci}`;
|
|
234
|
+
if (this.closedKeys.has(key)) return;
|
|
235
|
+
const text = part.text ?? part.refusal ?? "";
|
|
236
|
+
const blk = this.open?.key === key ? this.open : text ? this.textBlock(key) : null;
|
|
237
|
+
if (!blk) return;
|
|
238
|
+
if (!blk.sawDelta && text) this.delta({ type: "text_delta", text });
|
|
239
|
+
const cites = blk.citations.length ? blk.citations : (part.annotations || []).filter((a) => a.type === "url_citation");
|
|
240
|
+
const seen = new Set();
|
|
241
|
+
const lines = cites
|
|
242
|
+
.filter((c) => c.url && !seen.has(c.url) && seen.add(c.url))
|
|
243
|
+
.map((c) => `- [${c.title || c.url}](${c.url})`);
|
|
244
|
+
if (lines.length) this.delta({ type: "text_delta", text: `\n\nSources:\n${lines.join("\n")}` });
|
|
245
|
+
this.closeBlock();
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
finish(response) {
|
|
251
|
+
if (this.done) return;
|
|
252
|
+
this.closeBlock();
|
|
253
|
+
this.done = true;
|
|
254
|
+
this.usage = anthropicUsage(response?.usage);
|
|
255
|
+
let stop = this.sawToolUse ? "tool_use" : "end_turn";
|
|
256
|
+
if (response?.status === "incomplete") {
|
|
257
|
+
const reason = response.incomplete_details?.reason;
|
|
258
|
+
stop = reason === "max_output_tokens" ? "max_tokens" : reason === "content_filter" ? "refusal" : stop;
|
|
259
|
+
}
|
|
260
|
+
this.stopReason = stop;
|
|
261
|
+
this.emit("message_delta", { delta: { stop_reason: stop, stop_sequence: null }, usage: this.usage });
|
|
262
|
+
this.emit("message_stop", {});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
fail(message, code) {
|
|
266
|
+
if (this.done) return;
|
|
267
|
+
this.closeBlock();
|
|
268
|
+
this.done = true;
|
|
269
|
+
const type = /rate|limit|quota/i.test(`${code} ${message}`) ? "rate_limit_error" : /overload|capacity/i.test(message) ? "overloaded_error" : "api_error";
|
|
270
|
+
this.error = { type, message: code ? `${message} (${code})` : message };
|
|
271
|
+
this.emit("error", { error: this.error });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Call when the upstream stream ended; reports truncated streams as errors. */
|
|
275
|
+
end() {
|
|
276
|
+
if (!this.done) {
|
|
277
|
+
this.start();
|
|
278
|
+
this.fail("Upstream stream ended before the response completed");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Rebuilds a complete Anthropic message object from the Anthropic event stream. */
|
|
284
|
+
export class MessageAggregator {
|
|
285
|
+
constructor() {
|
|
286
|
+
this.message = null;
|
|
287
|
+
this.json = new Map();
|
|
288
|
+
this.error = null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
onEvent = (name, data) => {
|
|
292
|
+
switch (name) {
|
|
293
|
+
case "message_start":
|
|
294
|
+
this.message = { ...data.message, content: [] };
|
|
295
|
+
break;
|
|
296
|
+
case "content_block_start":
|
|
297
|
+
this.message.content[data.index] = { ...data.content_block };
|
|
298
|
+
break;
|
|
299
|
+
case "content_block_delta": {
|
|
300
|
+
const b = this.message.content[data.index];
|
|
301
|
+
const d = data.delta;
|
|
302
|
+
if (d.type === "text_delta") b.text += d.text;
|
|
303
|
+
else if (d.type === "thinking_delta") b.thinking += d.thinking;
|
|
304
|
+
else if (d.type === "signature_delta") b.signature = d.signature;
|
|
305
|
+
else if (d.type === "input_json_delta") this.json.set(data.index, (this.json.get(data.index) || "") + d.partial_json);
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
case "content_block_stop": {
|
|
309
|
+
const b = this.message.content[data.index];
|
|
310
|
+
if (b.type === "tool_use") {
|
|
311
|
+
try {
|
|
312
|
+
b.input = JSON.parse(this.json.get(data.index) || "{}");
|
|
313
|
+
} catch {
|
|
314
|
+
b.input = {};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
case "message_delta":
|
|
320
|
+
Object.assign(this.message, data.delta);
|
|
321
|
+
this.message.usage = mergeUsage(this.message.usage, data.usage);
|
|
322
|
+
break;
|
|
323
|
+
case "error":
|
|
324
|
+
this.error = data.error;
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
}
|