@anteros/core 0.0.41 → 0.0.44

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.
Files changed (133) hide show
  1. package/database/audit.ts +9 -59
  2. package/database/collection.ts +3 -1
  3. package/database/deleteLog.ts +67 -3
  4. package/database/file.ts +4 -3
  5. package/database/mongodbadapter.ts +103 -42
  6. package/database/replication.ts +724 -58
  7. package/database/ttl.ts +68 -0
  8. package/database/workflow.ts +466 -240
  9. package/index.ts +6 -0
  10. package/lib/agent.ts +1567 -0
  11. package/lib/agentMemory.ts +689 -0
  12. package/lib/agents.ts +180 -0
  13. package/lib/ai.ts +0 -0
  14. package/lib/api.ts +246 -0
  15. package/lib/attachment.ts +351 -0
  16. package/lib/auditResult.ts +13 -0
  17. package/lib/define.ts +43 -1
  18. package/lib/endpoints.ts +80 -0
  19. package/lib/files.ts +2 -1
  20. package/lib/jsonSchema.ts +183 -0
  21. package/lib/lifecycle.ts +7 -1
  22. package/lib/load.ts +17 -0
  23. package/lib/mcp.ts +3 -2
  24. package/lib/mcpClient.ts +128 -0
  25. package/lib/messages.ts +299 -0
  26. package/lib/providers.ts +970 -0
  27. package/lib/routes.ts +14 -1
  28. package/lib/scriptWorker.ts +4 -1
  29. package/lib/scripts.ts +6 -1
  30. package/lib/skills.ts +481 -0
  31. package/lib/sockets.ts +3 -1
  32. package/lib/workflow.ts +209 -37
  33. package/lib/zip.ts +109 -0
  34. package/package.json +1 -1
  35. package/server/access.ts +96 -0
  36. package/server/agents.ts +472 -0
  37. package/server/api.ts +80 -65
  38. package/server/boot.ts +11 -0
  39. package/server/hono.ts +8 -3
  40. package/server/mcp.ts +17 -63
  41. package/server/routes.ts +19 -24
  42. package/tests/agent-mcp.test.ts +116 -0
  43. package/tests/agent-memory.test.ts +617 -0
  44. package/tests/agent-provider.test.ts +288 -0
  45. package/tests/agent-runtime.test.ts +369 -0
  46. package/tests/agent-skills.test.ts +177 -0
  47. package/tests/agent-summarize.test.ts +124 -0
  48. package/tests/agent-working-memory.test.ts +404 -0
  49. package/tests/agent.test.ts +1241 -0
  50. package/tests/agents-api.test.ts +593 -0
  51. package/tests/api-private-fields.test.ts +196 -0
  52. package/tests/api.test.ts +51 -0
  53. package/tests/attachment.test.ts +241 -0
  54. package/tests/audit-operations.test.ts +1 -1
  55. package/tests/audit-redact.test.ts +29 -3
  56. package/tests/audit-run-action.test.ts +1 -1
  57. package/tests/endpoints.test.ts +80 -0
  58. package/tests/file-collection-replication.test.ts +2 -0
  59. package/tests/fixtures/agent-tenant/agents/support.agent.ts +9 -0
  60. package/tests/fixtures/agent-tenant/agents/weather.agent.ts +21 -0
  61. package/tests/fixtures/agents-api/agents/assistant.agent.ts +46 -0
  62. package/tests/fixtures/agents-api/agents/plain.agent.ts +23 -0
  63. package/tests/fixtures/agents-api/agents/private.agent.ts +15 -0
  64. package/tests/fixtures/agents-api/collections/notes.model.ts +26 -0
  65. package/tests/fixtures/agents-api/routes/probe.route.ts +21 -0
  66. package/tests/fixtures/agents-api/services/probe.service.ts +19 -0
  67. package/tests/fixtures/attachment/data.csv +2 -0
  68. package/tests/fixtures/fake-provider.ts +128 -0
  69. package/tests/fixtures/repl-only/app.ts +3 -0
  70. package/tests/fixtures/replication-meta/agents/chat.agent.ts +16 -0
  71. package/tests/fixtures/replication-meta/collections/counters.model.ts +13 -0
  72. package/tests/fixtures/replication-meta/collections/orders.model.ts +9 -0
  73. package/tests/fixtures/replication-meta/workflows/run.workflow.ts +8 -0
  74. package/tests/fixtures/skills-tenant/agents/docs.agent.ts +23 -0
  75. package/tests/fixtures/skills-tenant/agents/incomplete.agent.ts +14 -0
  76. package/tests/fixtures/skills-tenant/broken/bad-name.md +6 -0
  77. package/tests/fixtures/skills-tenant/broken/dup-a.md +6 -0
  78. package/tests/fixtures/skills-tenant/broken/dup-b.md +6 -0
  79. package/tests/fixtures/skills-tenant/broken/empty-body.md +4 -0
  80. package/tests/fixtures/skills-tenant/broken/long-compat.md +7 -0
  81. package/tests/fixtures/skills-tenant/broken/long-description.md +6 -0
  82. package/tests/fixtures/skills-tenant/broken/mismatch/SKILL.md +6 -0
  83. package/tests/fixtures/skills-tenant/broken/no-description.md +5 -0
  84. package/tests/fixtures/skills-tenant/broken/no-name.md +5 -0
  85. package/tests/fixtures/skills-tenant/broken/plain.md +4 -0
  86. package/tests/fixtures/skills-tenant/broken/unknown-key.md +9 -0
  87. package/tests/fixtures/skills-tenant/docs/pdf-forms.md +18 -0
  88. package/tests/fixtures/skills-tenant/guide/ml.md +9 -0
  89. package/tests/fixtures/skills-tenant/guides/pdf/SKILL.md +9 -0
  90. package/tests/fixtures/skills-tenant/runbooks/incident.md +9 -0
  91. package/tests/fixtures/skills-tenant/runbooks/notes.txt +2 -0
  92. package/tests/fixtures/vars-tenant/vars/private.var.ts +4 -1
  93. package/tests/fixtures/workflow-engine/workflows/failing.workflow.ts +34 -0
  94. package/tests/fixtures/workflow-engine/workflows/flaky.workflow.ts +23 -0
  95. package/tests/fixtures/workflow-engine/workflows/hang.workflow.ts +17 -0
  96. package/tests/fixtures/workflow-engine/workflows/simple.workflow.ts +19 -0
  97. package/tests/fixtures/workflow-engine/workflows/slow.workflow.ts +23 -0
  98. package/tests/fixtures/workflow-engine/workflows/versioned.workflow.ts +19 -0
  99. package/tests/fixtures/workflow-tenant/workflows/broken.workflow.ts +3 -0
  100. package/tests/fixtures/workflow-tenant/workflows/disabled.workflow.ts +8 -0
  101. package/tests/fixtures/workflow-tenant/workflows/invoice.workflow.ts +21 -0
  102. package/tests/fixtures/workflow-tenant/workflows/no-id.workflow.ts +7 -0
  103. package/tests/fixtures/workflow-tenant/workflows/order-duplicate.workflow.ts +13 -0
  104. package/tests/fixtures/workflow-tenant/workflows/order.workflow.ts +17 -0
  105. package/tests/hono.test.ts +6 -3
  106. package/tests/locks.test.ts +120 -0
  107. package/tests/mcp.test.ts +11 -11
  108. package/tests/messages.test.ts +258 -0
  109. package/tests/replication-change-stream.test.ts +240 -0
  110. package/tests/replication-control.test.ts +4 -2
  111. package/tests/replication-initial-sync.test.ts +2 -0
  112. package/tests/replication-meta.test.ts +246 -0
  113. package/tests/replication.test.ts +2 -0
  114. package/tests/upload.test.ts +44 -12
  115. package/tests/vars-api.test.ts +1 -1
  116. package/tests/vars-replication.test.ts +5 -2
  117. package/tests/workflow-engine.test.ts +340 -0
  118. package/tests/workflow-retention.test.ts +103 -0
  119. package/tests/workflow.test.ts +186 -0
  120. package/types/agent.d.ts +1067 -0
  121. package/types/config.d.ts +16 -0
  122. package/types/file.d.ts +7 -7
  123. package/types/hook.d.ts +6 -0
  124. package/types/lifecycle.d.ts +6 -0
  125. package/types/mcp.d.ts +11 -1
  126. package/types/replication.d.ts +70 -4
  127. package/types/route.d.ts +7 -1
  128. package/types/scripts.d.ts +6 -0
  129. package/types/service.d.ts +7 -1
  130. package/types/tenant.d.ts +13 -0
  131. package/types/vars.d.ts +2 -2
  132. package/types/websocket.d.ts +6 -0
  133. package/types/workflow.d.ts +50 -3
@@ -0,0 +1,970 @@
1
+ import { AppError } from "./error";
2
+ import { logger } from "../utils/logger";
3
+ import type {
4
+ AgentProtocol,
5
+ AgentMediaPart,
6
+ AgentMessage,
7
+ AgentProvider,
8
+ AgentThinking,
9
+ AgentThinkingBlock,
10
+ AgentToolChoice,
11
+ AgentUsage,
12
+ } from "../types/agent";
13
+
14
+ /**
15
+ * Model provider adapters.
16
+ *
17
+ * Two wire protocols cover the whole field:
18
+ *
19
+ * - `openai` — `POST {baseUrl}/chat/completions`, `Authorization: Bearer …`.
20
+ * Every OpenAI-compatible gateway (Groq, Mistral, OpenRouter,
21
+ * Ollama, LM Studio, vLLM…) speaks it too, only `baseUrl` changes.
22
+ * - `anthropic` — `POST {baseUrl}/v1/messages`, `x-api-key: …`.
23
+ *
24
+ * Both are called with `fetch` — no SDK, no transitive dependency, and the
25
+ * streaming path is plain SSE in both cases. The agent runtime only ever sees
26
+ * the normalized shapes below.
27
+ */
28
+
29
+ /** A tool advertised to the model (already a JSON Schema). */
30
+ export type AgentToolSpec = {
31
+ name: string;
32
+ description?: string;
33
+ parameters: any;
34
+ };
35
+
36
+ export type ModelChatRequest = {
37
+ /** System prompt (kept separate from `messages` — Anthropic wants it out of band). */
38
+ system?: string;
39
+ messages: AgentMessage[];
40
+ tools?: AgentToolSpec[];
41
+ /** How the model may use the tools (default: it chooses). */
42
+ toolChoice?: AgentToolChoice;
43
+ /** Extended thinking — mapped to a budget (Anthropic) or an effort (OpenAI). */
44
+ thinking?: AgentThinking;
45
+ /** Per-request override of the adapter's `options.temperature`. */
46
+ temperature?: number;
47
+ /** Per-request override of the adapter's `options.maxTokens`. */
48
+ maxTokens?: number;
49
+ signal?: AbortSignal;
50
+ /** Ask for a JSON object (OpenAI-compatible providers: `response_format`). */
51
+ json?: boolean;
52
+ /**
53
+ * Ask for a JSON object **matching this schema** — what the provider can really
54
+ * enforce: `response_format.json_schema` on OpenAI-compatible gateways, a forced
55
+ * tool on Anthropic (which has no such field). Takes precedence over `json`.
56
+ */
57
+ jsonSchema?: { name: string; description?: string; schema: any };
58
+ };
59
+
60
+ export type ModelChatResponse = {
61
+ text: string;
62
+ toolCalls: Array<{ id: string; name: string; argsText: string }>;
63
+ finishReason: string;
64
+ usage: AgentUsage;
65
+ /** The model's reasoning, when extended thinking is on. */
66
+ reasoning?: string;
67
+ /** The raw reasoning blocks — Anthropic needs them echoed back with a tool round. */
68
+ thinking?: AgentThinkingBlock[];
69
+ raw?: any;
70
+ };
71
+
72
+ export type ModelStreamEvent =
73
+ | { type: "text"; text: string }
74
+ /** Thinking output — the consumer decides whether to show it. */
75
+ | { type: "reasoning"; text: string }
76
+ /**
77
+ * The finished reasoning blocks, signatures included — emitted at the end of a
78
+ * turn. Anthropic requires them to be echoed back verbatim on a tool round.
79
+ */
80
+ | { type: "thinking_blocks"; blocks: AgentThinkingBlock[] }
81
+ | { type: "tool_call"; index: number; id?: string; name?: string; argsText?: string }
82
+ | { type: "usage"; usage: Partial<AgentUsage> }
83
+ | { type: "finish"; finishReason: string };
84
+
85
+ export type ModelAdapter = {
86
+ /** The wire protocol this adapter speaks. */
87
+ protocol: AgentProtocol;
88
+ model: string;
89
+ chat(req: ModelChatRequest): Promise<ModelChatResponse>;
90
+ chatStream(req: ModelChatRequest): AsyncGenerator<ModelStreamEvent, void, unknown>;
91
+ };
92
+
93
+ const DEFAULT_BASE_URL: Record<AgentProtocol, string> = {
94
+ openai: "https://api.openai.com/v1",
95
+ anthropic: "https://api.anthropic.com",
96
+ };
97
+
98
+ const ENV_API_KEY: Record<AgentProtocol, string[]> = {
99
+ openai: ["ANTEROS_AI_API_KEY", "OPENAI_API_KEY"],
100
+ anthropic: ["ANTEROS_AI_API_KEY", "ANTHROPIC_API_KEY"],
101
+ };
102
+
103
+ const DEFAULT_MAX_TOKENS = 4096;
104
+ const DEFAULT_TIMEOUT = 120_000;
105
+ const DEFAULT_RETRIES = 2;
106
+
107
+ const EMPTY_USAGE: AgentUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0, requests: 0 };
108
+
109
+ // ─── Configuration resolution ────────────────────────────────────────────
110
+
111
+ /**
112
+ * The wire protocol to speak. `protocol` is canonical; `compatible` is the
113
+ * deprecated alias kept for definitions written before the rename — `protocol`
114
+ * wins when both are set, so an override never silently loses to the old field.
115
+ */
116
+ function resolveProtocol(provider: AgentProvider): AgentProtocol {
117
+ const protocol = provider.protocol ?? provider.compatible ?? "openai";
118
+ if (protocol !== "openai" && protocol !== "anthropic") {
119
+ throw new AppError(`Unknown provider 'protocol': '${protocol}' — expected 'openai' or 'anthropic'`, {
120
+ status: 500,
121
+ code: "AGENT_PROVIDER_INVALID",
122
+ });
123
+ }
124
+ return protocol;
125
+ }
126
+
127
+ /** @deprecated Renamed to `resolveProtocol`. */
128
+ const resolveCompatible = resolveProtocol;
129
+
130
+ function resolveApiKey(provider: AgentProvider, protocol: AgentProtocol): string {
131
+ if (provider.apiKey) return provider.apiKey;
132
+ for (const name of ENV_API_KEY[protocol]) {
133
+ const value = Bun.env?.[name] ?? process.env?.[name];
134
+ if (value) return value;
135
+ }
136
+ throw new AppError(
137
+ `No API key for provider '${provider.model}' (protocol: ${protocol}) — pass \`provider.apiKey\` or set ${ENV_API_KEY[protocol].join(" / ")}`,
138
+ { status: 500, code: "AGENT_PROVIDER_NO_API_KEY" },
139
+ );
140
+ }
141
+
142
+ /**
143
+ * API root. A bare host gets the conventional prefix appended
144
+ * (`http://localhost:11434` → `http://localhost:11434/v1` for OpenAI-compatible
145
+ * servers), an explicit path is used verbatim.
146
+ */
147
+ function resolveBaseUrl(provider: AgentProvider, protocol: AgentProtocol): string {
148
+ const raw = provider.baseUrl ?? DEFAULT_BASE_URL[protocol];
149
+ const trimmed = raw.replace(/\/+$/, "");
150
+ if (protocol !== "openai") return trimmed;
151
+ try {
152
+ const url = new URL(trimmed);
153
+ if (url.pathname === "" || url.pathname === "/") url.pathname = "/v1";
154
+ return url.toString().replace(/\/+$/, "");
155
+ } catch {
156
+ return trimmed;
157
+ }
158
+ }
159
+
160
+ // ─── Message conversion ──────────────────────────────────────────────────
161
+
162
+ /** A part the adapters must never see — `normalizeMessages` hoists tool parts away. */
163
+ function unsupportedPart(part: unknown): AppError {
164
+ return new AppError(
165
+ `Unsupported content part '${(part as any)?.type}' — a \`tool-call\` / \`tool-result\` part belongs to the message, not its content`,
166
+ { status: 500, code: "AGENT_CONTENT_INVALID" },
167
+ );
168
+ }
169
+
170
+ function openaiContent(content: string | AgentMediaPart[]): any {
171
+ if (typeof content === "string") return content;
172
+ return content.map((part) => {
173
+ switch (part.type) {
174
+ case "text":
175
+ return { type: "text", text: part.text };
176
+ case "image":
177
+ // A URL is handed to the endpoint (it fetches the image itself);
178
+ // base64 becomes the `data:` URL OpenAI expects
179
+ return {
180
+ type: "image_url",
181
+ image_url: {
182
+ url: part.url ?? `data:${part.mimeType ?? "image/png"};base64,${part.data}`,
183
+ },
184
+ };
185
+ case "file":
186
+ // Document input (PDF) — supported by the recent models; a model that
187
+ // does not accept it answers with a clear provider error. There is no
188
+ // URL form: it has been inlined by `normalizeMessages`.
189
+ return {
190
+ type: "file",
191
+ file: {
192
+ filename: part.name ?? "document.pdf",
193
+ file_data: part.url ?? `data:${part.mimeType ?? "application/pdf"};base64,${part.data}`,
194
+ },
195
+ };
196
+ default:
197
+ // `tool-call` / `tool-result` parts are hoisted by `normalizeMessages`
198
+ // before this point — reaching one here is a bug, not a payload
199
+ throw unsupportedPart(part);
200
+ }
201
+ });
202
+ }
203
+
204
+ /** `AgentMessage[]` → OpenAI `messages[]`. */
205
+ function openaiMessages(system: string | undefined, messages: AgentMessage[]): any[] {
206
+ const out: any[] = [];
207
+ if (system) out.push({ role: "system", content: system });
208
+ for (const message of messages) {
209
+ const name = message.name ? { name: message.name } : {};
210
+ switch (message.role) {
211
+ case "system":
212
+ case "developer":
213
+ // `developer` is OpenAI's own role — passed through as such
214
+ out.push({ role: message.role, content: message.content, ...name });
215
+ break;
216
+ case "user":
217
+ out.push({ role: "user", content: openaiContent(message.content), ...name });
218
+ break;
219
+ case "assistant": {
220
+ const msg: any = { role: "assistant", content: message.content ?? null, ...name };
221
+ if (message.toolCalls?.length) {
222
+ msg.tool_calls = message.toolCalls.map((call) => ({
223
+ id: call.id,
224
+ type: "function",
225
+ function: { name: call.name, arguments: call.argsText ?? JSON.stringify(call.args ?? {}) },
226
+ }));
227
+ }
228
+ out.push(msg);
229
+ break;
230
+ }
231
+ case "tool":
232
+ out.push({ role: "tool", tool_call_id: message.toolCallId, content: message.content });
233
+ break;
234
+ }
235
+ }
236
+ return out;
237
+ }
238
+
239
+ function anthropicContent(content: string | AgentMediaPart[]): any {
240
+ if (typeof content === "string") return [{ type: "text", text: content }];
241
+ return content.map((part) => {
242
+ switch (part.type) {
243
+ case "text":
244
+ return { type: "text", text: part.text };
245
+ case "image":
246
+ return {
247
+ type: "image",
248
+ source: part.url
249
+ ? { type: "url", url: part.url }
250
+ : { type: "base64", media_type: part.mimeType ?? "image/png", data: part.data },
251
+ };
252
+ case "file":
253
+ return {
254
+ type: "document",
255
+ source: part.url
256
+ ? { type: "url", url: part.url }
257
+ : { type: "base64", media_type: part.mimeType ?? "application/pdf", data: part.data },
258
+ };
259
+ default:
260
+ throw unsupportedPart(part);
261
+ }
262
+ });
263
+ }
264
+
265
+ /**
266
+ * `AgentMessage[]` → Anthropic `messages[]`.
267
+ *
268
+ * Two structural differences with OpenAI: `system` is out of band (returned
269
+ * separately) and a tool result is a **user** message holding `tool_result`
270
+ * blocks — consecutive results are merged into a single turn. A `developer`
271
+ * message joins the system prompt, and a message `name` is dropped (the API has
272
+ * no such field).
273
+ */
274
+ function anthropicMessages(messages: AgentMessage[]): { system: string | undefined; messages: any[] } {
275
+ const out: any[] = [];
276
+ const systems: string[] = [];
277
+
278
+ for (const message of messages) {
279
+ switch (message.role) {
280
+ case "system":
281
+ case "developer":
282
+ systems.push(message.content);
283
+ break;
284
+ case "user":
285
+ out.push({ role: "user", content: anthropicContent(message.content) });
286
+ break;
287
+ case "assistant": {
288
+ // The thinking blocks come first and **as returned**: Anthropic rejects a
289
+ // tool round whose assistant turn lost its signed reasoning blocks.
290
+ const blocks: any[] = [...(message.thinking ?? [])];
291
+ if (message.content) blocks.push({ type: "text", text: message.content });
292
+ for (const call of message.toolCalls ?? []) {
293
+ blocks.push({ type: "tool_use", id: call.id, name: call.name, input: call.args ?? {} });
294
+ }
295
+ out.push({ role: "assistant", content: blocks.length ? blocks : [{ type: "text", text: "" }] });
296
+ break;
297
+ }
298
+ case "tool": {
299
+ const block: any = { type: "tool_result", tool_use_id: message.toolCallId, content: message.content };
300
+ if (message.isError) block.is_error = true;
301
+ const last = out[out.length - 1];
302
+ if (last?.role === "user" && Array.isArray(last.content)
303
+ && last.content.every((b: any) => b.type === "tool_result")) {
304
+ last.content.push(block);
305
+ } else {
306
+ out.push({ role: "user", content: [block] });
307
+ }
308
+ break;
309
+ }
310
+ }
311
+ }
312
+
313
+ return { system: systems.length ? systems.join("\n\n") : undefined, messages: out };
314
+ }
315
+
316
+ function openaiToolSpecs(tools: AgentToolSpec[]): any[] {
317
+ return tools.map((tool) => ({
318
+ type: "function",
319
+ function: {
320
+ name: tool.name,
321
+ description: tool.description,
322
+ parameters: tool.parameters,
323
+ },
324
+ }));
325
+ }
326
+
327
+ function anthropicToolSpecs(tools: AgentToolSpec[]): any[] {
328
+ return tools.map((tool) => ({
329
+ name: tool.name,
330
+ description: tool.description,
331
+ input_schema: tool.parameters,
332
+ }));
333
+ }
334
+
335
+ /** Extended thinking, normalized: an effort level and/or an Anthropic token budget. */
336
+ function thinkingOf(thinking?: AgentThinking): { effort: "low" | "medium" | "high"; budgetTokens: number } | null {
337
+ if (!thinking) return null;
338
+ if (thinking === true) return { effort: "medium", budgetTokens: 4_096 };
339
+ if (thinking === "low") return { effort: "low", budgetTokens: 2_048 };
340
+ if (thinking === "medium") return { effort: "medium", budgetTokens: 4_096 };
341
+ if (thinking === "high") return { effort: "high", budgetTokens: 8_192 };
342
+
343
+ const budgetTokens = Number((thinking as { budgetTokens?: number })?.budgetTokens);
344
+ if (!Number.isFinite(budgetTokens) || budgetTokens < 1_024) {
345
+ throw new AppError("`thinking.budgetTokens` must be at least 1024", {
346
+ status: 400, code: "AGENT_THINKING_INVALID",
347
+ });
348
+ }
349
+ // OpenAI-compatible endpoints only know levels — pick the closest one
350
+ const effort = budgetTokens <= 2_048 ? "low" : budgetTokens <= 8_192 ? "medium" : "high";
351
+ return { effort, budgetTokens };
352
+ }
353
+
354
+ /** The maximum, forced-tool name an Anthropic run uses for structured output. */
355
+ const SCHEMA_TOOL = "structured_output";
356
+ /**
357
+ * A gateway that does not know `response_format.json_schema` refuses the request
358
+ * with a 400 that names it — that is the only case we downgrade for.
359
+ */
360
+ const SCHEMA_UNSUPPORTED = /response_format|json_schema|json mode/i;
361
+
362
+ /** `tool_choice` — the one place the two vocabularies differ. */
363
+ function openaiToolChoice(choice?: AgentToolChoice): any | undefined {
364
+ if (!choice || choice === "auto") return undefined;
365
+ if (choice === "none" || choice === "required") return choice;
366
+ return { type: "function", function: { name: choice.name } };
367
+ }
368
+
369
+ function anthropicToolChoice(choice?: AgentToolChoice): any | undefined {
370
+ if (!choice || choice === "auto") return undefined;
371
+ if (choice === "none") return { type: "none" };
372
+ if (choice === "required") return { type: "any" };
373
+ return { type: "tool", name: choice.name };
374
+ }
375
+
376
+ /** Provider-specific finish reasons → the normalized vocabulary. */
377
+ function normalizeFinishReason(reason: string | null | undefined): string {
378
+ switch (reason) {
379
+ case "tool_calls":
380
+ case "tool_use":
381
+ return "tool_use";
382
+ case "end_turn":
383
+ case "stop_sequence":
384
+ case "stop":
385
+ return "stop";
386
+ case "max_tokens":
387
+ case "length":
388
+ return "length";
389
+ case "content_filter":
390
+ return "content_filter";
391
+ case null:
392
+ case undefined:
393
+ return "stop";
394
+ default:
395
+ return reason;
396
+ }
397
+ }
398
+
399
+ function mapUsage(raw: any, protocol: AgentProtocol): AgentUsage {
400
+ if (!raw) return { ...EMPTY_USAGE };
401
+ const input = protocol === "anthropic" ? raw.input_tokens : raw.prompt_tokens;
402
+ const output = protocol === "anthropic" ? raw.output_tokens : raw.completion_tokens;
403
+ const inputTokens = Number(input ?? 0);
404
+ const outputTokens = Number(output ?? 0);
405
+ return {
406
+ inputTokens,
407
+ outputTokens,
408
+ totalTokens: Number(raw.total_tokens ?? inputTokens + outputTokens),
409
+ requests: 1,
410
+ };
411
+ }
412
+
413
+ // ─── HTTP + SSE ──────────────────────────────────────────────────────────
414
+
415
+ function providerErrorMessage(body: string, status: number): string {
416
+ try {
417
+ const parsed = JSON.parse(body);
418
+ const message = parsed?.error?.message ?? parsed?.message ?? parsed?.error;
419
+ if (typeof message === "string" && message) return message;
420
+ } catch {
421
+ // not JSON — fall through to the raw body
422
+ }
423
+ return body.slice(0, 500) || `HTTP ${status}`;
424
+ }
425
+
426
+ /**
427
+ * Fetch with retries on network failures, 429 and 5xx.
428
+ * HTTP-specific (it inspects the status code), unlike the replication retry.
429
+ */
430
+ async function fetchWithRetry(
431
+ url: string,
432
+ init: RequestInit,
433
+ provider: AgentProvider,
434
+ protocol: AgentProtocol,
435
+ ): Promise<Response> {
436
+ const options = provider.options ?? {};
437
+ const retries = options.retries ?? DEFAULT_RETRIES;
438
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT;
439
+ const callerSignal = init.signal ?? undefined;
440
+ let lastError: any;
441
+
442
+ for (let attempt = 0; attempt <= retries; attempt++) {
443
+ if (callerSignal?.aborted) throw abortedError();
444
+ const signal = AbortSignal.any([AbortSignal.timeout(timeout), ...(callerSignal ? [callerSignal] : [])]);
445
+ try {
446
+ const response = await fetch(url, { ...init, signal });
447
+ if (response.status === 429 || response.status >= 500) {
448
+ const body = await response.text().catch(() => "");
449
+ lastError = new AppError(`Provider error (${response.status}): ${providerErrorMessage(body, response.status)}`, {
450
+ status: 502,
451
+ code: "AGENT_PROVIDER_ERROR",
452
+ reason: { provider: protocol, status: response.status },
453
+ });
454
+ } else {
455
+ return response;
456
+ }
457
+ } catch (err: any) {
458
+ if (callerSignal?.aborted) throw abortedError();
459
+ if (err instanceof AppError) lastError = err;
460
+ if (err?.name === "TimeoutError" || err?.name === "AbortError") {
461
+ throw new AppError(`Provider '${provider.model}' timed out after ${timeout}ms`, {
462
+ status: 504,
463
+ code: "AGENT_PROVIDER_TIMEOUT",
464
+ });
465
+ } else {
466
+ lastError = new AppError(`Provider request failed: ${err?.message ?? err}`, {
467
+ status: 502,
468
+ code: "AGENT_PROVIDER_ERROR",
469
+ });
470
+ }
471
+ }
472
+
473
+ if (attempt < retries) await Bun.sleep(250 * Math.pow(2, attempt));
474
+ }
475
+
476
+ throw lastError;
477
+ }
478
+
479
+ function abortedError(): AppError {
480
+ return new AppError("Agent run aborted", { status: 499, code: "AGENT_ABORTED" });
481
+ }
482
+
483
+ /** Read a `text/event-stream` body and yield the `data:` payloads. */
484
+ async function* sseData(body: ReadableStream<Uint8Array>): AsyncGenerator<string> {
485
+ const decoder = new TextDecoder();
486
+ const separator = /\r?\n\r?\n/;
487
+ let buffer = "";
488
+
489
+ for await (const chunk of body as any) {
490
+ buffer += decoder.decode(chunk as Uint8Array, { stream: true });
491
+ let match: RegExpMatchArray | null;
492
+ while ((match = separator.exec(buffer)) !== null) {
493
+ const start = match.index ?? 0;
494
+ const block = buffer.slice(0, start);
495
+ buffer = buffer.slice(start + match[0].length);
496
+ const data = block
497
+ .split(/\r?\n/)
498
+ .filter((line) => line.startsWith("data:"))
499
+ .map((line) => line.slice(5).trim())
500
+ .join("\n");
501
+ if (data) yield data;
502
+ }
503
+ }
504
+
505
+ const tail = buffer
506
+ .split(/\r?\n/)
507
+ .filter((line) => line.startsWith("data:"))
508
+ .map((line) => line.slice(5).trim())
509
+ .join("\n");
510
+ if (tail) yield tail;
511
+ }
512
+
513
+ /** An error event sent by the provider in the middle of a stream. */
514
+ function streamError(payload: any, protocol: AgentProtocol): AppError | null {
515
+ const message = payload?.error?.message ?? (typeof payload?.error === "string" ? payload.error : null);
516
+ if (!message) return null;
517
+ return new AppError(`Provider stream error: ${message}`, {
518
+ status: 502,
519
+ code: "AGENT_PROVIDER_ERROR",
520
+ reason: { provider: protocol },
521
+ });
522
+ }
523
+
524
+ // ─── OpenAI-compatible adapter ───────────────────────────────────────────
525
+
526
+ function openaiAdapter(provider: AgentProvider, protocol: AgentProtocol): ModelAdapter {
527
+ const baseUrl = resolveBaseUrl(provider, protocol);
528
+ const url = `${baseUrl}/chat/completions`;
529
+ const options = provider.options ?? {};
530
+
531
+ const headers = (): Record<string, string> => ({
532
+ "Content-Type": "application/json",
533
+ Authorization: `Bearer ${resolveApiKey(provider, protocol)}`,
534
+ ...(provider.headers ?? {}),
535
+ });
536
+
537
+ const body = (req: ModelChatRequest, stream: boolean, downgrade = false): any => {
538
+ const payload: any = {
539
+ model: provider.model,
540
+ messages: openaiMessages(req.system, req.messages),
541
+ max_tokens: req.maxTokens ?? options.maxTokens ?? DEFAULT_MAX_TOKENS,
542
+ };
543
+ if (req.tools?.length) {
544
+ payload.tools = openaiToolSpecs(req.tools);
545
+ payload.tool_choice = openaiToolChoice(req.toolChoice) ?? "auto";
546
+ }
547
+ const thinking = thinkingOf(req.thinking);
548
+ if (thinking) payload.reasoning_effort = thinking.effort;
549
+ const temperature = req.temperature ?? options.temperature;
550
+ if (temperature !== undefined) payload.temperature = temperature;
551
+ if (options.topP !== undefined) payload.top_p = options.topP;
552
+ // `json_schema` is the real constraint (the gateway validates against it);
553
+ // `json_object` only says "an object" and leaves the shape to the prompt
554
+ if (req.jsonSchema && !downgrade) {
555
+ payload.response_format = {
556
+ type: "json_schema",
557
+ json_schema: {
558
+ name: req.jsonSchema.name,
559
+ ...(req.jsonSchema.description ? { description: req.jsonSchema.description } : {}),
560
+ schema: req.jsonSchema.schema,
561
+ },
562
+ };
563
+ } else if (req.json) {
564
+ payload.response_format = { type: "json_object" };
565
+ }
566
+ if (stream) {
567
+ payload.stream = true;
568
+ payload.stream_options = { include_usage: true };
569
+ }
570
+ return payload;
571
+ };
572
+
573
+ /**
574
+ * POST the request, downgrading a rejected `json_schema` **once**. OpenAI-compatible
575
+ * gateways are of very uneven quality: one that refuses the field still answers
576
+ * `json_object`, and the schema is then only enforced by the prompt and the repair
577
+ * turn — better than failing the run.
578
+ */
579
+ const post = async (req: ModelChatRequest, stream: boolean): Promise<Response> => {
580
+ const send = (downgrade: boolean) => fetchWithRetry(url, {
581
+ method: "POST",
582
+ headers: headers(),
583
+ body: JSON.stringify(body(req, stream, downgrade)),
584
+ signal: req.signal,
585
+ }, provider, protocol);
586
+
587
+ const response = await send(false);
588
+ if (response.ok || !req.jsonSchema || response.status !== 400) return response;
589
+
590
+ const text = await response.text().catch(() => "");
591
+ // Not about the schema — hand the error back as it came
592
+ if (!SCHEMA_UNSUPPORTED.test(text)) {
593
+ return new Response(text, { status: response.status, headers: { "Content-Type": "application/json" } });
594
+ }
595
+
596
+ logger.file("warn", "agent: provider refused json_schema, retrying with json_object", {
597
+ model: provider.model,
598
+ provider: protocol,
599
+ });
600
+ return send(true);
601
+ };
602
+
603
+ return {
604
+ protocol,
605
+ model: provider.model,
606
+
607
+ async chat(req) {
608
+ const response = await post(req, false);
609
+
610
+ if (!response.ok) {
611
+ const text = await response.text().catch(() => "");
612
+ throw new AppError(`Provider error (${response.status}): ${providerErrorMessage(text, response.status)}`, {
613
+ status: 502,
614
+ code: "AGENT_PROVIDER_ERROR",
615
+ reason: { provider: protocol, status: response.status },
616
+ });
617
+ }
618
+
619
+ const raw: any = await response.json();
620
+ const choice = raw?.choices?.[0];
621
+ const toolCalls = (choice?.message?.tool_calls ?? []).map((call: any, index: number) => ({
622
+ id: call?.id ?? `call_${index}`,
623
+ name: call?.function?.name ?? "unknown",
624
+ argsText: call?.function?.arguments ?? "{}",
625
+ }));
626
+
627
+ return {
628
+ text: typeof choice?.message?.content === "string" ? choice.message.content : "",
629
+ // Reasoning models on OpenAI-compatible gateways (DeepSeek, Groq…) use
630
+ // `reasoning_content` — never mixed with the answer
631
+ ...(typeof choice?.message?.reasoning_content === "string" && choice.message.reasoning_content
632
+ ? { reasoning: choice.message.reasoning_content }
633
+ : {}),
634
+ toolCalls,
635
+ finishReason: normalizeFinishReason(choice?.finish_reason),
636
+ usage: mapUsage(raw?.usage, protocol),
637
+ raw,
638
+ };
639
+ },
640
+
641
+ async *chatStream(req) {
642
+ const response = await post(req, true);
643
+
644
+ if (!response.ok || !response.body) {
645
+ const text = await response.text().catch(() => "");
646
+ throw new AppError(`Provider error (${response.status}): ${providerErrorMessage(text, response.status)}`, {
647
+ status: 502,
648
+ code: "AGENT_PROVIDER_ERROR",
649
+ reason: { provider: protocol, status: response.status },
650
+ });
651
+ }
652
+
653
+ for await (const data of sseData(response.body)) {
654
+ if (data === "[DONE]") return;
655
+ let payload: any;
656
+ try {
657
+ payload = JSON.parse(data);
658
+ } catch {
659
+ continue;
660
+ }
661
+ const error = streamError(payload, protocol);
662
+ if (error) throw error;
663
+
664
+ const choice = payload?.choices?.[0];
665
+ const delta = choice?.delta;
666
+ if (typeof delta?.content === "string" && delta.content) {
667
+ yield { type: "text", text: delta.content };
668
+ }
669
+ if (typeof delta?.reasoning_content === "string" && delta.reasoning_content) {
670
+ yield { type: "reasoning", text: delta.reasoning_content };
671
+ }
672
+ for (const call of delta?.tool_calls ?? []) {
673
+ const index = call?.index ?? 0;
674
+ const argsText = call?.function?.arguments;
675
+ if (call?.id || call?.function?.name || argsText) {
676
+ yield {
677
+ type: "tool_call",
678
+ index,
679
+ ...(call?.id ? { id: call.id } : {}),
680
+ ...(call?.function?.name ? { name: call.function.name } : {}),
681
+ ...(argsText ? { argsText } : {}),
682
+ };
683
+ }
684
+ }
685
+ if (payload?.usage) yield { type: "usage", usage: mapUsage(payload.usage, protocol) };
686
+ if (choice?.finish_reason) yield { type: "finish", finishReason: normalizeFinishReason(choice.finish_reason) };
687
+ }
688
+ },
689
+ };
690
+ }
691
+
692
+ // ─── Anthropic adapter ───────────────────────────────────────────────────
693
+
694
+ function anthropicAdapter(provider: AgentProvider, protocol: AgentProtocol): ModelAdapter {
695
+ const baseUrl = resolveBaseUrl(provider, protocol);
696
+ const url = `${baseUrl}/v1/messages`;
697
+ const options = provider.options ?? {};
698
+
699
+ const headers = (): Record<string, string> => ({
700
+ "Content-Type": "application/json",
701
+ "x-api-key": resolveApiKey(provider, protocol),
702
+ "anthropic-version": "2023-06-01",
703
+ ...(provider.headers ?? {}),
704
+ });
705
+
706
+ /**
707
+ * Does this run ask for a schema, and can Anthropic honour it? A forced tool is
708
+ * incompatible with extended thinking (the API refuses `tool_choice` then) — the
709
+ * schema is still asked for in the prompt, and the repair turn still validates.
710
+ */
711
+ const wantsSchema = (req: ModelChatRequest): boolean => !!req.jsonSchema && !thinkingOf(req.thinking);
712
+
713
+ const body = (req: ModelChatRequest, stream: boolean): any => {
714
+ const converted = anthropicMessages(req.messages);
715
+ const system = [req.system, converted.system].filter(Boolean).join("\n\n");
716
+ const thinking = thinkingOf(req.thinking);
717
+ const maxTokens = req.maxTokens ?? options.maxTokens ?? DEFAULT_MAX_TOKENS;
718
+
719
+ const payload: any = {
720
+ model: provider.model,
721
+ messages: converted.messages,
722
+ // `max_tokens` is REQUIRED by the Messages API — and must stay **above**
723
+ // the thinking budget, so it is raised rather than refused
724
+ max_tokens: thinking && maxTokens <= thinking.budgetTokens
725
+ ? thinking.budgetTokens + 1_024
726
+ : maxTokens,
727
+ };
728
+ if (system) {
729
+ // A prompt-cache breakpoint: everything up to here (instructions, skills,
730
+ // working memory, tools) is billed once instead of on every run
731
+ payload.system = options.cache
732
+ ? [{ type: "text", text: system, cache_control: { type: "ephemeral" } }]
733
+ : system;
734
+ }
735
+
736
+ // The Messages API has no `response_format`: the schema is carried by a tool
737
+ // the model is **forced** to call, and the adapter turns that call into text
738
+ const schemaTool = wantsSchema(req)
739
+ ? [{ name: SCHEMA_TOOL, description: "Return the answer as a JSON object matching the schema.", input_schema: req.jsonSchema!.schema }]
740
+ : [];
741
+ const specs = [...(req.tools?.length ? anthropicToolSpecs(req.tools) : []), ...schemaTool];
742
+ if (specs.length) {
743
+ payload.tools = options.cache
744
+ ? [...specs.slice(0, -1), { ...specs[specs.length - 1]!, cache_control: { type: "ephemeral" } }]
745
+ : specs;
746
+ const toolChoice = schemaTool.length
747
+ ? { type: "tool", name: SCHEMA_TOOL }
748
+ : anthropicToolChoice(req.toolChoice);
749
+ if (toolChoice) payload.tool_choice = toolChoice;
750
+ }
751
+ if (thinking) payload.thinking = { type: "enabled", budget_tokens: thinking.budgetTokens };
752
+ const temperature = req.temperature ?? options.temperature;
753
+ // Extended thinking requires the default temperature (1)
754
+ if (temperature !== undefined && !thinking) payload.temperature = temperature;
755
+ if (options.topP !== undefined) payload.top_p = options.topP;
756
+ if (stream) payload.stream = true;
757
+ return payload;
758
+ };
759
+
760
+ return {
761
+ protocol,
762
+ model: provider.model,
763
+
764
+ async chat(req) {
765
+ const response = await fetchWithRetry(url, {
766
+ method: "POST",
767
+ headers: headers(),
768
+ body: JSON.stringify(body(req, false)),
769
+ signal: req.signal,
770
+ }, provider, protocol);
771
+
772
+ if (!response.ok) {
773
+ const text = await response.text().catch(() => "");
774
+ throw new AppError(`Provider error (${response.status}): ${providerErrorMessage(text, response.status)}`, {
775
+ status: 502,
776
+ code: "AGENT_PROVIDER_ERROR",
777
+ reason: { provider: protocol, status: response.status },
778
+ });
779
+ }
780
+
781
+ const raw: any = await response.json();
782
+ const blocks: any[] = Array.isArray(raw?.content) ? raw.content : [];
783
+ const calls = blocks
784
+ .filter((block) => block?.type === "tool_use")
785
+ .map((block: any, index: number) => ({
786
+ id: block?.id ?? `toolu_${index}`,
787
+ name: block?.name ?? "unknown",
788
+ argsText: JSON.stringify(block?.input ?? {}),
789
+ }));
790
+
791
+ // The forced schema tool **is** the answer: it becomes the text, so the
792
+ // runtime never sees a tool call it would try to execute
793
+ const forced = wantsSchema(req) ? calls.find((call) => call.name === SCHEMA_TOOL) : undefined;
794
+ const toolCalls = forced ? calls.filter((call) => call !== forced) : calls;
795
+
796
+ return {
797
+ text: forced
798
+ ? forced.argsText
799
+ : blocks.filter((block) => block?.type === "text").map((block) => block.text).join(""),
800
+ toolCalls,
801
+ // The raw blocks are kept: a tool round must echo the **signed** ones back
802
+ ...(blocks.some((block) => block?.type === "thinking" || block?.type === "redacted_thinking")
803
+ ? {
804
+ thinking: blocks.filter((block) =>
805
+ block?.type === "thinking" || block?.type === "redacted_thinking"),
806
+ reasoning: blocks
807
+ .filter((block) => block?.type === "thinking")
808
+ .map((block) => block.thinking)
809
+ .join(""),
810
+ }
811
+ : {}),
812
+ finishReason: normalizeFinishReason(raw?.stop_reason),
813
+ usage: mapUsage(raw?.usage, protocol),
814
+ raw,
815
+ };
816
+ },
817
+
818
+ async *chatStream(req) {
819
+ // Thinking blocks, accumulated by index — signatures included, so the run
820
+ // can echo them back if the model then asks for a tool
821
+ const thinking = new Map<number, any>();
822
+ // Index of the forced schema tool, whose deltas are the answer itself
823
+ let schemaBlock: number | null = null;
824
+
825
+ const response = await fetchWithRetry(url, {
826
+ method: "POST",
827
+ headers: headers(),
828
+ body: JSON.stringify(body(req, true)),
829
+ signal: req.signal,
830
+ }, provider, protocol);
831
+
832
+ if (!response.ok || !response.body) {
833
+ const text = await response.text().catch(() => "");
834
+ throw new AppError(`Provider error (${response.status}): ${providerErrorMessage(text, response.status)}`, {
835
+ status: 502,
836
+ code: "AGENT_PROVIDER_ERROR",
837
+ reason: { provider: protocol, status: response.status },
838
+ });
839
+ }
840
+
841
+ for await (const data of sseData(response.body)) {
842
+ let payload: any;
843
+ try {
844
+ payload = JSON.parse(data);
845
+ } catch {
846
+ continue;
847
+ }
848
+ const error = streamError(payload, protocol);
849
+ if (error) throw error;
850
+
851
+ switch (payload?.type) {
852
+ case "message_start": {
853
+ const usage = payload?.message?.usage;
854
+ // Only the keys actually reported: the agent merges the partials
855
+ // (input at the start, output at the end) instead of overwriting.
856
+ if (usage) yield { type: "usage", usage: { inputTokens: Number(usage.input_tokens ?? 0) } };
857
+ break;
858
+ }
859
+ case "content_block_start": {
860
+ const block = payload?.content_block;
861
+ const index = payload?.index ?? 0;
862
+ if (block?.type === "tool_use") {
863
+ // A forced schema call streams its JSON like any tool call — and
864
+ // is surfaced as text instead, see `chat()`
865
+ if (wantsSchema(req) && block.name === SCHEMA_TOOL) {
866
+ schemaBlock = index;
867
+ break;
868
+ }
869
+ yield {
870
+ type: "tool_call",
871
+ index,
872
+ ...(block.id ? { id: block.id } : {}),
873
+ ...(block.name ? { name: block.name } : {}),
874
+ };
875
+ } else if (block?.type === "thinking") {
876
+ // A reasoning block: text and signature arrive as deltas
877
+ thinking.set(index, { type: "thinking", thinking: "" });
878
+ } else if (block?.type === "redacted_thinking" && block.data) {
879
+ thinking.set(index, { type: "redacted_thinking", data: block.data });
880
+ }
881
+ break;
882
+ }
883
+ case "content_block_delta": {
884
+ const delta = payload?.delta;
885
+ const index = payload?.index ?? 0;
886
+ if (delta?.type === "text_delta" && delta.text) {
887
+ yield { type: "text", text: delta.text };
888
+ } else if (delta?.type === "thinking_delta" && delta.thinking) {
889
+ const block = thinking.get(index) ?? { type: "thinking", thinking: "" };
890
+ block.thinking = `${block.thinking ?? ""}${delta.thinking}`;
891
+ thinking.set(index, block);
892
+ yield { type: "reasoning", text: delta.thinking };
893
+ } else if (delta?.type === "signature_delta" && delta.signature) {
894
+ const block = thinking.get(index) ?? { type: "thinking", thinking: "" };
895
+ block.signature = delta.signature;
896
+ thinking.set(index, block);
897
+ } else if (delta?.type === "input_json_delta" && delta.partial_json) {
898
+ // The schema tool's JSON is the answer, token by token
899
+ if (index === schemaBlock) {
900
+ yield { type: "text", text: delta.partial_json };
901
+ break;
902
+ }
903
+ yield { type: "tool_call", index, argsText: delta.partial_json };
904
+ }
905
+ break;
906
+ }
907
+ case "message_delta": {
908
+ if (payload?.usage) {
909
+ yield { type: "usage", usage: { outputTokens: Number(payload.usage.output_tokens ?? 0) } };
910
+ }
911
+ if (payload?.delta?.stop_reason) {
912
+ yield { type: "finish", finishReason: normalizeFinishReason(payload.delta.stop_reason) };
913
+ }
914
+ break;
915
+ }
916
+ case "error":
917
+ throw new AppError(`Provider stream error: ${payload?.error?.message ?? "unknown"}`, {
918
+ status: 502,
919
+ code: "AGENT_PROVIDER_ERROR",
920
+ reason: { provider: protocol },
921
+ });
922
+ }
923
+ }
924
+
925
+ // The reasoning blocks, whole and signed — what a tool round must echo
926
+ if (thinking.size) {
927
+ yield {
928
+ type: "thinking_blocks",
929
+ blocks: [...thinking.entries()].sort((a, b) => a[0] - b[0]).map(([, block]) => block),
930
+ };
931
+ }
932
+ },
933
+ };
934
+ }
935
+
936
+ // ─── Entry point ─────────────────────────────────────────────────────────
937
+
938
+ /**
939
+ * Build the adapter for a provider — resolves the key, the base URL and the
940
+ * defaults once, so a run never re-reads the environment.
941
+ */
942
+ function resolveProvider(provider: AgentProvider): ModelAdapter {
943
+ if (!provider?.model) {
944
+ throw new AppError("`provider.model` is required", { status: 500, code: "AGENT_PROVIDER_INVALID" });
945
+ }
946
+ const protocol = resolveProtocol(provider);
947
+ return protocol === "anthropic"
948
+ ? anthropicAdapter(provider, protocol)
949
+ : openaiAdapter(provider, protocol);
950
+ }
951
+
952
+ export {
953
+ EMPTY_USAGE,
954
+ DEFAULT_MAX_TOKENS,
955
+ resolveProtocol,
956
+ /** @deprecated Renamed to `resolveProtocol`. */
957
+ resolveCompatible,
958
+ resolveApiKey,
959
+ resolveBaseUrl,
960
+ resolveProvider,
961
+ openaiContent,
962
+ openaiMessages,
963
+ anthropicContent,
964
+ anthropicMessages,
965
+ openaiToolSpecs,
966
+ anthropicToolSpecs,
967
+ normalizeFinishReason,
968
+ mapUsage,
969
+ sseData,
970
+ };