@alma-harness/providers 0.1.0 → 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/README.md +60 -1
- package/dist/index.d.ts +86 -12
- package/dist/index.js +603 -80
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,10 +1,57 @@
|
|
|
1
1
|
// src/anthropic/client.ts
|
|
2
2
|
import Anthropic from "@anthropic-ai/sdk";
|
|
3
3
|
|
|
4
|
+
// src/errors.ts
|
|
5
|
+
import { ProviderError } from "@alma-harness/core";
|
|
6
|
+
var CONTEXT_WINDOW = /context_length_exceeded|prompt is too long|context window|maximum context length|too many tokens|exceeds the context|input length/i;
|
|
7
|
+
var OVERLOADED = /overloaded|too many requests|capacity|server_busy/i;
|
|
8
|
+
function hintsOf(e) {
|
|
9
|
+
const nested = typeof e.error === "object" && e.error !== null ? e.error : {};
|
|
10
|
+
const inner = typeof nested.error === "object" && nested.error !== null ? nested.error : {};
|
|
11
|
+
return [e.code, e.type, nested.code, nested.type, inner.code, inner.type, inner.message].filter((h) => typeof h === "string").join(" ").toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
function toProviderError(provider, err) {
|
|
14
|
+
if (err instanceof ProviderError) return err;
|
|
15
|
+
const e = typeof err === "object" && err !== null ? err : {};
|
|
16
|
+
const own = typeof e.name === "string" && e.name !== "Error" ? e.name : "";
|
|
17
|
+
const name = own !== "" ? own : err?.constructor?.name ?? "";
|
|
18
|
+
if (name === "APIUserAbortError" || name === "AbortError") return err;
|
|
19
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
20
|
+
const status = typeof e.status === "number" ? e.status : void 0;
|
|
21
|
+
const kind = classifyFailure(name, status, `${hintsOf(e)} ${message.toLowerCase()}`);
|
|
22
|
+
return new ProviderError(provider, kind, message, { ...status !== void 0 ? { status } : {}, cause: err });
|
|
23
|
+
}
|
|
24
|
+
function classifyFailure(name, status, hints) {
|
|
25
|
+
if (status === 429 || /rate_limit/.test(hints)) return hints.includes("insufficient_quota") ? "rejected" : "rate_limited";
|
|
26
|
+
if (status === 529 || status === 503 || (status === void 0 || status >= 500) && OVERLOADED.test(hints)) return "overloaded";
|
|
27
|
+
if (status !== void 0 && status >= 500) return "unavailable";
|
|
28
|
+
if ((status === void 0 || status === 400) && CONTEXT_WINDOW.test(hints)) return "context_window";
|
|
29
|
+
if (status !== void 0 && status >= 400) return "rejected";
|
|
30
|
+
if (/connection|timeout|timed out|fetch|network|socket|econn|enotfound|server_error|internal|unavailable/i.test(`${name} ${hints}`)) {
|
|
31
|
+
return "unavailable";
|
|
32
|
+
}
|
|
33
|
+
return "provider_drift";
|
|
34
|
+
}
|
|
35
|
+
|
|
4
36
|
// src/anthropic/translate.ts
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
37
|
+
import {
|
|
38
|
+
ProviderError as ProviderError2
|
|
39
|
+
} from "@alma-harness/core";
|
|
40
|
+
|
|
41
|
+
// src/arguments.ts
|
|
42
|
+
function parseToolArguments(json) {
|
|
43
|
+
if (json === "") return { input: {} };
|
|
44
|
+
try {
|
|
45
|
+
return { input: JSON.parse(json) };
|
|
46
|
+
} catch {
|
|
47
|
+
return { input: {}, malformed: json };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/anthropic/translate.ts
|
|
52
|
+
var AnthropicTranslationError = class extends ProviderError2 {
|
|
53
|
+
constructor(message, kind = "rejected") {
|
|
54
|
+
super("anthropic", kind, message);
|
|
8
55
|
this.name = "AnthropicTranslationError";
|
|
9
56
|
}
|
|
10
57
|
};
|
|
@@ -14,17 +61,50 @@ function toAnthropicParams(req) {
|
|
|
14
61
|
`AnthropicModelClient received a request for provider ${JSON.stringify(req.model.provider)}`
|
|
15
62
|
);
|
|
16
63
|
}
|
|
64
|
+
const reasoning = req.reasoning;
|
|
65
|
+
const replay = reasoning !== void 0 && reasoning.effort !== "none";
|
|
17
66
|
const params = {
|
|
18
67
|
model: req.model.id,
|
|
19
68
|
max_tokens: req.maxTokens,
|
|
20
69
|
stream: true,
|
|
21
70
|
system: toSystem(req.system),
|
|
22
|
-
messages: req.messages.
|
|
71
|
+
messages: req.messages.flatMap((m) => toMessageParams(m, replay))
|
|
23
72
|
};
|
|
24
|
-
if (
|
|
73
|
+
if (reasoning !== void 0) {
|
|
74
|
+
if (reasoning.effort === "none") {
|
|
75
|
+
params.thinking = { type: "disabled" };
|
|
76
|
+
} else {
|
|
77
|
+
params.thinking = { type: "adaptive" };
|
|
78
|
+
params.output_config = { effort: toAnthropicEffort(reasoning.effort) };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
switch (req.serviceTier) {
|
|
82
|
+
case void 0:
|
|
83
|
+
case "standard":
|
|
84
|
+
break;
|
|
85
|
+
case "priority":
|
|
86
|
+
params.service_tier = "auto";
|
|
87
|
+
break;
|
|
88
|
+
default:
|
|
89
|
+
throw new AnthropicTranslationError(
|
|
90
|
+
`the Anthropic Messages API cannot serve the ${req.serviceTier} tier on a streaming request`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const tools = [...req.tools.map(toTool), ...(req.providerTools ?? []).map(toServerTool)];
|
|
94
|
+
if (tools.length > 0) params.tools = tools;
|
|
25
95
|
markConversationTail(params.messages);
|
|
26
96
|
return params;
|
|
27
97
|
}
|
|
98
|
+
function toServerTool(spec) {
|
|
99
|
+
const tool = { type: "web_search_20250305", name: "web_search" };
|
|
100
|
+
if (spec.maxUses !== void 0) tool.max_uses = spec.maxUses;
|
|
101
|
+
if (spec.allowedDomains !== void 0) tool.allowed_domains = [...spec.allowedDomains];
|
|
102
|
+
if (spec.blockedDomains !== void 0) tool.blocked_domains = [...spec.blockedDomains];
|
|
103
|
+
return tool;
|
|
104
|
+
}
|
|
105
|
+
function toAnthropicEffort(effort) {
|
|
106
|
+
return effort === "minimal" ? "low" : effort;
|
|
107
|
+
}
|
|
28
108
|
function markConversationTail(messages) {
|
|
29
109
|
const last = messages.at(-1);
|
|
30
110
|
if (!last || typeof last.content === "string") return;
|
|
@@ -42,14 +122,16 @@ function toSystem(blocks) {
|
|
|
42
122
|
return param;
|
|
43
123
|
});
|
|
44
124
|
}
|
|
45
|
-
function
|
|
125
|
+
function toMessageParams(msg, replayReasoning) {
|
|
46
126
|
switch (msg.role) {
|
|
47
127
|
case "user":
|
|
48
|
-
return { role: "user", content: msg.blocks.map(toUserBlock) };
|
|
49
|
-
case "assistant":
|
|
50
|
-
|
|
128
|
+
return [{ role: "user", content: msg.blocks.map(toUserBlock) }];
|
|
129
|
+
case "assistant": {
|
|
130
|
+
const content = msg.blocks.flatMap((b) => toAssistantBlocks(b, replayReasoning));
|
|
131
|
+
return content.length === 0 ? [] : [{ role: "assistant", content }];
|
|
132
|
+
}
|
|
51
133
|
case "tool":
|
|
52
|
-
return { role: "user", content: msg.blocks.map(toToolResultBlock) };
|
|
134
|
+
return [{ role: "user", content: msg.blocks.map(toToolResultBlock) }];
|
|
53
135
|
}
|
|
54
136
|
}
|
|
55
137
|
function toUserBlock(block) {
|
|
@@ -69,18 +151,41 @@ function toUserBlock(block) {
|
|
|
69
151
|
throw new AnthropicTranslationError(`block type ${JSON.stringify(block.type)} is not valid in a user message`);
|
|
70
152
|
}
|
|
71
153
|
}
|
|
72
|
-
function
|
|
154
|
+
function toAssistantBlocks(block, replayReasoning) {
|
|
73
155
|
switch (block.type) {
|
|
74
156
|
case "text":
|
|
75
|
-
return { type: "text", text: block.text };
|
|
157
|
+
return [{ type: "text", text: block.text }];
|
|
76
158
|
case "tool_call":
|
|
77
|
-
return { type: "tool_use", id: block.id, name: block.name, input: block.input };
|
|
159
|
+
return [{ type: "tool_use", id: block.id, name: block.name, input: block.input }];
|
|
160
|
+
case "reasoning":
|
|
161
|
+
return block.provider === "anthropic" && replayReasoning ? [toThinkingParam(block)] : [];
|
|
162
|
+
case "provider_tool_call":
|
|
163
|
+
return block.provider === "anthropic" ? [{ type: "server_tool_use", id: block.id, name: block.name, input: block.input }] : [];
|
|
164
|
+
case "provider_tool_result":
|
|
165
|
+
return block.provider === "anthropic" ? [toWebSearchResultParam(block)] : [];
|
|
78
166
|
default:
|
|
79
167
|
throw new AnthropicTranslationError(
|
|
80
168
|
`block type ${JSON.stringify(block.type)} is not valid in an assistant message`
|
|
81
169
|
);
|
|
82
170
|
}
|
|
83
171
|
}
|
|
172
|
+
function toWebSearchResultParam(block) {
|
|
173
|
+
const opaque = block.opaque;
|
|
174
|
+
if (opaque?.content === void 0) {
|
|
175
|
+
throw new AnthropicTranslationError("an Anthropic web search result carries no replayable content");
|
|
176
|
+
}
|
|
177
|
+
return { type: "web_search_tool_result", tool_use_id: block.callId, content: opaque.content };
|
|
178
|
+
}
|
|
179
|
+
function toThinkingParam(block) {
|
|
180
|
+
const opaque = block.opaque;
|
|
181
|
+
if (typeof opaque?.redacted === "string") {
|
|
182
|
+
return { type: "redacted_thinking", data: opaque.redacted };
|
|
183
|
+
}
|
|
184
|
+
if (typeof opaque?.signature === "string") {
|
|
185
|
+
return { type: "thinking", thinking: block.text ?? "", signature: opaque.signature };
|
|
186
|
+
}
|
|
187
|
+
throw new AnthropicTranslationError("an Anthropic reasoning block carries neither a signature nor redacted data");
|
|
188
|
+
}
|
|
84
189
|
function toToolResultBlock(block) {
|
|
85
190
|
if (block.type !== "tool_result") {
|
|
86
191
|
throw new AnthropicTranslationError(
|
|
@@ -109,6 +214,9 @@ async function* translateStream(events) {
|
|
|
109
214
|
const usage = { inputTokens: 0, outputTokens: 0 };
|
|
110
215
|
let stopReason = null;
|
|
111
216
|
const pendingTools = /* @__PURE__ */ new Map();
|
|
217
|
+
const pendingThinking = /* @__PURE__ */ new Map();
|
|
218
|
+
const pendingRedacted = /* @__PURE__ */ new Map();
|
|
219
|
+
const pendingServer = /* @__PURE__ */ new Map();
|
|
112
220
|
for await (const event of events) {
|
|
113
221
|
switch (event.type) {
|
|
114
222
|
case "message_start": {
|
|
@@ -118,6 +226,10 @@ async function* translateStream(events) {
|
|
|
118
226
|
if (u.cache_creation_input_tokens != null) {
|
|
119
227
|
usage.cacheWriteInputTokens = u.cache_creation_input_tokens;
|
|
120
228
|
}
|
|
229
|
+
if (u.server_tool_use?.web_search_requests) usage.webSearchRequests = u.server_tool_use.web_search_requests;
|
|
230
|
+
if (u.service_tier === "standard" || u.service_tier === "priority" || u.service_tier === "batch") {
|
|
231
|
+
usage.serviceTier = u.service_tier;
|
|
232
|
+
}
|
|
121
233
|
break;
|
|
122
234
|
}
|
|
123
235
|
case "content_block_start":
|
|
@@ -127,25 +239,58 @@ async function* translateStream(events) {
|
|
|
127
239
|
name: event.content_block.name,
|
|
128
240
|
json: ""
|
|
129
241
|
});
|
|
242
|
+
} else if (event.content_block.type === "thinking") {
|
|
243
|
+
pendingThinking.set(event.index, { text: event.content_block.thinking, signature: event.content_block.signature });
|
|
244
|
+
} else if (event.content_block.type === "redacted_thinking") {
|
|
245
|
+
pendingRedacted.set(event.index, event.content_block.data);
|
|
246
|
+
} else if (event.content_block.type === "server_tool_use") {
|
|
247
|
+
if (event.content_block.name !== "web_search") {
|
|
248
|
+
throw new AnthropicTranslationError(`Unmapped Anthropic server tool ${JSON.stringify(event.content_block.name)} \u2014 provider drift?`, "provider_drift");
|
|
249
|
+
}
|
|
250
|
+
pendingServer.set(event.index, { id: event.content_block.id, name: "web_search", json: "" });
|
|
251
|
+
} else if (event.content_block.type === "web_search_tool_result") {
|
|
252
|
+
yield { type: "provider_tool_result", block: toProviderToolResult(event.content_block) };
|
|
130
253
|
}
|
|
131
254
|
break;
|
|
132
255
|
case "content_block_delta":
|
|
133
256
|
if (event.delta.type === "text_delta") {
|
|
134
257
|
yield { type: "text_delta", text: event.delta.text };
|
|
135
258
|
} else if (event.delta.type === "input_json_delta") {
|
|
136
|
-
const pending = pendingTools.get(event.index);
|
|
259
|
+
const pending = pendingTools.get(event.index) ?? pendingServer.get(event.index);
|
|
137
260
|
if (pending) pending.json += event.delta.partial_json;
|
|
261
|
+
} else if (event.delta.type === "thinking_delta") {
|
|
262
|
+
const pending = pendingThinking.get(event.index);
|
|
263
|
+
if (pending) pending.text += event.delta.thinking;
|
|
264
|
+
} else if (event.delta.type === "signature_delta") {
|
|
265
|
+
const pending = pendingThinking.get(event.index);
|
|
266
|
+
if (pending) pending.signature = event.delta.signature;
|
|
138
267
|
}
|
|
139
268
|
break;
|
|
140
269
|
case "content_block_stop": {
|
|
141
270
|
const pending = pendingTools.get(event.index);
|
|
142
271
|
if (pending) {
|
|
143
272
|
pendingTools.delete(event.index);
|
|
273
|
+
yield { type: "tool_call", id: pending.id, name: pending.name, ...parseToolArguments(pending.json) };
|
|
274
|
+
}
|
|
275
|
+
const thinking = pendingThinking.get(event.index);
|
|
276
|
+
if (thinking) {
|
|
277
|
+
pendingThinking.delete(event.index);
|
|
278
|
+
yield {
|
|
279
|
+
type: "reasoning",
|
|
280
|
+
block: { type: "reasoning", provider: "anthropic", text: thinking.text, opaque: { signature: thinking.signature } }
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const redacted = pendingRedacted.get(event.index);
|
|
284
|
+
if (redacted !== void 0) {
|
|
285
|
+
pendingRedacted.delete(event.index);
|
|
286
|
+
yield { type: "reasoning", block: { type: "reasoning", provider: "anthropic", opaque: { redacted } } };
|
|
287
|
+
}
|
|
288
|
+
const server = pendingServer.get(event.index);
|
|
289
|
+
if (server) {
|
|
290
|
+
pendingServer.delete(event.index);
|
|
144
291
|
yield {
|
|
145
|
-
type: "
|
|
146
|
-
id:
|
|
147
|
-
name: pending.name,
|
|
148
|
-
input: pending.json === "" ? {} : JSON.parse(pending.json)
|
|
292
|
+
type: "provider_tool_call",
|
|
293
|
+
block: { type: "provider_tool_call", id: server.id, name: server.name, provider: "anthropic", input: server.json === "" ? {} : JSON.parse(server.json) }
|
|
149
294
|
};
|
|
150
295
|
}
|
|
151
296
|
break;
|
|
@@ -153,6 +298,7 @@ async function* translateStream(events) {
|
|
|
153
298
|
case "message_delta":
|
|
154
299
|
if (event.delta.stop_reason != null) stopReason = event.delta.stop_reason;
|
|
155
300
|
usage.outputTokens = event.usage.output_tokens;
|
|
301
|
+
if (event.usage.server_tool_use?.web_search_requests) usage.webSearchRequests = event.usage.server_tool_use.web_search_requests;
|
|
156
302
|
break;
|
|
157
303
|
case "message_stop":
|
|
158
304
|
yield { type: "usage", usage: { ...usage } };
|
|
@@ -161,6 +307,26 @@ async function* translateStream(events) {
|
|
|
161
307
|
}
|
|
162
308
|
}
|
|
163
309
|
}
|
|
310
|
+
function toProviderToolResult(block) {
|
|
311
|
+
const result = {
|
|
312
|
+
type: "provider_tool_result",
|
|
313
|
+
callId: block.tool_use_id,
|
|
314
|
+
name: "web_search",
|
|
315
|
+
provider: "anthropic",
|
|
316
|
+
results: [],
|
|
317
|
+
opaque: { content: block.content }
|
|
318
|
+
};
|
|
319
|
+
if (Array.isArray(block.content)) {
|
|
320
|
+
result.results = block.content.map((r) => ({
|
|
321
|
+
url: r.url,
|
|
322
|
+
...r.title !== void 0 ? { title: r.title } : {},
|
|
323
|
+
...r.page_age != null ? { pageAge: r.page_age } : {}
|
|
324
|
+
}));
|
|
325
|
+
} else {
|
|
326
|
+
result.error = block.content.error_code;
|
|
327
|
+
}
|
|
328
|
+
return result;
|
|
329
|
+
}
|
|
164
330
|
function mapStopReason(reason) {
|
|
165
331
|
switch (reason) {
|
|
166
332
|
case "end_turn":
|
|
@@ -174,9 +340,12 @@ function mapStopReason(reason) {
|
|
|
174
340
|
return "refusal";
|
|
175
341
|
case "model_context_window_exceeded":
|
|
176
342
|
return "context_window_exceeded";
|
|
343
|
+
case "pause_turn":
|
|
344
|
+
return "pause";
|
|
177
345
|
default:
|
|
178
346
|
throw new AnthropicTranslationError(
|
|
179
|
-
`Unmapped Anthropic stop_reason ${JSON.stringify(reason)} \u2014 provider drift
|
|
347
|
+
`Unmapped Anthropic stop_reason ${JSON.stringify(reason)} \u2014 provider drift?`,
|
|
348
|
+
"provider_drift"
|
|
180
349
|
);
|
|
181
350
|
}
|
|
182
351
|
}
|
|
@@ -195,11 +364,101 @@ var AnthropicModelClient = class {
|
|
|
195
364
|
return translateStream(this.#rawEvents(params, opts?.signal));
|
|
196
365
|
}
|
|
197
366
|
async *#rawEvents(params, signal) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
367
|
+
try {
|
|
368
|
+
const stream = await this.#client.messages.create(
|
|
369
|
+
params,
|
|
370
|
+
signal !== void 0 ? { signal } : void 0
|
|
371
|
+
);
|
|
372
|
+
for await (const event of stream) yield event;
|
|
373
|
+
} catch (err) {
|
|
374
|
+
throw toProviderError("anthropic", err);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
// src/anthropic/jobs.ts
|
|
380
|
+
import Anthropic2 from "@anthropic-ai/sdk";
|
|
381
|
+
function translateMessage(message) {
|
|
382
|
+
const blocks = [];
|
|
383
|
+
for (const block of message.content) {
|
|
384
|
+
switch (block.type) {
|
|
385
|
+
case "text":
|
|
386
|
+
blocks.push({ type: "text", text: block.text });
|
|
387
|
+
break;
|
|
388
|
+
case "tool_use":
|
|
389
|
+
blocks.push({ type: "tool_call", id: block.id, name: block.name, input: block.input });
|
|
390
|
+
break;
|
|
391
|
+
case "thinking":
|
|
392
|
+
blocks.push({ type: "reasoning", provider: "anthropic", text: block.thinking, opaque: { signature: block.signature } });
|
|
393
|
+
break;
|
|
394
|
+
case "redacted_thinking":
|
|
395
|
+
blocks.push({ type: "reasoning", provider: "anthropic", opaque: { redacted: block.data } });
|
|
396
|
+
break;
|
|
397
|
+
default:
|
|
398
|
+
throw new AnthropicTranslationError(`Unmapped Anthropic content block ${JSON.stringify(block.type)} \u2014 provider drift?`, "provider_drift");
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const u = message.usage;
|
|
402
|
+
const usage = { inputTokens: u.input_tokens, outputTokens: u.output_tokens };
|
|
403
|
+
if (u.cache_read_input_tokens != null) usage.cacheReadInputTokens = u.cache_read_input_tokens;
|
|
404
|
+
if (u.cache_creation_input_tokens != null) usage.cacheWriteInputTokens = u.cache_creation_input_tokens;
|
|
405
|
+
if (u.service_tier === "standard" || u.service_tier === "priority" || u.service_tier === "batch") {
|
|
406
|
+
usage.serviceTier = u.service_tier;
|
|
407
|
+
}
|
|
408
|
+
return { blocks, usage, stop: mapStopReason(message.stop_reason) };
|
|
409
|
+
}
|
|
410
|
+
var AnthropicJobClient = class {
|
|
411
|
+
#client;
|
|
412
|
+
constructor(opts = {}) {
|
|
413
|
+
const init = {};
|
|
414
|
+
if (opts.apiKey !== void 0) init.apiKey = opts.apiKey;
|
|
415
|
+
if (opts.baseURL !== void 0) init.baseURL = opts.baseURL;
|
|
416
|
+
this.#client = new Anthropic2(init);
|
|
417
|
+
}
|
|
418
|
+
async submit(items) {
|
|
419
|
+
const first = items[0];
|
|
420
|
+
if (!first) throw new AnthropicTranslationError("a batch needs at least one item");
|
|
421
|
+
const requests = items.map((item) => {
|
|
422
|
+
const { serviceTier: _tier, ...req } = item.request;
|
|
423
|
+
void _tier;
|
|
424
|
+
const { stream: _stream, ...params } = toAnthropicParams(req);
|
|
425
|
+
void _stream;
|
|
426
|
+
return { custom_id: item.id, params };
|
|
427
|
+
});
|
|
428
|
+
const batch = await this.#client.messages.batches.create({ requests });
|
|
429
|
+
return { provider: "anthropic", id: batch.id, model: first.request.model };
|
|
430
|
+
}
|
|
431
|
+
async status(handle) {
|
|
432
|
+
const batch = await this.#client.messages.batches.retrieve(handle.id);
|
|
433
|
+
const c = batch.request_counts;
|
|
434
|
+
const total = c.processing + c.succeeded + c.errored + c.canceled + c.expired;
|
|
435
|
+
const status = batch.processing_status === "canceling" ? "cancelled" : batch.processing_status === "in_progress" ? "running" : c.canceled === total && total > 0 ? "cancelled" : c.expired === total && total > 0 ? "expired" : "done";
|
|
436
|
+
return { status, counts: { total, done: c.succeeded, failed: c.errored + c.canceled + c.expired } };
|
|
437
|
+
}
|
|
438
|
+
async *results(handle) {
|
|
439
|
+
const decoder = await this.#client.messages.batches.results(handle.id);
|
|
440
|
+
for await (const entry of decoder) {
|
|
441
|
+
const { custom_id: id, result } = entry;
|
|
442
|
+
switch (result.type) {
|
|
443
|
+
case "succeeded":
|
|
444
|
+
yield { id, outcome: "succeeded", output: translateMessage(result.message) };
|
|
445
|
+
break;
|
|
446
|
+
case "errored": {
|
|
447
|
+
const error = result.error?.error?.message;
|
|
448
|
+
yield { id, outcome: "errored", ...error !== void 0 ? { error } : {} };
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
case "canceled":
|
|
452
|
+
yield { id, outcome: "cancelled" };
|
|
453
|
+
break;
|
|
454
|
+
case "expired":
|
|
455
|
+
yield { id, outcome: "expired" };
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
async cancel(handle) {
|
|
461
|
+
await this.#client.messages.batches.cancel(handle.id);
|
|
203
462
|
}
|
|
204
463
|
};
|
|
205
464
|
|
|
@@ -207,9 +466,12 @@ var AnthropicModelClient = class {
|
|
|
207
466
|
import OpenAI from "openai";
|
|
208
467
|
|
|
209
468
|
// src/openai/translate.ts
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
469
|
+
import {
|
|
470
|
+
ProviderError as ProviderError3
|
|
471
|
+
} from "@alma-harness/core";
|
|
472
|
+
var OpenAITranslationError = class extends ProviderError3 {
|
|
473
|
+
constructor(message, kind = "rejected") {
|
|
474
|
+
super("openai", kind, message);
|
|
213
475
|
this.name = "OpenAITranslationError";
|
|
214
476
|
}
|
|
215
477
|
};
|
|
@@ -219,6 +481,8 @@ function toOpenAIParams(req) {
|
|
|
219
481
|
`OpenAIModelClient received a request for provider ${JSON.stringify(req.model.provider)}`
|
|
220
482
|
);
|
|
221
483
|
}
|
|
484
|
+
const reasoning = req.reasoning;
|
|
485
|
+
const replay = reasoning !== void 0 && reasoning.effort !== "none";
|
|
222
486
|
const params = {
|
|
223
487
|
model: req.model.id,
|
|
224
488
|
max_output_tokens: req.maxTokens,
|
|
@@ -226,22 +490,49 @@ function toOpenAIParams(req) {
|
|
|
226
490
|
// Privacy-first (§3, §10): the Responses API stores responses server-side
|
|
227
491
|
// by default; the harness never leaves conversation state at the provider.
|
|
228
492
|
store: false,
|
|
229
|
-
input: req.messages.flatMap(toInputItems)
|
|
493
|
+
input: req.messages.flatMap((m) => toInputItems(m, replay))
|
|
230
494
|
};
|
|
495
|
+
if (reasoning !== void 0) {
|
|
496
|
+
params.reasoning = replay ? { effort: reasoning.effort, summary: "auto" } : { effort: "none" };
|
|
497
|
+
if (replay) params.include = ["reasoning.encrypted_content"];
|
|
498
|
+
}
|
|
499
|
+
switch (req.serviceTier) {
|
|
500
|
+
case void 0:
|
|
501
|
+
case "standard":
|
|
502
|
+
break;
|
|
503
|
+
case "flex":
|
|
504
|
+
case "priority":
|
|
505
|
+
params.service_tier = req.serviceTier;
|
|
506
|
+
break;
|
|
507
|
+
default:
|
|
508
|
+
throw new OpenAITranslationError(
|
|
509
|
+
`the OpenAI Responses API cannot serve the ${req.serviceTier} tier on a streaming request`
|
|
510
|
+
);
|
|
511
|
+
}
|
|
231
512
|
const instructions = toInstructions(req.system);
|
|
232
513
|
if (instructions !== "") params.instructions = instructions;
|
|
233
|
-
|
|
514
|
+
const tools = [...req.tools.map(toTool2), ...(req.providerTools ?? []).map(toWebSearchTool)];
|
|
515
|
+
if (tools.length > 0) params.tools = tools;
|
|
516
|
+
if ((req.providerTools ?? []).length > 0) params.include = [...params.include ?? [], "web_search_call.action.sources"];
|
|
234
517
|
return params;
|
|
235
518
|
}
|
|
519
|
+
function toWebSearchTool(spec) {
|
|
520
|
+
if (spec.blockedDomains !== void 0) {
|
|
521
|
+
throw new OpenAITranslationError("the OpenAI web search has no blocked-domains form \u2014 refusing rather than searching them");
|
|
522
|
+
}
|
|
523
|
+
const tool = { type: "web_search" };
|
|
524
|
+
if (spec.allowedDomains !== void 0) tool.filters = { allowed_domains: [...spec.allowedDomains] };
|
|
525
|
+
return tool;
|
|
526
|
+
}
|
|
236
527
|
function toInstructions(blocks) {
|
|
237
528
|
return blocks.map((b) => b.text).join("\n\n");
|
|
238
529
|
}
|
|
239
|
-
function toInputItems(msg) {
|
|
530
|
+
function toInputItems(msg, replayReasoning) {
|
|
240
531
|
switch (msg.role) {
|
|
241
532
|
case "user":
|
|
242
533
|
return [{ role: "user", content: msg.blocks.map(toUserContentPart) }];
|
|
243
534
|
case "assistant":
|
|
244
|
-
return msg.blocks.
|
|
535
|
+
return msg.blocks.flatMap((b) => toAssistantItems(b, replayReasoning));
|
|
245
536
|
case "tool":
|
|
246
537
|
return msg.blocks.map(toFunctionCallOutput);
|
|
247
538
|
}
|
|
@@ -263,23 +554,50 @@ function toUserContentPart(block) {
|
|
|
263
554
|
);
|
|
264
555
|
}
|
|
265
556
|
}
|
|
266
|
-
function
|
|
557
|
+
function toAssistantItems(block, replayReasoning) {
|
|
267
558
|
switch (block.type) {
|
|
268
559
|
case "text":
|
|
269
|
-
return { role: "assistant", content: block.text };
|
|
560
|
+
return [{ role: "assistant", content: block.text }];
|
|
270
561
|
case "tool_call":
|
|
271
|
-
return
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
562
|
+
return [
|
|
563
|
+
{
|
|
564
|
+
type: "function_call",
|
|
565
|
+
call_id: block.id,
|
|
566
|
+
name: block.name,
|
|
567
|
+
arguments: JSON.stringify(block.input)
|
|
568
|
+
}
|
|
569
|
+
];
|
|
570
|
+
case "reasoning":
|
|
571
|
+
return block.provider === "openai" && replayReasoning ? [toReasoningItem(block)] : [];
|
|
572
|
+
case "provider_tool_call":
|
|
573
|
+
return [];
|
|
574
|
+
case "provider_tool_result":
|
|
575
|
+
return block.provider === "openai" ? [toWebSearchItem(block)] : [];
|
|
277
576
|
default:
|
|
278
577
|
throw new OpenAITranslationError(
|
|
279
578
|
`block type ${JSON.stringify(block.type)} is not valid in an assistant message`
|
|
280
579
|
);
|
|
281
580
|
}
|
|
282
581
|
}
|
|
582
|
+
function toWebSearchItem(block) {
|
|
583
|
+
const opaque = block.opaque;
|
|
584
|
+
if (opaque?.type !== "web_search_call") {
|
|
585
|
+
throw new OpenAITranslationError("an OpenAI web search result carries no replayable item");
|
|
586
|
+
}
|
|
587
|
+
return opaque;
|
|
588
|
+
}
|
|
589
|
+
function toReasoningItem(block) {
|
|
590
|
+
const opaque = block.opaque;
|
|
591
|
+
if (typeof opaque?.id !== "string" || typeof opaque.encrypted_content !== "string") {
|
|
592
|
+
throw new OpenAITranslationError("an OpenAI reasoning block carries no id or encrypted content");
|
|
593
|
+
}
|
|
594
|
+
return {
|
|
595
|
+
type: "reasoning",
|
|
596
|
+
id: opaque.id,
|
|
597
|
+
summary: Array.isArray(opaque.summary) ? opaque.summary : [],
|
|
598
|
+
encrypted_content: opaque.encrypted_content
|
|
599
|
+
};
|
|
600
|
+
}
|
|
283
601
|
function toFunctionCallOutput(block) {
|
|
284
602
|
if (block.type !== "tool_result") {
|
|
285
603
|
throw new OpenAITranslationError(
|
|
@@ -309,6 +627,8 @@ function toTool2(spec) {
|
|
|
309
627
|
async function* translateOpenAIStream(events) {
|
|
310
628
|
let sawToolCall = false;
|
|
311
629
|
let sawRefusal = false;
|
|
630
|
+
let searches = 0;
|
|
631
|
+
const withSearches = (usage) => searches > 0 ? { ...usage, webSearchRequests: searches } : usage;
|
|
312
632
|
for await (const event of events) {
|
|
313
633
|
switch (event.type) {
|
|
314
634
|
case "response.output_text.delta":
|
|
@@ -321,37 +641,66 @@ async function* translateOpenAIStream(events) {
|
|
|
321
641
|
case "response.output_item.done":
|
|
322
642
|
if (event.item.type === "function_call") {
|
|
323
643
|
sawToolCall = true;
|
|
644
|
+
yield { type: "tool_call", id: event.item.call_id, name: event.item.name, ...parseToolArguments(event.item.arguments) };
|
|
645
|
+
} else if (event.item.type === "web_search_call") {
|
|
646
|
+
searches += 1;
|
|
647
|
+
const item = event.item;
|
|
648
|
+
const action = item.action;
|
|
649
|
+
const sources = action.type === "search" ? (action.sources ?? []).map((s) => ({ url: s.url })) : [];
|
|
650
|
+
yield { type: "provider_tool_call", block: { type: "provider_tool_call", id: item.id, name: "web_search", provider: "openai", input: action } };
|
|
324
651
|
yield {
|
|
325
|
-
type: "
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
652
|
+
type: "provider_tool_result",
|
|
653
|
+
block: {
|
|
654
|
+
type: "provider_tool_result",
|
|
655
|
+
callId: item.id,
|
|
656
|
+
name: "web_search",
|
|
657
|
+
provider: "openai",
|
|
658
|
+
results: sources,
|
|
659
|
+
...item.status === "failed" ? { error: "failed" } : {},
|
|
660
|
+
opaque: item
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
} else if (event.item.type === "reasoning" && typeof event.item.encrypted_content === "string") {
|
|
664
|
+
const item = event.item;
|
|
665
|
+
const text = [
|
|
666
|
+
...(item.content ?? []).map((c) => c.text),
|
|
667
|
+
...item.summary.map((s) => s.text)
|
|
668
|
+
].filter((t) => t !== "").join("\n");
|
|
669
|
+
yield {
|
|
670
|
+
type: "reasoning",
|
|
671
|
+
block: {
|
|
672
|
+
type: "reasoning",
|
|
673
|
+
provider: "openai",
|
|
674
|
+
...text !== "" ? { text } : {},
|
|
675
|
+
opaque: { id: item.id, summary: item.summary, encrypted_content: item.encrypted_content }
|
|
676
|
+
}
|
|
329
677
|
};
|
|
330
678
|
}
|
|
331
679
|
break;
|
|
332
680
|
case "response.completed":
|
|
333
|
-
yield
|
|
681
|
+
yield { type: "usage", usage: withSearches(usageOf(event.response)) };
|
|
334
682
|
yield { type: "stop", reason: sawRefusal ? "refusal" : sawToolCall ? "tool_use" : "end_turn" };
|
|
335
683
|
break;
|
|
336
684
|
case "response.incomplete": {
|
|
337
|
-
yield
|
|
685
|
+
yield { type: "usage", usage: withSearches(usageOf(event.response)) };
|
|
338
686
|
yield { type: "stop", reason: mapIncompleteReason(event.response) };
|
|
339
687
|
break;
|
|
340
688
|
}
|
|
341
689
|
case "response.failed": {
|
|
342
690
|
const err = event.response.error;
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
691
|
+
const message = `OpenAI response failed: ${err ? `${err.code}: ${err.message}` : "unknown error"}`;
|
|
692
|
+
throw new OpenAITranslationError(message, classifyFailure("ResponseFailed", void 0, message.toLowerCase()));
|
|
693
|
+
}
|
|
694
|
+
case "error": {
|
|
695
|
+
const message = `OpenAI stream error: ${event.message}`;
|
|
696
|
+
throw new OpenAITranslationError(message, classifyFailure("StreamError", void 0, `${event.code ?? ""} ${message}`.toLowerCase()));
|
|
346
697
|
}
|
|
347
|
-
case "error":
|
|
348
|
-
throw new OpenAITranslationError(`OpenAI stream error: ${event.message}`);
|
|
349
698
|
default:
|
|
350
699
|
break;
|
|
351
700
|
}
|
|
352
701
|
}
|
|
353
702
|
}
|
|
354
|
-
function
|
|
703
|
+
function usageOf(response) {
|
|
355
704
|
const u = response.usage;
|
|
356
705
|
const cachedRead = u?.input_tokens_details?.cached_tokens ?? 0;
|
|
357
706
|
const usage = {
|
|
@@ -365,7 +714,12 @@ function usageEvent(response) {
|
|
|
365
714
|
if (cachedRead > 0) usage.cacheReadInputTokens = cachedRead;
|
|
366
715
|
const cacheWrite = u?.input_tokens_details?.cache_write_tokens;
|
|
367
716
|
if (cacheWrite !== void 0 && cacheWrite > 0) usage.cacheWriteInputTokens = cacheWrite;
|
|
368
|
-
|
|
717
|
+
const reasoningTokens = u?.output_tokens_details?.reasoning_tokens;
|
|
718
|
+
if (reasoningTokens !== void 0 && reasoningTokens > 0) usage.reasoningTokens = reasoningTokens;
|
|
719
|
+
const served = response.service_tier;
|
|
720
|
+
if (served === "default") usage.serviceTier = "standard";
|
|
721
|
+
else if (served === "flex" || served === "priority") usage.serviceTier = served;
|
|
722
|
+
return usage;
|
|
369
723
|
}
|
|
370
724
|
function mapIncompleteReason(response) {
|
|
371
725
|
const reason = response.incomplete_details?.reason;
|
|
@@ -376,7 +730,8 @@ function mapIncompleteReason(response) {
|
|
|
376
730
|
return "refusal";
|
|
377
731
|
default:
|
|
378
732
|
throw new OpenAITranslationError(
|
|
379
|
-
`Unmapped OpenAI incomplete reason ${JSON.stringify(reason)} \u2014 provider drift
|
|
733
|
+
`Unmapped OpenAI incomplete reason ${JSON.stringify(reason)} \u2014 provider drift?`,
|
|
734
|
+
"provider_drift"
|
|
380
735
|
);
|
|
381
736
|
}
|
|
382
737
|
}
|
|
@@ -395,21 +750,136 @@ var OpenAIModelClient = class {
|
|
|
395
750
|
return translateOpenAIStream(this.#rawEvents(params, opts?.signal));
|
|
396
751
|
}
|
|
397
752
|
async *#rawEvents(params, signal) {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
753
|
+
try {
|
|
754
|
+
const stream = await this.#client.responses.create(
|
|
755
|
+
params,
|
|
756
|
+
signal !== void 0 ? { signal } : void 0
|
|
757
|
+
);
|
|
758
|
+
for await (const event of stream) yield event;
|
|
759
|
+
} catch (err) {
|
|
760
|
+
throw toProviderError("openai", err);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
// src/openai/jobs.ts
|
|
766
|
+
import OpenAI2, { toFile } from "openai";
|
|
767
|
+
function toBatchLines(items) {
|
|
768
|
+
return items.map((item) => {
|
|
769
|
+
const { serviceTier: _tier, ...req } = item.request;
|
|
770
|
+
void _tier;
|
|
771
|
+
const { stream: _stream, ...body } = toOpenAIParams(req);
|
|
772
|
+
void _stream;
|
|
773
|
+
return { custom_id: item.id, method: "POST", url: "/v1/responses", body };
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
function translateResponse(response) {
|
|
777
|
+
const blocks = [];
|
|
778
|
+
let sawToolCall = false;
|
|
779
|
+
let sawRefusal = false;
|
|
780
|
+
for (const item of response.output) {
|
|
781
|
+
switch (item.type) {
|
|
782
|
+
case "message":
|
|
783
|
+
for (const part of item.content) {
|
|
784
|
+
if (part.type === "output_text") blocks.push({ type: "text", text: part.text });
|
|
785
|
+
else if (part.type === "refusal") {
|
|
786
|
+
sawRefusal = true;
|
|
787
|
+
blocks.push({ type: "text", text: part.refusal });
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
break;
|
|
791
|
+
case "function_call":
|
|
792
|
+
sawToolCall = true;
|
|
793
|
+
blocks.push({
|
|
794
|
+
type: "tool_call",
|
|
795
|
+
id: item.call_id,
|
|
796
|
+
name: item.name,
|
|
797
|
+
input: item.arguments === "" ? {} : JSON.parse(item.arguments)
|
|
798
|
+
});
|
|
799
|
+
break;
|
|
800
|
+
case "reasoning": {
|
|
801
|
+
if (typeof item.encrypted_content !== "string") break;
|
|
802
|
+
const text = [...(item.content ?? []).map((c) => c.text), ...item.summary.map((s) => s.text)].filter((t) => t !== "").join("\n");
|
|
803
|
+
blocks.push({
|
|
804
|
+
type: "reasoning",
|
|
805
|
+
provider: "openai",
|
|
806
|
+
...text !== "" ? { text } : {},
|
|
807
|
+
opaque: { id: item.id, summary: item.summary, encrypted_content: item.encrypted_content }
|
|
808
|
+
});
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
811
|
+
default:
|
|
812
|
+
throw new OpenAITranslationError(`Unmapped OpenAI output item ${JSON.stringify(item.type)} \u2014 provider drift?`, "provider_drift");
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
const stop = response.incomplete_details?.reason !== void 0 && response.incomplete_details?.reason !== null ? mapIncompleteReason(response) : sawRefusal ? "refusal" : sawToolCall ? "tool_use" : "end_turn";
|
|
816
|
+
return { blocks, usage: usageOf(response), stop };
|
|
817
|
+
}
|
|
818
|
+
var OpenAIJobClient = class {
|
|
819
|
+
#client;
|
|
820
|
+
constructor(opts = {}) {
|
|
821
|
+
const init = {};
|
|
822
|
+
if (opts.apiKey !== void 0) init.apiKey = opts.apiKey;
|
|
823
|
+
if (opts.baseURL !== void 0) init.baseURL = opts.baseURL;
|
|
824
|
+
this.#client = new OpenAI2(init);
|
|
825
|
+
}
|
|
826
|
+
async submit(items) {
|
|
827
|
+
const first = items[0];
|
|
828
|
+
if (!first) throw new OpenAITranslationError("a batch needs at least one item");
|
|
829
|
+
const jsonl = toBatchLines(items).map((line) => JSON.stringify(line)).join("\n") + "\n";
|
|
830
|
+
const file = await this.#client.files.create({
|
|
831
|
+
file: await toFile(Buffer.from(jsonl, "utf8"), "alma-batch.jsonl", { type: "application/jsonl" }),
|
|
832
|
+
purpose: "batch"
|
|
833
|
+
});
|
|
834
|
+
const batch = await this.#client.batches.create({
|
|
835
|
+
input_file_id: file.id,
|
|
836
|
+
endpoint: "/v1/responses",
|
|
837
|
+
completion_window: "24h"
|
|
838
|
+
});
|
|
839
|
+
return { provider: "openai", id: batch.id, model: first.request.model };
|
|
840
|
+
}
|
|
841
|
+
async status(handle) {
|
|
842
|
+
const batch = await this.#client.batches.retrieve(handle.id);
|
|
843
|
+
const status = batch.status === "validating" ? "queued" : batch.status === "in_progress" || batch.status === "finalizing" ? "running" : batch.status === "completed" ? "done" : batch.status === "failed" ? "failed" : batch.status === "expired" ? "expired" : "cancelled";
|
|
844
|
+
const c = batch.request_counts;
|
|
845
|
+
return {
|
|
846
|
+
status,
|
|
847
|
+
...c ? { counts: { total: c.total, done: c.completed, failed: c.failed } } : {}
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
async *results(handle) {
|
|
851
|
+
const batch = await this.#client.batches.retrieve(handle.id);
|
|
852
|
+
for (const fileId of [batch.output_file_id, batch.error_file_id]) {
|
|
853
|
+
if (!fileId) continue;
|
|
854
|
+
const text = await (await this.#client.files.content(fileId)).text();
|
|
855
|
+
for (const raw of text.split("\n")) {
|
|
856
|
+
if (raw.trim() === "") continue;
|
|
857
|
+
const line = JSON.parse(raw);
|
|
858
|
+
const body = line.response?.body;
|
|
859
|
+
if (line.response && line.response.status_code >= 200 && line.response.status_code < 300 && body && "output" in body) {
|
|
860
|
+
yield { id: line.custom_id, outcome: "succeeded", output: translateResponse(body) };
|
|
861
|
+
} else {
|
|
862
|
+
const error = line.error?.message ?? (body && "error" in body ? body.error?.message : void 0) ?? `status ${line.response?.status_code ?? "unknown"}`;
|
|
863
|
+
yield { id: line.custom_id, outcome: "errored", error };
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
async cancel(handle) {
|
|
869
|
+
await this.#client.batches.cancel(handle.id);
|
|
403
870
|
}
|
|
404
871
|
};
|
|
405
872
|
|
|
406
873
|
// src/openrouter/client.ts
|
|
407
|
-
import
|
|
874
|
+
import OpenAI3 from "openai";
|
|
408
875
|
|
|
409
876
|
// src/openrouter/translate.ts
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
877
|
+
import {
|
|
878
|
+
ProviderError as ProviderError4
|
|
879
|
+
} from "@alma-harness/core";
|
|
880
|
+
var OpenRouterTranslationError = class extends ProviderError4 {
|
|
881
|
+
constructor(message, kind = "rejected") {
|
|
882
|
+
super("openrouter", kind, message);
|
|
413
883
|
this.name = "OpenRouterTranslationError";
|
|
414
884
|
}
|
|
415
885
|
};
|
|
@@ -428,6 +898,8 @@ function toOpenRouterParams(req, routing) {
|
|
|
428
898
|
if (req.tools.length > 0) {
|
|
429
899
|
provider.require_parameters = true;
|
|
430
900
|
}
|
|
901
|
+
const reasoning = req.reasoning;
|
|
902
|
+
const replay = reasoning !== void 0 && reasoning.effort !== "none";
|
|
431
903
|
const params = {
|
|
432
904
|
model: req.model.id,
|
|
433
905
|
// The SDK deprecates this in favor of the OpenAI-specific
|
|
@@ -438,9 +910,20 @@ function toOpenRouterParams(req, routing) {
|
|
|
438
910
|
// Review amendment (spec 014): streamed chat completions only carry usage
|
|
439
911
|
// when asked — without this the BudgetGuard never sees a usage event.
|
|
440
912
|
stream_options: { include_usage: true },
|
|
441
|
-
messages: [...systemMessages(req.system), ...req.messages.flatMap(toWireMessages)],
|
|
913
|
+
messages: [...systemMessages(req.system), ...req.messages.flatMap((m) => toWireMessages(m, replay))],
|
|
442
914
|
provider
|
|
443
915
|
};
|
|
916
|
+
if (reasoning !== void 0) {
|
|
917
|
+
params.reasoning = replay ? { effort: reasoning.effort === "max" ? "xhigh" : reasoning.effort } : { enabled: false };
|
|
918
|
+
}
|
|
919
|
+
if (req.serviceTier !== void 0 && req.serviceTier !== "standard") {
|
|
920
|
+
throw new OpenRouterTranslationError(
|
|
921
|
+
`the OpenRouter adapter cannot serve the ${req.serviceTier} tier \u2014 the gateway prices by upstream`
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
if ((req.providerTools ?? []).length > 0) {
|
|
925
|
+
throw new OpenRouterTranslationError("the OpenRouter adapter cannot declare provider-executed tools \u2014 the gateway has no neutral web search");
|
|
926
|
+
}
|
|
444
927
|
if (req.tools.length > 0) params.tools = req.tools.map(toTool3);
|
|
445
928
|
return params;
|
|
446
929
|
}
|
|
@@ -448,12 +931,12 @@ function systemMessages(blocks) {
|
|
|
448
931
|
if (blocks.length === 0) return [];
|
|
449
932
|
return [{ role: "system", content: blocks.map((b) => b.text).join("\n\n") }];
|
|
450
933
|
}
|
|
451
|
-
function toWireMessages(msg) {
|
|
934
|
+
function toWireMessages(msg, replayReasoning) {
|
|
452
935
|
switch (msg.role) {
|
|
453
936
|
case "user":
|
|
454
937
|
return [{ role: "user", content: msg.blocks.map(toUserContentPart2) }];
|
|
455
938
|
case "assistant":
|
|
456
|
-
return [toAssistantMessage(msg.blocks)];
|
|
939
|
+
return [toAssistantMessage(msg.blocks, replayReasoning)];
|
|
457
940
|
case "tool":
|
|
458
941
|
return msg.blocks.map(toToolMessage);
|
|
459
942
|
}
|
|
@@ -475,9 +958,10 @@ function toUserContentPart2(block) {
|
|
|
475
958
|
);
|
|
476
959
|
}
|
|
477
960
|
}
|
|
478
|
-
function toAssistantMessage(blocks) {
|
|
961
|
+
function toAssistantMessage(blocks, replayReasoning) {
|
|
479
962
|
let text = "";
|
|
480
963
|
const toolCalls = [];
|
|
964
|
+
const details = [];
|
|
481
965
|
for (const block of blocks) {
|
|
482
966
|
switch (block.type) {
|
|
483
967
|
case "text":
|
|
@@ -490,6 +974,16 @@ function toAssistantMessage(blocks) {
|
|
|
490
974
|
function: { name: block.name, arguments: JSON.stringify(block.input) }
|
|
491
975
|
});
|
|
492
976
|
break;
|
|
977
|
+
case "reasoning": {
|
|
978
|
+
const opaque = block.opaque;
|
|
979
|
+
if (block.provider === "openrouter" && replayReasoning && Array.isArray(opaque?.reasoning_details)) {
|
|
980
|
+
details.push(...opaque.reasoning_details);
|
|
981
|
+
}
|
|
982
|
+
break;
|
|
983
|
+
}
|
|
984
|
+
case "provider_tool_call":
|
|
985
|
+
case "provider_tool_result":
|
|
986
|
+
break;
|
|
493
987
|
default:
|
|
494
988
|
throw new OpenRouterTranslationError(
|
|
495
989
|
`block type ${JSON.stringify(block.type)} is not valid in an assistant message`
|
|
@@ -501,6 +995,7 @@ function toAssistantMessage(blocks) {
|
|
|
501
995
|
content: text === "" ? null : text
|
|
502
996
|
};
|
|
503
997
|
if (toolCalls.length > 0) message.tool_calls = toolCalls;
|
|
998
|
+
if (details.length > 0) message.reasoning_details = details;
|
|
504
999
|
return message;
|
|
505
1000
|
}
|
|
506
1001
|
function toToolMessage(block) {
|
|
@@ -533,9 +1028,15 @@ async function* translateOpenRouterStream(chunks) {
|
|
|
533
1028
|
let finish = null;
|
|
534
1029
|
let usage = null;
|
|
535
1030
|
let toolsEmitted = false;
|
|
1031
|
+
let reasoningText = "";
|
|
1032
|
+
const reasoningDetails = [];
|
|
1033
|
+
let reasoningEmitted = false;
|
|
536
1034
|
for await (const chunk of chunks) {
|
|
537
1035
|
const choice = chunk.choices[0];
|
|
538
1036
|
if (choice) {
|
|
1037
|
+
const extra = choice.delta;
|
|
1038
|
+
if (typeof extra.reasoning === "string") reasoningText += extra.reasoning;
|
|
1039
|
+
if (Array.isArray(extra.reasoning_details)) reasoningDetails.push(...extra.reasoning_details);
|
|
539
1040
|
if (choice.delta.content != null && choice.delta.content !== "") {
|
|
540
1041
|
yield { type: "text_delta", text: choice.delta.content };
|
|
541
1042
|
}
|
|
@@ -548,15 +1049,22 @@ async function* translateOpenRouterStream(chunks) {
|
|
|
548
1049
|
}
|
|
549
1050
|
if (choice.finish_reason != null) {
|
|
550
1051
|
finish = choice.finish_reason;
|
|
1052
|
+
if (!reasoningEmitted && (reasoningText !== "" || reasoningDetails.length > 0)) {
|
|
1053
|
+
reasoningEmitted = true;
|
|
1054
|
+
yield {
|
|
1055
|
+
type: "reasoning",
|
|
1056
|
+
block: {
|
|
1057
|
+
type: "reasoning",
|
|
1058
|
+
provider: "openrouter",
|
|
1059
|
+
...reasoningText !== "" ? { text: reasoningText } : {},
|
|
1060
|
+
...reasoningDetails.length > 0 ? { opaque: { reasoning_details: reasoningDetails } } : {}
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
551
1064
|
if (!toolsEmitted) {
|
|
552
1065
|
toolsEmitted = true;
|
|
553
1066
|
for (const [, call] of [...pendingTools.entries()].sort(([a], [b]) => a - b)) {
|
|
554
|
-
yield {
|
|
555
|
-
type: "tool_call",
|
|
556
|
-
id: call.id,
|
|
557
|
-
name: call.name,
|
|
558
|
-
input: call.json === "" ? {} : JSON.parse(call.json)
|
|
559
|
-
};
|
|
1067
|
+
yield { type: "tool_call", id: call.id, name: call.name, ...parseToolArguments(call.json) };
|
|
560
1068
|
}
|
|
561
1069
|
pendingTools.clear();
|
|
562
1070
|
}
|
|
@@ -571,11 +1079,14 @@ async function* translateOpenRouterStream(chunks) {
|
|
|
571
1079
|
outputTokens: chunk.usage.completion_tokens
|
|
572
1080
|
};
|
|
573
1081
|
if (cachedRead > 0) usage.cacheReadInputTokens = cachedRead;
|
|
1082
|
+
const reasoningTokens = chunk.usage.completion_tokens_details?.reasoning_tokens;
|
|
1083
|
+
if (reasoningTokens !== void 0 && reasoningTokens > 0) usage.reasoningTokens = reasoningTokens;
|
|
574
1084
|
}
|
|
575
1085
|
}
|
|
576
1086
|
if (finish === null) {
|
|
577
1087
|
throw new OpenRouterTranslationError(
|
|
578
|
-
"OpenRouter stream ended without a finish_reason \u2014 provider drift?"
|
|
1088
|
+
"OpenRouter stream ended without a finish_reason \u2014 provider drift?",
|
|
1089
|
+
"provider_drift"
|
|
579
1090
|
);
|
|
580
1091
|
}
|
|
581
1092
|
if (usage !== null) yield { type: "usage", usage };
|
|
@@ -593,7 +1104,8 @@ function mapFinishReason(reason) {
|
|
|
593
1104
|
return "refusal";
|
|
594
1105
|
default:
|
|
595
1106
|
throw new OpenRouterTranslationError(
|
|
596
|
-
`Unmapped OpenRouter finish_reason ${JSON.stringify(reason)} \u2014 provider drift
|
|
1107
|
+
`Unmapped OpenRouter finish_reason ${JSON.stringify(reason)} \u2014 provider drift?`,
|
|
1108
|
+
"provider_drift"
|
|
597
1109
|
);
|
|
598
1110
|
}
|
|
599
1111
|
}
|
|
@@ -620,36 +1132,47 @@ var OpenRouterModelClient = class {
|
|
|
620
1132
|
"OpenRouterModelClient needs an API key: pass `apiKey` or set OPENROUTER_API_KEY"
|
|
621
1133
|
);
|
|
622
1134
|
}
|
|
623
|
-
this.#client = new
|
|
1135
|
+
this.#client = new OpenAI3({ apiKey, baseURL: opts.baseURL ?? OPENROUTER_BASE_URL });
|
|
624
1136
|
}
|
|
625
1137
|
stream(req, opts) {
|
|
626
1138
|
const params = toOpenRouterParams(req, this.#routing);
|
|
627
1139
|
return translateOpenRouterStream(this.#rawChunks(params, opts?.signal));
|
|
628
1140
|
}
|
|
629
1141
|
async *#rawChunks(params, signal) {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
1142
|
+
try {
|
|
1143
|
+
const stream = await this.#client.chat.completions.create(
|
|
1144
|
+
params,
|
|
1145
|
+
signal !== void 0 ? { signal } : void 0
|
|
1146
|
+
);
|
|
1147
|
+
for await (const chunk of stream) yield chunk;
|
|
1148
|
+
} catch (err) {
|
|
1149
|
+
throw toProviderError("openrouter", err);
|
|
1150
|
+
}
|
|
635
1151
|
}
|
|
636
1152
|
};
|
|
637
1153
|
|
|
638
1154
|
// src/index.ts
|
|
639
1155
|
var SUPPORTED_PROVIDERS = ["anthropic", "openai", "openrouter"];
|
|
640
1156
|
export {
|
|
1157
|
+
AnthropicJobClient,
|
|
641
1158
|
AnthropicModelClient,
|
|
642
1159
|
AnthropicTranslationError,
|
|
1160
|
+
OpenAIJobClient,
|
|
643
1161
|
OpenAIModelClient,
|
|
644
1162
|
OpenAITranslationError,
|
|
645
1163
|
OpenRouterModelClient,
|
|
646
1164
|
OpenRouterTranslationError,
|
|
647
1165
|
SUPPORTED_PROVIDERS,
|
|
1166
|
+
classifyFailure,
|
|
648
1167
|
toAnthropicParams,
|
|
1168
|
+
toBatchLines,
|
|
649
1169
|
toOpenAIParams,
|
|
650
1170
|
toOpenRouterParams,
|
|
1171
|
+
toProviderError,
|
|
1172
|
+
translateMessage,
|
|
651
1173
|
translateOpenAIStream,
|
|
652
1174
|
translateOpenRouterStream,
|
|
1175
|
+
translateResponse,
|
|
653
1176
|
translateStream
|
|
654
1177
|
};
|
|
655
1178
|
//# sourceMappingURL=index.js.map
|