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