babelgate 0.0.0 → 1.1.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/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +467 -1
- package/dist/app.js +40 -0
- package/dist/cli.js +203 -0
- package/dist/config/env-config.js +99 -0
- package/dist/index.js +60 -0
- package/dist/middleware/error-handler.js +158 -0
- package/dist/middleware/request-logger.js +66 -0
- package/dist/middleware/require-api-key.js +80 -0
- package/dist/middleware/resolve-source.js +28 -0
- package/dist/routes/proxy.js +74 -0
- package/dist/services/http-client.js +220 -0
- package/dist/services/proxy.service.js +329 -0
- package/dist/services/stream-pipeline.js +119 -0
- package/dist/services/upstream-errors.js +34 -0
- package/dist/transformers/adapters/anthropic.adapter.js +292 -0
- package/dist/transformers/adapters/anthropic.messages.js +261 -0
- package/dist/transformers/adapters/anthropic.stream.js +378 -0
- package/dist/transformers/adapters/gemini.adapter.js +418 -0
- package/dist/transformers/adapters/gemini.schema.js +61 -0
- package/dist/transformers/adapters/gemini.stream.js +341 -0
- package/dist/transformers/adapters/gemini.tools.js +246 -0
- package/dist/transformers/adapters/index.js +141 -0
- package/dist/transformers/adapters/ollama.adapter.js +288 -0
- package/dist/transformers/adapters/ollama.stream.js +294 -0
- package/dist/transformers/adapters/ollama.tools.js +107 -0
- package/dist/transformers/adapters/openai.adapter.js +460 -0
- package/dist/transformers/adapters/openai.stream.js +229 -0
- package/dist/transformers/index.js +110 -0
- package/dist/types/anthropic.types.js +2 -0
- package/dist/types/canonical.schema.js +160 -0
- package/dist/types/canonical.types.js +8 -0
- package/dist/types/gemini.types.js +2 -0
- package/dist/types/ollama.types.js +4 -0
- package/dist/types/openai.types.js +2 -0
- package/dist/utils/canonical-error.js +77 -0
- package/dist/utils/errors.js +95 -0
- package/dist/utils/ids.js +17 -0
- package/dist/utils/logger.js +21 -0
- package/dist/utils/model-map.js +39 -0
- package/dist/utils/sse.js +147 -0
- package/dist/utils/stream-reassemble.js +104 -0
- package/dist/utils/token-estimate.js +36 -0
- package/dist/utils/tool-args.js +26 -0
- package/package.json +54 -7
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.anthropicAdapter = void 0;
|
|
4
|
+
exports.toCanonicalRequest = toCanonicalRequest;
|
|
5
|
+
exports.fromCanonicalRequest = fromCanonicalRequest;
|
|
6
|
+
exports.toCanonicalResponse = toCanonicalResponse;
|
|
7
|
+
exports.mapFinishReason = mapFinishReason;
|
|
8
|
+
exports.fromCanonicalResponse = fromCanonicalResponse;
|
|
9
|
+
exports.toCanonicalModelList = toCanonicalModelList;
|
|
10
|
+
exports.fromCanonicalModelList = fromCanonicalModelList;
|
|
11
|
+
exports.mapToAnthropicStopReason = mapToAnthropicStopReason;
|
|
12
|
+
exports.toCanonicalError = toCanonicalError;
|
|
13
|
+
exports.fromCanonicalError = fromCanonicalError;
|
|
14
|
+
const crypto_1 = require("crypto");
|
|
15
|
+
const canonical_error_1 = require("../../utils/canonical-error");
|
|
16
|
+
const logger_1 = require("../../utils/logger");
|
|
17
|
+
const anthropic_messages_1 = require("./anthropic.messages");
|
|
18
|
+
const anthropic_stream_1 = require("./anthropic.stream");
|
|
19
|
+
exports.anthropicAdapter = {
|
|
20
|
+
toCanonicalRequest,
|
|
21
|
+
fromCanonicalRequest,
|
|
22
|
+
toCanonicalResponse,
|
|
23
|
+
fromCanonicalResponse,
|
|
24
|
+
toCanonicalModelList,
|
|
25
|
+
fromCanonicalModelList,
|
|
26
|
+
toCanonicalError,
|
|
27
|
+
fromCanonicalError,
|
|
28
|
+
createStreamDecoder: anthropic_stream_1.createStreamDecoder,
|
|
29
|
+
createStreamEncoder: anthropic_stream_1.createStreamEncoder,
|
|
30
|
+
};
|
|
31
|
+
// ============ REQUEST: Anthropic → Canonical ============
|
|
32
|
+
/**
|
|
33
|
+
* Claude Code fields with no canonical equivalent are dropped safely
|
|
34
|
+
* (spec §3.5) — a debug line names the dropped thing, never its content.
|
|
35
|
+
*/
|
|
36
|
+
function logDropped(request) {
|
|
37
|
+
if (request.thinking !== undefined) {
|
|
38
|
+
logger_1.logger.debug('Dropping request field "thinking" (no canonical equivalent)');
|
|
39
|
+
}
|
|
40
|
+
if (request.metadata !== undefined) {
|
|
41
|
+
logger_1.logger.debug('Dropping request field "metadata" (no canonical equivalent)');
|
|
42
|
+
}
|
|
43
|
+
if (hasCacheControl(request)) {
|
|
44
|
+
logger_1.logger.debug('Dropping "cache_control" fields (no canonical equivalent)');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** cache_control may sit on any block, tool or system part (§3.5). */
|
|
48
|
+
function hasCacheControl(value) {
|
|
49
|
+
if (Array.isArray(value))
|
|
50
|
+
return value.some(hasCacheControl);
|
|
51
|
+
if (value !== null && typeof value === "object") {
|
|
52
|
+
const obj = value;
|
|
53
|
+
if ("cache_control" in obj)
|
|
54
|
+
return true;
|
|
55
|
+
return Object.values(obj).some(hasCacheControl);
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
function toCanonicalRequest(request) {
|
|
60
|
+
logDropped(request);
|
|
61
|
+
const messages = request.messages.flatMap((m) => (0, anthropic_messages_1.toCanonicalMessage)(m));
|
|
62
|
+
// §3.5: a system array's text blocks join with a blank line.
|
|
63
|
+
const systemPrompt = !request.system
|
|
64
|
+
? undefined
|
|
65
|
+
: typeof request.system === "string"
|
|
66
|
+
? request.system
|
|
67
|
+
: request.system
|
|
68
|
+
.filter((b) => b.type === "text")
|
|
69
|
+
.map((b) => b.text)
|
|
70
|
+
.join("\n\n");
|
|
71
|
+
// Add system as first message if present
|
|
72
|
+
if (systemPrompt !== undefined) {
|
|
73
|
+
messages.unshift({
|
|
74
|
+
role: "system",
|
|
75
|
+
content: systemPrompt,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const result = {
|
|
79
|
+
model: request.model,
|
|
80
|
+
messages,
|
|
81
|
+
// Canonical treats absent and stream:false identically — only an
|
|
82
|
+
// explicit true turns streaming on.
|
|
83
|
+
...(request.stream === true && { stream: true }),
|
|
84
|
+
...(request.tools && { tools: request.tools.map(anthropic_messages_1.toCanonicalTool) }),
|
|
85
|
+
...(request.tool_choice && {
|
|
86
|
+
toolChoice: (0, anthropic_messages_1.toCanonicalToolChoice)(request.tool_choice),
|
|
87
|
+
}),
|
|
88
|
+
...(systemPrompt !== undefined && { systemPrompt }),
|
|
89
|
+
config: {
|
|
90
|
+
maxTokens: request.max_tokens,
|
|
91
|
+
...(request.temperature !== undefined && {
|
|
92
|
+
temperature: request.temperature,
|
|
93
|
+
}),
|
|
94
|
+
...(request.top_p !== undefined && { topP: request.top_p }),
|
|
95
|
+
...(request.top_k !== undefined && { topK: request.top_k }),
|
|
96
|
+
...(request.stop_sequences && { stopSequences: request.stop_sequences }),
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
// ============ REQUEST: Canonical → Anthropic ============
|
|
102
|
+
function fromCanonicalRequest(request) {
|
|
103
|
+
const nonSystem = request.messages.filter((m) => m.role !== "system");
|
|
104
|
+
const messages = [];
|
|
105
|
+
for (let i = 0; i < nonSystem.length; i++) {
|
|
106
|
+
const m = nonSystem[i];
|
|
107
|
+
if (m.role === "tool") {
|
|
108
|
+
// Merge the run of consecutive tool messages into ONE user
|
|
109
|
+
// message of tool_result blocks (spec §3.4).
|
|
110
|
+
const results = [];
|
|
111
|
+
while (i < nonSystem.length && nonSystem[i].role === "tool") {
|
|
112
|
+
results.push((0, anthropic_messages_1.toToolResultBlock)(nonSystem[i]));
|
|
113
|
+
i++;
|
|
114
|
+
}
|
|
115
|
+
i--;
|
|
116
|
+
messages.push({ role: "user", content: results });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
messages.push((0, anthropic_messages_1.toMessageParam)(m));
|
|
120
|
+
}
|
|
121
|
+
const systemPrompt = request.systemPrompt ||
|
|
122
|
+
request.messages.find((m) => m.role === "system")?.content;
|
|
123
|
+
const result = {
|
|
124
|
+
model: request.model,
|
|
125
|
+
messages,
|
|
126
|
+
max_tokens: request.config?.maxTokens ?? 4096,
|
|
127
|
+
...(systemPrompt &&
|
|
128
|
+
typeof systemPrompt === "string" && { system: systemPrompt }),
|
|
129
|
+
...(request.config?.temperature !== undefined && {
|
|
130
|
+
temperature: request.config.temperature,
|
|
131
|
+
}),
|
|
132
|
+
...(request.config?.topP !== undefined && { top_p: request.config.topP }),
|
|
133
|
+
...(request.config?.topK !== undefined && { top_k: request.config.topK }),
|
|
134
|
+
...(request.config?.stopSequences && {
|
|
135
|
+
stop_sequences: request.config.stopSequences,
|
|
136
|
+
}),
|
|
137
|
+
...(request.tools && { tools: request.tools.map(anthropic_messages_1.toAnthropicTool) }),
|
|
138
|
+
...(request.toolChoice && {
|
|
139
|
+
tool_choice: (0, anthropic_messages_1.toAnthropicToolChoice)(request.toolChoice),
|
|
140
|
+
}),
|
|
141
|
+
};
|
|
142
|
+
if (request.stream !== undefined) {
|
|
143
|
+
result.stream = request.stream;
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
// ============ RESPONSE: Anthropic → Canonical ============
|
|
148
|
+
function toCanonicalResponse(response, model) {
|
|
149
|
+
const textContent = response.content
|
|
150
|
+
.filter((block) => block.type === "text")
|
|
151
|
+
.map((block) => block.text)
|
|
152
|
+
.join("");
|
|
153
|
+
const toolCalls = response.content
|
|
154
|
+
.filter((block) => block.type === "tool_use")
|
|
155
|
+
.map((block) => ({
|
|
156
|
+
id: block.id,
|
|
157
|
+
type: "function",
|
|
158
|
+
function: {
|
|
159
|
+
name: block.name,
|
|
160
|
+
arguments: JSON.stringify(block.input ?? {}),
|
|
161
|
+
},
|
|
162
|
+
}));
|
|
163
|
+
return {
|
|
164
|
+
id: response.id || `anthropic-${(0, crypto_1.randomUUID)()}`,
|
|
165
|
+
model,
|
|
166
|
+
message: {
|
|
167
|
+
role: "assistant",
|
|
168
|
+
content: textContent,
|
|
169
|
+
...(toolCalls.length > 0 && { toolCalls }),
|
|
170
|
+
},
|
|
171
|
+
finishReason: mapFinishReason(response.stop_reason),
|
|
172
|
+
usage: {
|
|
173
|
+
inputTokens: response.usage.input_tokens,
|
|
174
|
+
outputTokens: response.usage.output_tokens,
|
|
175
|
+
totalTokens: response.usage.input_tokens + response.usage.output_tokens,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function mapFinishReason(reason) {
|
|
180
|
+
switch (reason) {
|
|
181
|
+
case "end_turn":
|
|
182
|
+
return "stop";
|
|
183
|
+
case "max_tokens":
|
|
184
|
+
return "length";
|
|
185
|
+
case "tool_use":
|
|
186
|
+
return "tool_call";
|
|
187
|
+
default:
|
|
188
|
+
return "stop";
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// ============ RESPONSE: Canonical → Anthropic ============
|
|
192
|
+
function fromCanonicalResponse(response) {
|
|
193
|
+
const text = typeof response.message.content === "string"
|
|
194
|
+
? response.message.content
|
|
195
|
+
: "";
|
|
196
|
+
// Text comes before tool_use blocks (spec §3.4).
|
|
197
|
+
const content = [];
|
|
198
|
+
if (text) {
|
|
199
|
+
content.push({ type: "text", text });
|
|
200
|
+
}
|
|
201
|
+
for (const tc of response.message.toolCalls ?? []) {
|
|
202
|
+
content.push((0, anthropic_messages_1.toToolUseBlock)(tc));
|
|
203
|
+
}
|
|
204
|
+
if (content.length === 0) {
|
|
205
|
+
content.push({ type: "text", text: "" });
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
id: response.id || `msg_${(0, crypto_1.randomUUID)()}`,
|
|
209
|
+
type: "message",
|
|
210
|
+
role: "assistant",
|
|
211
|
+
content: content,
|
|
212
|
+
model: response.model,
|
|
213
|
+
stop_reason: mapToAnthropicStopReason(response.finishReason),
|
|
214
|
+
stop_sequence: null,
|
|
215
|
+
usage: {
|
|
216
|
+
input_tokens: response.usage?.inputTokens ?? 0,
|
|
217
|
+
output_tokens: response.usage?.outputTokens ?? 0,
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function toCanonicalModelList(response) {
|
|
222
|
+
const res = response;
|
|
223
|
+
return {
|
|
224
|
+
models: (res.data ?? [])
|
|
225
|
+
.filter((m) => Boolean(m.id))
|
|
226
|
+
.map((m) => ({
|
|
227
|
+
id: m.id,
|
|
228
|
+
displayName: m.display_name,
|
|
229
|
+
// ISO timestamp → Unix seconds
|
|
230
|
+
created: m.created_at
|
|
231
|
+
? Math.floor(Date.parse(m.created_at) / 1000)
|
|
232
|
+
: undefined,
|
|
233
|
+
})),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function fromCanonicalModelList(list) {
|
|
237
|
+
return {
|
|
238
|
+
data: list.models.map((m) => ({
|
|
239
|
+
id: m.id,
|
|
240
|
+
display_name: m.displayName,
|
|
241
|
+
created_at: m.created
|
|
242
|
+
? new Date(m.created * 1000).toISOString()
|
|
243
|
+
: undefined,
|
|
244
|
+
type: "model",
|
|
245
|
+
})),
|
|
246
|
+
has_more: false,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function mapToAnthropicStopReason(reason) {
|
|
250
|
+
switch (reason) {
|
|
251
|
+
case "stop":
|
|
252
|
+
return "end_turn";
|
|
253
|
+
case "length":
|
|
254
|
+
return "max_tokens";
|
|
255
|
+
case "tool_call":
|
|
256
|
+
return "tool_use";
|
|
257
|
+
default:
|
|
258
|
+
return "end_turn";
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// ============ ERRORS ============
|
|
262
|
+
const ANTHROPIC_ERROR_TYPE = {
|
|
263
|
+
invalid_request: "invalid_request_error",
|
|
264
|
+
authentication: "authentication_error",
|
|
265
|
+
permission: "permission_error",
|
|
266
|
+
not_found: "not_found_error",
|
|
267
|
+
request_too_large: "request_too_large",
|
|
268
|
+
rate_limit: "rate_limit_error",
|
|
269
|
+
unsupported: "api_error",
|
|
270
|
+
upstream: "api_error",
|
|
271
|
+
unavailable: "overloaded_error",
|
|
272
|
+
timeout: "timeout_error",
|
|
273
|
+
internal: "api_error",
|
|
274
|
+
};
|
|
275
|
+
function toCanonicalError(status, body) {
|
|
276
|
+
const err = body?.error;
|
|
277
|
+
const message = typeof err?.message === "string" && err.message.length > 0
|
|
278
|
+
? err.message
|
|
279
|
+
: `Upstream anthropic returned ${status}`;
|
|
280
|
+
return (0, canonical_error_1.makeCanonicalError)((0, canonical_error_1.kindFromStatus)(status), message);
|
|
281
|
+
}
|
|
282
|
+
function fromCanonicalError(err) {
|
|
283
|
+
return {
|
|
284
|
+
type: "error",
|
|
285
|
+
error: {
|
|
286
|
+
type: ANTHROPIC_ERROR_TYPE[err.kind],
|
|
287
|
+
message: err.message,
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
// Message/tool conversion lives in ./anthropic.messages; streaming in
|
|
292
|
+
// ./anthropic.stream — re-exported on the adapter above.
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toCanonicalMessage = toCanonicalMessage;
|
|
4
|
+
exports.toCanonicalTool = toCanonicalTool;
|
|
5
|
+
exports.toCanonicalToolChoice = toCanonicalToolChoice;
|
|
6
|
+
exports.toMessageParam = toMessageParam;
|
|
7
|
+
exports.toToolResultBlock = toToolResultBlock;
|
|
8
|
+
exports.toToolUseBlock = toToolUseBlock;
|
|
9
|
+
exports.toAnthropicTool = toAnthropicTool;
|
|
10
|
+
exports.toAnthropicToolChoice = toAnthropicToolChoice;
|
|
11
|
+
const tool_args_1 = require("../../utils/tool-args");
|
|
12
|
+
const errors_1 = require("../../utils/errors");
|
|
13
|
+
const logger_1 = require("../../utils/logger");
|
|
14
|
+
// ============ Anthropic → Canonical ============
|
|
15
|
+
/**
|
|
16
|
+
* One Anthropic message → one or more canonical messages (spec §3.4):
|
|
17
|
+
* a user message's tool_result blocks become one canonical tool message
|
|
18
|
+
* each (in order), followed by a user message for any remaining
|
|
19
|
+
* text/image blocks. Assistant tool_use blocks become toolCalls.
|
|
20
|
+
*/
|
|
21
|
+
function toCanonicalMessage(m) {
|
|
22
|
+
if (typeof m.content === "string" || !Array.isArray(m.content)) {
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
role: m.role,
|
|
26
|
+
content: typeof m.content === "string" ? m.content : "",
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
const blocks = m.content;
|
|
31
|
+
const toolResults = blocks.filter((b) => b.type === "tool_result");
|
|
32
|
+
const toolUses = blocks.filter((b) => b.type === "tool_use");
|
|
33
|
+
const rest = blocks.filter((b) => b.type !== "tool_result" && b.type !== "tool_use");
|
|
34
|
+
if (m.role === "user" && toolResults.length > 0) {
|
|
35
|
+
const out = toolResults.map((r) => ({
|
|
36
|
+
role: "tool",
|
|
37
|
+
toolCallId: r.tool_use_id,
|
|
38
|
+
content: toolResultContentToString(r.content),
|
|
39
|
+
...(r.is_error === true && { isError: true }),
|
|
40
|
+
}));
|
|
41
|
+
if (rest.length > 0) {
|
|
42
|
+
out.push({ role: "user", content: extractContent(rest) });
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
const msg = {
|
|
47
|
+
role: m.role,
|
|
48
|
+
content: extractContent(rest),
|
|
49
|
+
};
|
|
50
|
+
if (toolUses.length > 0) {
|
|
51
|
+
msg.toolCalls = toolUses.map(toolUseToCanonical);
|
|
52
|
+
}
|
|
53
|
+
return [msg];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* tool_result.content is a string or an array of blocks (spec §3.4). Text
|
|
57
|
+
* blocks join with "\n"; canonical tool results carry no images, so any
|
|
58
|
+
* other block type is a 400 naming it — never a silent drop.
|
|
59
|
+
*/
|
|
60
|
+
function toolResultContentToString(content) {
|
|
61
|
+
if (content === undefined) {
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
if (typeof content === "string") {
|
|
65
|
+
return content;
|
|
66
|
+
}
|
|
67
|
+
return content
|
|
68
|
+
.map((block) => {
|
|
69
|
+
if (block.type === "text") {
|
|
70
|
+
return block.text;
|
|
71
|
+
}
|
|
72
|
+
throw new errors_1.ValidationError(`Unsupported content block type "${block.type}" in tool_result`);
|
|
73
|
+
})
|
|
74
|
+
.join("\n");
|
|
75
|
+
}
|
|
76
|
+
function toolUseToCanonical(block) {
|
|
77
|
+
return {
|
|
78
|
+
id: block.id,
|
|
79
|
+
type: "function",
|
|
80
|
+
function: {
|
|
81
|
+
name: block.name,
|
|
82
|
+
arguments: JSON.stringify(block.input ?? {}),
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function toCanonicalTool(tool) {
|
|
87
|
+
if (!("name" in tool)) {
|
|
88
|
+
// Server-side toolsets have no canonical equivalent — 400 naming
|
|
89
|
+
// the type rather than silently dropping the declaration.
|
|
90
|
+
throw new errors_1.ValidationError(`Unsupported tool type "${tool.type}" in tools[]`);
|
|
91
|
+
}
|
|
92
|
+
const fn = { name: tool.name };
|
|
93
|
+
if ("description" in tool && typeof tool.description === "string") {
|
|
94
|
+
fn.description = tool.description;
|
|
95
|
+
}
|
|
96
|
+
if ("input_schema" in tool && tool.input_schema) {
|
|
97
|
+
fn.parameters = tool.input_schema;
|
|
98
|
+
}
|
|
99
|
+
return { type: "function", function: fn };
|
|
100
|
+
}
|
|
101
|
+
// auto→auto, any→required, tool{name}→function choice, none→none (§3.4).
|
|
102
|
+
function toCanonicalToolChoice(choice) {
|
|
103
|
+
if ("disable_parallel_tool_use" in choice &&
|
|
104
|
+
choice.disable_parallel_tool_use !== undefined) {
|
|
105
|
+
logger_1.logger.debug('Dropping tool_choice field "disable_parallel_tool_use" (no canonical equivalent)');
|
|
106
|
+
}
|
|
107
|
+
switch (choice.type) {
|
|
108
|
+
case "auto":
|
|
109
|
+
return "auto";
|
|
110
|
+
case "any":
|
|
111
|
+
return "required";
|
|
112
|
+
case "none":
|
|
113
|
+
return "none";
|
|
114
|
+
case "tool":
|
|
115
|
+
return { type: "function", function: { name: choice.name } };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* §3.5: `thinking`/`redacted_thinking` blocks are dropped safely (debug
|
|
120
|
+
* log names the type only); any other unrecognised block type is a 400
|
|
121
|
+
* naming it — Claude Code blocks we can't honour must fail loudly, not
|
|
122
|
+
* silently turn into empty text.
|
|
123
|
+
*/
|
|
124
|
+
function extractContent(content) {
|
|
125
|
+
if (typeof content === "string") {
|
|
126
|
+
return content;
|
|
127
|
+
}
|
|
128
|
+
const parts = [];
|
|
129
|
+
for (const block of content) {
|
|
130
|
+
if (block.type === "text") {
|
|
131
|
+
parts.push({ type: "text", text: block.text });
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (block.type === "image" && "source" in block) {
|
|
135
|
+
const source = block.source;
|
|
136
|
+
if (source.type === "base64" && source.data) {
|
|
137
|
+
parts.push({
|
|
138
|
+
type: "image",
|
|
139
|
+
image: {
|
|
140
|
+
data: source.data,
|
|
141
|
+
mimeType: (source.media_type ||
|
|
142
|
+
"image/png"),
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
// image without a usable base64 source keeps the old placeholder
|
|
148
|
+
parts.push({ type: "text", text: "" });
|
|
149
|
+
}
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (block.type === "thinking" || block.type === "redacted_thinking") {
|
|
153
|
+
logger_1.logger.debug(`Dropping content block "${block.type}" (no canonical equivalent)`);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
throw new errors_1.ValidationError(`Unsupported content block type "${block.type}"`);
|
|
157
|
+
}
|
|
158
|
+
return parts.length > 0 ? parts : "";
|
|
159
|
+
}
|
|
160
|
+
// ============ Canonical → Anthropic ============
|
|
161
|
+
function toMessageParam(m) {
|
|
162
|
+
const content = convertToAnthropicContent(m.content);
|
|
163
|
+
if (m.role !== "assistant" || !m.toolCalls || m.toolCalls.length === 0) {
|
|
164
|
+
return { role: m.role === "assistant" ? "assistant" : "user", content };
|
|
165
|
+
}
|
|
166
|
+
// Assistant message: text (and other content blocks) first, then the
|
|
167
|
+
// tool_use blocks (spec §3.4).
|
|
168
|
+
const blocks = typeof content === "string"
|
|
169
|
+
? content
|
|
170
|
+
? [{ type: "text", text: content }]
|
|
171
|
+
: []
|
|
172
|
+
: content;
|
|
173
|
+
return {
|
|
174
|
+
role: "assistant",
|
|
175
|
+
content: [...blocks, ...m.toolCalls.map(toToolUseBlock)],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function toToolResultBlock(m) {
|
|
179
|
+
return {
|
|
180
|
+
type: "tool_result",
|
|
181
|
+
tool_use_id: m.toolCallId ?? "",
|
|
182
|
+
content: convertToAnthropicContent(m.content),
|
|
183
|
+
// is_error is sent only when true; Anthropic defaults it to false.
|
|
184
|
+
...(m.isError === true && { is_error: true }),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function toToolUseBlock(tc) {
|
|
188
|
+
const parsed = (0, tool_args_1.parseToolArguments)(tc.function.arguments);
|
|
189
|
+
if (!parsed.valid) {
|
|
190
|
+
logger_1.logger.warn("Tool call arguments are not valid JSON; sending _invalid_arguments wrapper");
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
type: "tool_use",
|
|
194
|
+
id: tc.id,
|
|
195
|
+
name: tc.function.name,
|
|
196
|
+
input: parsed.args,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function toAnthropicTool(tool) {
|
|
200
|
+
return {
|
|
201
|
+
name: tool.function.name,
|
|
202
|
+
...(tool.function.description && {
|
|
203
|
+
description: tool.function.description,
|
|
204
|
+
}),
|
|
205
|
+
input_schema: {
|
|
206
|
+
type: "object",
|
|
207
|
+
...(tool.function.parameters ?? {}),
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
// auto→auto, required→any, none→none, function choice→tool{name} (§3.4).
|
|
212
|
+
function toAnthropicToolChoice(choice) {
|
|
213
|
+
if (typeof choice === "object") {
|
|
214
|
+
return { type: "tool", name: choice.function.name };
|
|
215
|
+
}
|
|
216
|
+
switch (choice) {
|
|
217
|
+
case "auto":
|
|
218
|
+
return { type: "auto" };
|
|
219
|
+
case "required":
|
|
220
|
+
return { type: "any" };
|
|
221
|
+
case "none":
|
|
222
|
+
return { type: "none" };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function convertToAnthropicContent(content) {
|
|
226
|
+
if (typeof content === "string") {
|
|
227
|
+
return content;
|
|
228
|
+
}
|
|
229
|
+
const blocks = content.map((part) => {
|
|
230
|
+
if (part.type === "text") {
|
|
231
|
+
return { type: "text", text: part.text || "" };
|
|
232
|
+
}
|
|
233
|
+
if (part.type === "image" && part.image) {
|
|
234
|
+
return {
|
|
235
|
+
type: "image",
|
|
236
|
+
source: {
|
|
237
|
+
type: "base64",
|
|
238
|
+
media_type: validateMediaType(part.image.mimeType),
|
|
239
|
+
data: part.image.data,
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return { type: "text", text: "" };
|
|
244
|
+
});
|
|
245
|
+
// If only one text block, return as string
|
|
246
|
+
if (blocks.length === 1 && blocks[0].type === "text") {
|
|
247
|
+
return blocks[0].text;
|
|
248
|
+
}
|
|
249
|
+
return blocks;
|
|
250
|
+
}
|
|
251
|
+
function validateMediaType(type) {
|
|
252
|
+
const validTypes = [
|
|
253
|
+
"image/jpeg",
|
|
254
|
+
"image/png",
|
|
255
|
+
"image/gif",
|
|
256
|
+
"image/webp",
|
|
257
|
+
];
|
|
258
|
+
return validTypes.includes(type)
|
|
259
|
+
? type
|
|
260
|
+
: "image/png";
|
|
261
|
+
}
|