clauderipple 0.2.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/CHANGELOG.md +229 -0
- package/LICENSE +674 -0
- package/README.ko.md +328 -0
- package/README.md +372 -0
- package/bin/clauderipple.js +12 -0
- package/dist/app/assets/trayDownTemplate.png +0 -0
- package/dist/app/assets/trayDownTemplate@2x.png +0 -0
- package/dist/app/assets/trayTemplate.png +0 -0
- package/dist/app/assets/trayTemplate@2x.png +0 -0
- package/dist/app/assets/trayWarnTemplate.png +0 -0
- package/dist/app/assets/trayWarnTemplate@2x.png +0 -0
- package/dist/app/assets/trayWin.png +0 -0
- package/dist/app/assets/trayWin@2x.png +0 -0
- package/dist/app/assets/trayWinDown.png +0 -0
- package/dist/app/assets/trayWinDown@2x.png +0 -0
- package/dist/app/assets/trayWinWarn.png +0 -0
- package/dist/app/assets/trayWinWarn@2x.png +0 -0
- package/dist/app/dist/main.js +518 -0
- package/dist/cli/src/browser.js +21 -0
- package/dist/cli/src/bundle.js +51 -0
- package/dist/cli/src/certs.js +33 -0
- package/dist/cli/src/claude-auth.js +112 -0
- package/dist/cli/src/codex.js +172 -0
- package/dist/cli/src/gen-certs.js +7 -0
- package/dist/cli/src/hooks/agent-title.js +160 -0
- package/dist/cli/src/index.js +489 -0
- package/dist/cli/src/launchd.js +183 -0
- package/dist/cli/src/picker.js +166 -0
- package/dist/cli/src/probe.js +55 -0
- package/dist/cli/src/runtime.js +62 -0
- package/dist/cli/src/schtasks.js +134 -0
- package/dist/cli/src/settings.js +142 -0
- package/dist/cli/src/supervisor.js +100 -0
- package/dist/cli/src/tray.js +85 -0
- package/dist/router/src/admin.js +945 -0
- package/dist/router/src/bootstrap.js +80 -0
- package/dist/router/src/certs.js +65 -0
- package/dist/router/src/compat.js +172 -0
- package/dist/router/src/config.js +179 -0
- package/dist/router/src/health.js +45 -0
- package/dist/router/src/identity.js +51 -0
- package/dist/router/src/index.js +144 -0
- package/dist/router/src/ingress/models.js +29 -0
- package/dist/router/src/ingress/server.js +400 -0
- package/dist/router/src/ingress/translate.js +457 -0
- package/dist/router/src/log.js +81 -0
- package/dist/router/src/picker.js +74 -0
- package/dist/router/src/presets.js +267 -0
- package/dist/router/src/providers/anthropic-observed.js +88 -0
- package/dist/router/src/providers/anthropic-token-file.js +48 -0
- package/dist/router/src/providers/anthropic.js +203 -0
- package/dist/router/src/providers/chatgpt/auth.js +226 -0
- package/dist/router/src/providers/chatgpt/index.js +274 -0
- package/dist/router/src/providers/chatgpt/sse.js +28 -0
- package/dist/router/src/providers/chatgpt/translate.js +393 -0
- package/dist/router/src/providers/claude-oauth.js +252 -0
- package/dist/router/src/providers/openai/index.js +193 -0
- package/dist/router/src/providers/openai/translate.js +504 -0
- package/dist/router/src/proxy.js +724 -0
- package/dist/router/src/redact.js +43 -0
- package/dist/router/src/requestlog.js +346 -0
- package/dist/router/src/routing.js +113 -0
- package/dist/router/src/version.js +8 -0
- package/dist/router/src/x509.js +203 -0
- package/dist/ui/app.js +1228 -0
- package/dist/ui/i18n.js +95 -0
- package/dist/ui/index.html +104 -0
- package/dist/ui/presets-fallback.js +61 -0
- package/dist/ui/style.css +347 -0
- package/docs/ARCHITECTURE.md +441 -0
- package/package.json +66 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
// Anthropic Messages ⇄ OpenAI-compatible Chat Completions / Responses. Pure translation
|
|
2
|
+
// and streaming mapping. The generated prefix is deterministic so vendors that cache a
|
|
3
|
+
// stable prompt prefix can retain their native prompt-cache behavior.
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import { clampEffort } from "../../compat.js";
|
|
6
|
+
import { conversationKey, estimateTokens, normalizeSchema, systemText } from "../chatgpt/translate.js";
|
|
7
|
+
import { identityPrefix, instructionsSuffix } from "../../identity.js";
|
|
8
|
+
function textOf(content) {
|
|
9
|
+
if (typeof content === "string")
|
|
10
|
+
return content;
|
|
11
|
+
if (!Array.isArray(content))
|
|
12
|
+
return "";
|
|
13
|
+
return content
|
|
14
|
+
.flatMap((block) => block.type === "text" ? [String(block.text ?? "")] : block.type === "image" ? ["[image omitted]"] : [])
|
|
15
|
+
.filter((text) => text.length > 0)
|
|
16
|
+
.join("\n");
|
|
17
|
+
}
|
|
18
|
+
function imageUrl(block) {
|
|
19
|
+
const source = block.source;
|
|
20
|
+
if (!source || typeof source !== "object")
|
|
21
|
+
return null;
|
|
22
|
+
if (source.type === "base64" && typeof source.data === "string")
|
|
23
|
+
return `data:${typeof source.media_type === "string" ? source.media_type : "image/png"};base64,${source.data}`;
|
|
24
|
+
if (source.type === "url" && typeof source.url === "string")
|
|
25
|
+
return source.url;
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
function functionTools(tools) {
|
|
29
|
+
return (tools ?? [])
|
|
30
|
+
.filter((tool) => typeof tool.name === "string")
|
|
31
|
+
.map((tool) => ({ type: "function", function: { name: tool.name, description: tool.description ?? "", parameters: normalizeSchema(tool.input_schema) } }));
|
|
32
|
+
}
|
|
33
|
+
function mapToolChoice(req, tools) {
|
|
34
|
+
if (!tools.length)
|
|
35
|
+
return undefined;
|
|
36
|
+
const choice = req.tool_choice;
|
|
37
|
+
if (!choice || choice.type === "auto")
|
|
38
|
+
return "auto";
|
|
39
|
+
if (choice.type === "any")
|
|
40
|
+
return "required";
|
|
41
|
+
if (choice.type === "none")
|
|
42
|
+
return "none";
|
|
43
|
+
return choice.type === "tool" && choice.name ? { type: "function", function: { name: choice.name } } : undefined;
|
|
44
|
+
}
|
|
45
|
+
function mapResponsesToolChoice(req, tools) {
|
|
46
|
+
if (!tools.length)
|
|
47
|
+
return undefined;
|
|
48
|
+
const choice = req.tool_choice;
|
|
49
|
+
if (!choice || choice.type === "auto")
|
|
50
|
+
return "auto";
|
|
51
|
+
if (choice.type === "any")
|
|
52
|
+
return "required";
|
|
53
|
+
if (choice.type === "none")
|
|
54
|
+
return "none";
|
|
55
|
+
return choice.type === "tool" && choice.name ? { type: "function", name: choice.name } : undefined;
|
|
56
|
+
}
|
|
57
|
+
/** The system text this provider should see: what it is, the caller's prompt, the configured addendum. */
|
|
58
|
+
function systemWithIdentity(sys, opts) {
|
|
59
|
+
// The effort named is the one that survives the capability mapping; a provider that takes no
|
|
60
|
+
// reasoning effort is told none, rather than a level it will never see.
|
|
61
|
+
return [identityPrefix({ model: opts.model, effort: mappedEffort(opts), identity: opts.identity }), sys, instructionsSuffix({ model: opts.model, instructionsAppend: opts.instructionsAppend })]
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.join("\n\n");
|
|
64
|
+
}
|
|
65
|
+
function mappedEffort(opts) {
|
|
66
|
+
if (opts.caps?.reasoning !== "effort" || !opts.effort)
|
|
67
|
+
return undefined;
|
|
68
|
+
const levels = opts.caps.effortLevels;
|
|
69
|
+
return levels && levels.length > 0 ? clampEffort(opts.effort, levels) : opts.effort;
|
|
70
|
+
}
|
|
71
|
+
/** Convert every Anthropic message into OpenAI Chat Completion messages without inventing unstable text. */
|
|
72
|
+
export function toChatMessages(req, opts) {
|
|
73
|
+
const messages = [];
|
|
74
|
+
const sys = systemText(req.system);
|
|
75
|
+
const content = opts ? systemWithIdentity(sys, opts) : sys;
|
|
76
|
+
if (content)
|
|
77
|
+
messages.push({ role: "system", content });
|
|
78
|
+
const knownCalls = new Set();
|
|
79
|
+
for (const message of req.messages) {
|
|
80
|
+
const role = message.role === "assistant" ? "assistant" : "user";
|
|
81
|
+
if (typeof message.content === "string") {
|
|
82
|
+
if (message.content)
|
|
83
|
+
messages.push({ role, content: message.content });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (!Array.isArray(message.content))
|
|
87
|
+
continue;
|
|
88
|
+
if (role === "assistant") {
|
|
89
|
+
const text = [];
|
|
90
|
+
const calls = [];
|
|
91
|
+
for (const block of message.content) {
|
|
92
|
+
if (block.type === "text")
|
|
93
|
+
text.push(String(block.text ?? ""));
|
|
94
|
+
if (block.type === "tool_use") {
|
|
95
|
+
const call = block;
|
|
96
|
+
knownCalls.add(call.id);
|
|
97
|
+
calls.push({ id: call.id, type: "function", function: { name: call.name, arguments: typeof call.input === "string" ? call.input : JSON.stringify(call.input ?? {}) } });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (text.some(Boolean) || calls.length)
|
|
101
|
+
messages.push({ role: "assistant", content: text.join("\n") || null, ...(calls.length ? { tool_calls: calls } : {}) });
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
let parts = [];
|
|
105
|
+
const flush = () => {
|
|
106
|
+
if (!parts.length)
|
|
107
|
+
return;
|
|
108
|
+
const onlyText = parts.every((part) => part.type === "text");
|
|
109
|
+
messages.push({ role: "user", content: onlyText ? parts.map((part) => part.text).join("\n") : parts });
|
|
110
|
+
parts = [];
|
|
111
|
+
};
|
|
112
|
+
for (const block of message.content) {
|
|
113
|
+
if (block.type === "text") {
|
|
114
|
+
const text = String(block.text ?? "");
|
|
115
|
+
if (text)
|
|
116
|
+
parts.push({ type: "text", text });
|
|
117
|
+
}
|
|
118
|
+
else if (block.type === "image") {
|
|
119
|
+
const url = imageUrl(block);
|
|
120
|
+
if (url)
|
|
121
|
+
parts.push({ type: "image_url", image_url: { url } });
|
|
122
|
+
}
|
|
123
|
+
else if (block.type === "tool_result") {
|
|
124
|
+
flush();
|
|
125
|
+
const result = block;
|
|
126
|
+
let output = textOf(result.content);
|
|
127
|
+
if (result.is_error && !output)
|
|
128
|
+
output = "Tool execution failed";
|
|
129
|
+
if (knownCalls.has(result.tool_use_id))
|
|
130
|
+
messages.push({ role: "tool", tool_call_id: result.tool_use_id, content: output });
|
|
131
|
+
else
|
|
132
|
+
parts.push({ type: "text", text: `[Tool result]\n${output}` });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
flush();
|
|
136
|
+
}
|
|
137
|
+
return messages;
|
|
138
|
+
}
|
|
139
|
+
/** Translate to the stateless OpenAI Responses input grammar. */
|
|
140
|
+
export function toResponsesInput(req) {
|
|
141
|
+
const input = [];
|
|
142
|
+
const knownCalls = new Set();
|
|
143
|
+
for (const message of req.messages) {
|
|
144
|
+
const role = message.role === "assistant" ? "assistant" : "user";
|
|
145
|
+
if (typeof message.content === "string") {
|
|
146
|
+
if (message.content)
|
|
147
|
+
input.push({ type: "message", role, content: [{ type: role === "user" ? "input_text" : "output_text", text: message.content }] });
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (!Array.isArray(message.content))
|
|
151
|
+
continue;
|
|
152
|
+
let parts = [];
|
|
153
|
+
const flush = () => {
|
|
154
|
+
if (parts.length)
|
|
155
|
+
input.push({ type: "message", role, content: parts });
|
|
156
|
+
parts = [];
|
|
157
|
+
};
|
|
158
|
+
for (const block of message.content) {
|
|
159
|
+
if (block.type === "text") {
|
|
160
|
+
const text = String(block.text ?? "");
|
|
161
|
+
if (text)
|
|
162
|
+
parts.push({ type: role === "user" ? "input_text" : "output_text", text });
|
|
163
|
+
}
|
|
164
|
+
else if (block.type === "image" && role === "user") {
|
|
165
|
+
const url = imageUrl(block);
|
|
166
|
+
if (url)
|
|
167
|
+
parts.push({ type: "input_image", image_url: url });
|
|
168
|
+
}
|
|
169
|
+
else if (block.type === "tool_use") {
|
|
170
|
+
flush();
|
|
171
|
+
const call = block;
|
|
172
|
+
knownCalls.add(call.id);
|
|
173
|
+
input.push({ type: "function_call", call_id: call.id, name: call.name, arguments: typeof call.input === "string" ? call.input : JSON.stringify(call.input ?? {}) });
|
|
174
|
+
}
|
|
175
|
+
else if (block.type === "tool_result") {
|
|
176
|
+
flush();
|
|
177
|
+
const result = block;
|
|
178
|
+
let output = textOf(result.content);
|
|
179
|
+
if (result.is_error && !output)
|
|
180
|
+
output = "Tool execution failed";
|
|
181
|
+
if (knownCalls.has(result.tool_use_id))
|
|
182
|
+
input.push({ type: "function_call_output", call_id: result.tool_use_id, output });
|
|
183
|
+
else
|
|
184
|
+
parts.push({ type: "input_text", text: `[Tool result]\n${output}` });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
flush();
|
|
188
|
+
}
|
|
189
|
+
return input;
|
|
190
|
+
}
|
|
191
|
+
export function toOpenAiRequest(req, opts) {
|
|
192
|
+
const effort = mappedEffort(opts);
|
|
193
|
+
const tools = functionTools(req.tools);
|
|
194
|
+
const parallel = !(req.tool_choice?.disable_parallel_tool_use ?? false);
|
|
195
|
+
if (opts.wire === "chat") {
|
|
196
|
+
const out = {
|
|
197
|
+
model: opts.model,
|
|
198
|
+
messages: toChatMessages(req, opts),
|
|
199
|
+
stream: true,
|
|
200
|
+
stream_options: { include_usage: true },
|
|
201
|
+
};
|
|
202
|
+
if (tools.length) {
|
|
203
|
+
out.tools = tools;
|
|
204
|
+
out.parallel_tool_calls = parallel;
|
|
205
|
+
const choice = mapToolChoice(req, tools);
|
|
206
|
+
if (choice)
|
|
207
|
+
out.tool_choice = choice;
|
|
208
|
+
}
|
|
209
|
+
if (typeof req.max_tokens === "number")
|
|
210
|
+
out.max_tokens = req.max_tokens;
|
|
211
|
+
if (typeof req.temperature === "number")
|
|
212
|
+
out.temperature = req.temperature;
|
|
213
|
+
if (Array.isArray(req.stop_sequences) && req.stop_sequences.every((value) => typeof value === "string"))
|
|
214
|
+
out.stop = req.stop_sequences;
|
|
215
|
+
if (effort)
|
|
216
|
+
out.reasoning_effort = effort;
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
const responseTools = tools.map(({ function: fn }) => ({ type: "function", ...fn }));
|
|
220
|
+
const out = {
|
|
221
|
+
model: opts.model,
|
|
222
|
+
input: toResponsesInput(req),
|
|
223
|
+
stream: true,
|
|
224
|
+
};
|
|
225
|
+
const instructions = systemWithIdentity(systemText(req.system), opts);
|
|
226
|
+
if (instructions)
|
|
227
|
+
out.instructions = instructions;
|
|
228
|
+
if (responseTools.length) {
|
|
229
|
+
out.tools = responseTools;
|
|
230
|
+
out.parallel_tool_calls = parallel;
|
|
231
|
+
const choice = mapResponsesToolChoice(req, responseTools);
|
|
232
|
+
if (choice)
|
|
233
|
+
out.tool_choice = choice;
|
|
234
|
+
}
|
|
235
|
+
if (typeof req.max_tokens === "number")
|
|
236
|
+
out.max_output_tokens = req.max_tokens;
|
|
237
|
+
if (typeof req.temperature === "number")
|
|
238
|
+
out.temperature = req.temperature;
|
|
239
|
+
if (effort)
|
|
240
|
+
out.reasoning = { effort };
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
export { conversationKey, estimateTokens };
|
|
244
|
+
/** Maps standard OpenAI Chat Completion and Responses SSE records to Anthropic Messages SSE. */
|
|
245
|
+
export class OpenAiStreamMapper {
|
|
246
|
+
started = false;
|
|
247
|
+
finished = false;
|
|
248
|
+
blockIndex = -1;
|
|
249
|
+
open = null;
|
|
250
|
+
sawTool = false;
|
|
251
|
+
finishReason;
|
|
252
|
+
responseItems = [];
|
|
253
|
+
responseItemsById = new Map();
|
|
254
|
+
messageId = `msg_${crypto.randomBytes(12).toString("hex")}`;
|
|
255
|
+
content = [];
|
|
256
|
+
usage = { input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 };
|
|
257
|
+
stopReason = "end_turn";
|
|
258
|
+
model;
|
|
259
|
+
startInput;
|
|
260
|
+
constructor(model, startInput = 0) {
|
|
261
|
+
this.model = model;
|
|
262
|
+
this.startInput = startInput;
|
|
263
|
+
}
|
|
264
|
+
get isFinished() { return this.finished; }
|
|
265
|
+
start() {
|
|
266
|
+
if (this.started)
|
|
267
|
+
return [];
|
|
268
|
+
this.started = true;
|
|
269
|
+
return [{ event: "message_start", data: { type: "message_start", message: { id: this.messageId, type: "message", role: "assistant", model: this.model, content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: this.startInput, output_tokens: 0, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 } } } }];
|
|
270
|
+
}
|
|
271
|
+
closeBlock() {
|
|
272
|
+
if (!this.open)
|
|
273
|
+
return [];
|
|
274
|
+
const index = this.open.index;
|
|
275
|
+
this.open = null;
|
|
276
|
+
return [{ event: "content_block_stop", data: { type: "content_block_stop", index } }];
|
|
277
|
+
}
|
|
278
|
+
openBlock(kind, contentBlock, toolIndex) {
|
|
279
|
+
const out = this.closeBlock();
|
|
280
|
+
const index = ++this.blockIndex;
|
|
281
|
+
this.open = { kind, index, ...(toolIndex === undefined ? {} : { toolIndex }) };
|
|
282
|
+
out.push({ event: "content_block_start", data: { type: "content_block_start", index, content_block: contentBlock } });
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
setUsage(value) {
|
|
286
|
+
if (!value || typeof value !== "object")
|
|
287
|
+
return;
|
|
288
|
+
const usage = value;
|
|
289
|
+
const input = typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : typeof usage.input_tokens === "number" ? usage.input_tokens : undefined;
|
|
290
|
+
const output = typeof usage.completion_tokens === "number" ? usage.completion_tokens : typeof usage.output_tokens === "number" ? usage.output_tokens : undefined;
|
|
291
|
+
if (input === undefined && output === undefined)
|
|
292
|
+
return;
|
|
293
|
+
const cachedValue = usage.prompt_tokens_details?.cached_tokens ?? usage.input_tokens_details?.cached_tokens;
|
|
294
|
+
const cached = typeof cachedValue === "number" && Number.isFinite(cachedValue) ? Math.max(0, cachedValue) : 0;
|
|
295
|
+
this.usage = { input_tokens: Math.max(0, (input ?? this.usage.input_tokens + cached) - cached), output_tokens: Math.max(0, output ?? this.usage.output_tokens), cache_read_input_tokens: cached, cache_creation_input_tokens: 0 };
|
|
296
|
+
}
|
|
297
|
+
toolFor(index, delta) {
|
|
298
|
+
let tool = this.content.find((block) => block.type === "tool_use" && block.index === index);
|
|
299
|
+
if (!tool) {
|
|
300
|
+
const id = typeof delta.id === "string" ? delta.id : `call_${crypto.randomBytes(8).toString("hex")}`;
|
|
301
|
+
const name = typeof delta.function?.name === "string" ? delta.function.name : "tool";
|
|
302
|
+
tool = { type: "tool_use", id, name, input: {}, args: "", index };
|
|
303
|
+
this.content.push(tool);
|
|
304
|
+
this.sawTool = true;
|
|
305
|
+
}
|
|
306
|
+
if (typeof delta.id === "string")
|
|
307
|
+
tool.id = delta.id;
|
|
308
|
+
if (typeof delta.function?.name === "string")
|
|
309
|
+
tool.name = delta.function.name;
|
|
310
|
+
return tool;
|
|
311
|
+
}
|
|
312
|
+
feedChat(ev) {
|
|
313
|
+
const out = [...this.start()];
|
|
314
|
+
const usage = ev.usage;
|
|
315
|
+
if (usage)
|
|
316
|
+
this.setUsage(usage);
|
|
317
|
+
const choices = Array.isArray(ev.choices) ? ev.choices : [];
|
|
318
|
+
for (const choice of choices) {
|
|
319
|
+
const delta = choice.delta ?? {};
|
|
320
|
+
if (typeof delta.content === "string" && delta.content) {
|
|
321
|
+
if (!this.open || this.open.kind !== "text") {
|
|
322
|
+
this.content.push({ type: "text", text: "" });
|
|
323
|
+
out.push(...this.openBlock("text", { type: "text", text: "" }));
|
|
324
|
+
}
|
|
325
|
+
const last = this.content[this.content.length - 1];
|
|
326
|
+
if (last?.type === "text")
|
|
327
|
+
last.text += delta.content;
|
|
328
|
+
out.push({ event: "content_block_delta", data: { type: "content_block_delta", index: this.open.index, delta: { type: "text_delta", text: delta.content } } });
|
|
329
|
+
}
|
|
330
|
+
if (Array.isArray(delta.tool_calls)) {
|
|
331
|
+
for (const raw of delta.tool_calls) {
|
|
332
|
+
if (!raw || typeof raw !== "object")
|
|
333
|
+
continue;
|
|
334
|
+
const call = raw;
|
|
335
|
+
const index = typeof call.index === "number" ? call.index : 0;
|
|
336
|
+
const tool = this.toolFor(index, call);
|
|
337
|
+
if (!this.open || this.open.kind !== "tool" || this.open.toolIndex !== index)
|
|
338
|
+
out.push(...this.openBlock("tool", { type: "tool_use", id: tool.id, name: tool.name, input: {} }, index));
|
|
339
|
+
const args = call.function?.arguments;
|
|
340
|
+
if (typeof args === "string" && args) {
|
|
341
|
+
tool.args += args;
|
|
342
|
+
out.push({ event: "content_block_delta", data: { type: "content_block_delta", index: this.open.index, delta: { type: "input_json_delta", partial_json: args } } });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (typeof choice.finish_reason === "string" && choice.finish_reason)
|
|
347
|
+
this.finishReason = choice.finish_reason;
|
|
348
|
+
}
|
|
349
|
+
return out;
|
|
350
|
+
}
|
|
351
|
+
responseItem(id, kind) {
|
|
352
|
+
if (typeof id === "string")
|
|
353
|
+
return this.responseItemsById.get(id);
|
|
354
|
+
// Standard Responses SSE includes item_id. A few compatible servers omit it for
|
|
355
|
+
// a single in-flight item; support only the unambiguous form, never guess across
|
|
356
|
+
// parallel calls.
|
|
357
|
+
const candidates = this.responseItems.filter((item) => !item.done && (!kind || item.kind === kind));
|
|
358
|
+
return candidates.length === 1 ? candidates[0] : undefined;
|
|
359
|
+
}
|
|
360
|
+
flushResponses() {
|
|
361
|
+
const out = [];
|
|
362
|
+
for (const item of this.responseItems) {
|
|
363
|
+
if (item.emitted) {
|
|
364
|
+
if (!item.done)
|
|
365
|
+
break;
|
|
366
|
+
if (!item.closed) {
|
|
367
|
+
out.push(...this.closeBlock());
|
|
368
|
+
item.closed = true;
|
|
369
|
+
}
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (item.kind === "text") {
|
|
373
|
+
const text = item.block;
|
|
374
|
+
out.push(...this.openBlock("text", { type: "text", text: "" }));
|
|
375
|
+
if (text.text) {
|
|
376
|
+
out.push({ event: "content_block_delta", data: { type: "content_block_delta", index: this.open.index, delta: { type: "text_delta", text: text.text } } });
|
|
377
|
+
}
|
|
378
|
+
item.emittedText = text.text.length;
|
|
379
|
+
item.emitted = true;
|
|
380
|
+
if (!item.done)
|
|
381
|
+
break;
|
|
382
|
+
out.push(...this.closeBlock());
|
|
383
|
+
item.closed = true;
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
const tool = item.block;
|
|
387
|
+
out.push(...this.openBlock("tool", { type: "tool_use", id: tool.id, name: tool.name, input: {} }, tool.index));
|
|
388
|
+
if (tool.args) {
|
|
389
|
+
out.push({ event: "content_block_delta", data: { type: "content_block_delta", index: this.open.index, delta: { type: "input_json_delta", partial_json: tool.args } } });
|
|
390
|
+
}
|
|
391
|
+
item.emittedText = tool.args.length;
|
|
392
|
+
item.emitted = true;
|
|
393
|
+
if (!item.done)
|
|
394
|
+
break;
|
|
395
|
+
out.push(...this.closeBlock());
|
|
396
|
+
item.closed = true;
|
|
397
|
+
}
|
|
398
|
+
return out;
|
|
399
|
+
}
|
|
400
|
+
feedResponses(ev) {
|
|
401
|
+
const out = [...this.start()];
|
|
402
|
+
const type = typeof ev.type === "string" ? ev.type : "";
|
|
403
|
+
if (type === "response.output_item.added") {
|
|
404
|
+
const item = ev.item;
|
|
405
|
+
const id = typeof item?.id === "string" ? item.id : undefined;
|
|
406
|
+
if (id && item?.type === "message") {
|
|
407
|
+
const block = { type: "text", text: "" };
|
|
408
|
+
this.content.push(block);
|
|
409
|
+
const responseItem = { id, kind: "text", block, done: false, emitted: false, closed: false, emittedText: 0 };
|
|
410
|
+
this.responseItems.push(responseItem);
|
|
411
|
+
this.responseItemsById.set(id, responseItem);
|
|
412
|
+
}
|
|
413
|
+
else if (id && item?.type === "function_call") {
|
|
414
|
+
const index = this.content.filter((block) => block.type === "tool_use").length;
|
|
415
|
+
const tool = this.toolFor(index, { id: item.call_id ?? item.id, function: { name: item.name } });
|
|
416
|
+
const responseItem = { id, kind: "tool", block: tool, done: false, emitted: false, closed: false, emittedText: 0 };
|
|
417
|
+
this.responseItems.push(responseItem);
|
|
418
|
+
this.responseItemsById.set(id, responseItem);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
else if (type === "response.output_text.delta") {
|
|
422
|
+
const text = typeof ev.delta === "string" ? ev.delta : "";
|
|
423
|
+
const item = this.responseItem(ev.item_id, "text");
|
|
424
|
+
if (item?.kind === "text" && text) {
|
|
425
|
+
const block = item.block;
|
|
426
|
+
block.text += text;
|
|
427
|
+
if (item.emitted && this.open?.kind === "text") {
|
|
428
|
+
out.push({ event: "content_block_delta", data: { type: "content_block_delta", index: this.open.index, delta: { type: "text_delta", text } } });
|
|
429
|
+
item.emittedText += text.length;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
else if (type === "response.function_call_arguments.delta") {
|
|
434
|
+
const text = typeof ev.delta === "string" ? ev.delta : "";
|
|
435
|
+
const item = this.responseItem(ev.item_id, "tool");
|
|
436
|
+
if (item?.kind === "tool" && text) {
|
|
437
|
+
const tool = item.block;
|
|
438
|
+
tool.args += text;
|
|
439
|
+
if (item.emitted && this.open?.kind === "tool" && this.open.toolIndex === tool.index) {
|
|
440
|
+
out.push({ event: "content_block_delta", data: { type: "content_block_delta", index: this.open.index, delta: { type: "input_json_delta", partial_json: text } } });
|
|
441
|
+
item.emittedText += text.length;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
else if (type === "response.output_item.done") {
|
|
446
|
+
const item = this.responseItem(ev.item_id ?? ev.item?.id);
|
|
447
|
+
if (item)
|
|
448
|
+
item.done = true;
|
|
449
|
+
}
|
|
450
|
+
else if (type === "response.completed" || type === "response.incomplete") {
|
|
451
|
+
const response = ev.response;
|
|
452
|
+
this.setUsage(response?.usage);
|
|
453
|
+
if (response?.incomplete_details?.reason === "max_output_tokens")
|
|
454
|
+
this.finishReason = "length";
|
|
455
|
+
}
|
|
456
|
+
else if (type === "response.failed" || type === "error") {
|
|
457
|
+
const error = (type === "error" ? ev.error : ev.response?.error);
|
|
458
|
+
return [...out, ...this.fail(typeof error?.message === "string" ? error.message : "upstream response failed", typeof error?.code === "string" ? error.code : undefined)];
|
|
459
|
+
}
|
|
460
|
+
out.push(...this.flushResponses());
|
|
461
|
+
return out;
|
|
462
|
+
}
|
|
463
|
+
feed(ev, wire) {
|
|
464
|
+
if (this.finished)
|
|
465
|
+
return [];
|
|
466
|
+
// Some OpenAI-compatible servers emit an ordinary `{error:{...}}` JSON SSE payload
|
|
467
|
+
// instead of the Responses `error` event shape. Do not finish it as a successful answer.
|
|
468
|
+
if (ev.error && typeof ev.error === "object") {
|
|
469
|
+
const error = ev.error;
|
|
470
|
+
return [...this.start(), ...this.fail(typeof error.message === "string" ? error.message : "upstream error", typeof error.code === "string" ? error.code : undefined)];
|
|
471
|
+
}
|
|
472
|
+
return wire === "chat" ? this.feedChat(ev) : this.feedResponses(ev);
|
|
473
|
+
}
|
|
474
|
+
finish() {
|
|
475
|
+
if (this.finished)
|
|
476
|
+
return [];
|
|
477
|
+
this.finished = true;
|
|
478
|
+
this.stopReason = this.sawTool || this.finishReason === "tool_calls" ? "tool_use" : this.finishReason === "length" || this.finishReason === "max_output_tokens" ? "max_tokens" : "end_turn";
|
|
479
|
+
return [...this.start(), ...this.closeBlock(), { event: "message_delta", data: { type: "message_delta", delta: { stop_reason: this.stopReason, stop_sequence: null }, usage: this.usage } }, { event: "message_stop", data: { type: "message_stop" } }];
|
|
480
|
+
}
|
|
481
|
+
fail(message, code) {
|
|
482
|
+
if (this.finished)
|
|
483
|
+
return [];
|
|
484
|
+
this.finished = true;
|
|
485
|
+
const type = code === "rate_limit_exceeded" ? "rate_limit_error" : "api_error";
|
|
486
|
+
return [...this.start(), ...this.closeBlock(), { event: "error", data: { type: "error", error: { type, message } } }];
|
|
487
|
+
}
|
|
488
|
+
message() {
|
|
489
|
+
const content = this.content.map((block) => {
|
|
490
|
+
if (block.type !== "tool_use")
|
|
491
|
+
return block;
|
|
492
|
+
let input = {};
|
|
493
|
+
try {
|
|
494
|
+
input = block.args ? JSON.parse(block.args) : {};
|
|
495
|
+
}
|
|
496
|
+
catch { /* malformed vendor arguments become an empty tool input */ }
|
|
497
|
+
return { type: "tool_use", id: block.id, name: block.name, input };
|
|
498
|
+
});
|
|
499
|
+
return { id: this.messageId, type: "message", role: "assistant", model: this.model, content, stop_reason: this.stopReason, stop_sequence: null, usage: this.usage };
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
export function formatSse(event) {
|
|
503
|
+
return `event: ${event.event}\ndata: ${JSON.stringify(event.data)}\n\n`;
|
|
504
|
+
}
|