@odla-ai/ai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,290 @@
1
+ # @odla-ai/ai
2
+
3
+ One general-purpose interface for AI inference across **Anthropic (Claude)**,
4
+ **OpenAI (GPT)**, and **Google (Gemini)** — text, image, and audio — plus a
5
+ tool-use **agent engine** and an **eval harness**. It's the place every odla app
6
+ reaches for when it needs AI.
7
+
8
+ - **Library-only.** Import it in-process and bring your own keys. No hosted
9
+ gateway, no network hop.
10
+ - **One canonical shape.** Anthropic-flavored content blocks, forced tool calls
11
+ with object inputs, a normalized streaming event vocabulary, cache-aware usage,
12
+ and a closed error taxonomy — the same regardless of which provider answers.
13
+ - **Capability-checked.** Every request is validated against the model before
14
+ dispatch, so "audio to Claude" fails fast with a clear error instead of an
15
+ opaque provider 400.
16
+ - **Isomorphic.** Node 20+, Cloudflare Workers, the browser. Provider SDKs are
17
+ lazy-loaded — a Claude-only consumer never pulls in `openai` or `@google/genai`.
18
+
19
+ ```bash
20
+ npm install @odla-ai/ai
21
+ ```
22
+
23
+ ## Quick start
24
+
25
+ ```ts
26
+ import { init } from "@odla-ai/ai";
27
+
28
+ const ai = init({
29
+ keys: {
30
+ anthropic: process.env.ANTHROPIC_API_KEY,
31
+ openai: process.env.OPENAI_API_KEY,
32
+ google: process.env.GOOGLE_API_KEY,
33
+ },
34
+ defaultModel: "claude-opus-4-8",
35
+ });
36
+
37
+ // One call, any provider — pick the model, the provider is resolved for you.
38
+ const res = await ai.chat({
39
+ model: "gpt-5",
40
+ messages: [{ role: "user", content: "Say hello in one word." }],
41
+ maxTokens: 64,
42
+ });
43
+ console.log(res.content); // [{ type: "text", text: "Hello" }]
44
+ ```
45
+
46
+ ### Streaming
47
+
48
+ ```ts
49
+ for await (const ev of ai.stream({ model: "gemini-2.5-flash", messages: [...], maxTokens: 512 })) {
50
+ if (ev.type === "content_block_delta" && ev.delta.type === "text_delta") {
51
+ process.stdout.write(ev.delta.text);
52
+ }
53
+ }
54
+ ```
55
+
56
+ The event union (`message_start → content_block_start → content_block_delta →
57
+ content_block_stop → message_delta → message_stop`, plus `error`) is identical
58
+ across providers.
59
+
60
+ ### Structured extraction (forced tool call)
61
+
62
+ ```ts
63
+ const { value } = await ai.extract<{ sentiment: "positive" | "negative" | "neutral" }>({
64
+ model: "claude-haiku-4-5",
65
+ user: "Classify: 'I love this!'",
66
+ tool: {
67
+ name: "classify",
68
+ parameters: {
69
+ type: "object",
70
+ properties: { sentiment: { type: "string", enum: ["positive", "negative", "neutral"] } },
71
+ required: ["sentiment"],
72
+ },
73
+ },
74
+ });
75
+ // value.sentiment === "positive" (already a parsed object — never a JSON string)
76
+ ```
77
+
78
+ ### Web search
79
+
80
+ ```ts
81
+ const { text } = await ai.search({ model: "claude-sonnet-5", user: "Who won the 2026 World Cup?" });
82
+ ```
83
+
84
+ ## Multimodal: text, image, audio
85
+
86
+ Content blocks carry the modality. Image is broadly supported; **audio is
87
+ accepted by OpenAI and Google, but not Anthropic** — sending an `audio` block to
88
+ a Claude model throws a `CapabilityError` before any network call.
89
+
90
+ ```ts
91
+ // Image (works on Claude, GPT, Gemini)
92
+ await ai.chat({
93
+ model: "gpt-4o",
94
+ messages: [{ role: "user", content: [
95
+ { type: "image", source: { type: "base64", mediaType: "image/png", data: pngB64 } },
96
+ { type: "text", text: "What's in this image?" },
97
+ ] }],
98
+ maxTokens: 128,
99
+ });
100
+
101
+ // Audio (OpenAI: gpt-audio; Google: gemini-2.5-*)
102
+ await ai.chat({
103
+ model: "gpt-audio",
104
+ messages: [{ role: "user", content: [
105
+ { type: "audio", source: { type: "base64", mediaType: "audio/wav", data: wavB64 } },
106
+ { type: "text", text: "Transcribe this." },
107
+ ] }],
108
+ maxTokens: 256,
109
+ });
110
+ ```
111
+
112
+ ## Agents: build, run, and test
113
+
114
+ A **persona** is a role as config-as-data: a model, a system prompt, its tools,
115
+ and reasoning knobs. `runAgent` drives the tool-use loop (model turn → run tool
116
+ handlers → feed results back → repeat) until the model stops or a step limit hits.
117
+
118
+ ```ts
119
+ import { init, runAgent, type Persona, type ToolDef } from "@odla-ai/ai";
120
+
121
+ const ai = init({ keys: { anthropic: process.env.ANTHROPIC_API_KEY } });
122
+
123
+ const add: ToolDef = {
124
+ name: "add",
125
+ description: "Add two integers.",
126
+ inputSchema: { type: "object", properties: { a: { type: "number" }, b: { type: "number" } }, required: ["a", "b"] },
127
+ handler: (input) => ({ content: String((input.a as number) + (input.b as number)) }),
128
+ };
129
+
130
+ const calculator: Persona = {
131
+ name: "calculator",
132
+ model: "claude-opus-4-8",
133
+ system: "Use the add tool for arithmetic, then state the result.",
134
+ tools: [add],
135
+ maxSteps: 4,
136
+ };
137
+
138
+ const run = await runAgent(ai, calculator, { input: "What is 21 + 21?" });
139
+ console.log(run.finalText); // "…42"
140
+ console.log(run.toolCalls); // [{ toolUse, output }]
141
+ console.log(run.usage); // accumulated across turns
142
+ ```
143
+
144
+ Tools can declare taint constraints (`outputTaint` / `acceptsTaint`) for a
145
+ lightweight CaMeL-style prompt-injection gate, and personas can carry a pluggable
146
+ `MemoryScope`.
147
+
148
+ ### Skills
149
+
150
+ A **skill** is a named bundle of tools + instructions. Attach skills to a persona
151
+ and their tools are merged into the model's tool list and their instructions into
152
+ the system prompt — the primitive for "add a skill and call it as tools."
153
+
154
+ ```ts
155
+ import { entityCrudSkill, runAgent } from "@odla-ai/ai";
156
+ import { init as odlaInit } from "@odla-ai/db";
157
+
158
+ const db = odlaInit({ appId, adminToken: appKey, endpoint: ODLA_URL });
159
+
160
+ // Turn an odla-db entity into list/create/update/delete tools that change data.
161
+ const todos = entityCrudSkill({
162
+ db,
163
+ entity: "todos",
164
+ fields: { text: { type: "string" }, done: { type: "boolean" } },
165
+ required: ["text"],
166
+ });
167
+
168
+ const assistant = { name: "todo-bot", model: "claude-opus-4-8", skills: [todos] };
169
+ const run = await runAgent(ai, assistant, { input: "Add 'buy milk' and show my open todos." });
170
+ // the agent calls create_todo then list_todos, writing/reading via odla-db
171
+ ```
172
+
173
+ `entityCrudSkill` generates `list_<plural>`, `create_<singular>`, `update_<singular>`
174
+ (partial — pass only the fields that change, e.g. toggle `done`), and
175
+ `delete_<singular>` tools whose handlers call `db.transact`/`db.query`. Or hand-roll
176
+ a `Skill` = `{ name, instructions?, tools: ToolDef[] }` for anything else.
177
+
178
+ ### Evals
179
+
180
+ ```ts
181
+ import { evaluate, exactMatch, llmJudge } from "@odla-ai/ai";
182
+
183
+ const report = await evaluate({
184
+ inference: ai,
185
+ model: "claude-opus-4-8",
186
+ system: "Answer with only the number.",
187
+ grader: exactMatch(),
188
+ cases: [
189
+ { input: "What is 2+2?", expected: "4" },
190
+ { input: "What is 10-3?", expected: "7" },
191
+ ],
192
+ });
193
+ console.log(`${report.passed}/${report.total} passed`);
194
+ ```
195
+
196
+ Graders: `exactMatch`, `includes`, `structuredMatch`, and `llmJudge` (which
197
+ dogfoods the inference core to grade open-ended output against a rubric). Pass a
198
+ `persona` instead of a `model` to evaluate a full agent.
199
+
200
+ ## Keys & roles
201
+
202
+ Keys are **BYO** — a static per-provider map and/or a dynamic `resolveKey`
203
+ (multi-tenant / per-call). A provider with no resolvable key is simply
204
+ unavailable: requesting one of its models throws a `ConfigError`.
205
+
206
+ ```ts
207
+ const ai = init({
208
+ resolveKey: (provider) => lookupTenantKey(currentTenant, provider),
209
+ });
210
+ ```
211
+
212
+ "Roles" are personas (above) — config-as-data, not RBAC.
213
+
214
+ ## Storage, memory & BYO keys via odla-db
215
+
216
+ odla-ai follows odla-db's model: it's a self-contained npm package, ships an
217
+ `llms.txt` for machine context, and integrates with `@odla-ai/db` for durable
218
+ storage — **without taking a runtime dependency on it**. You install `@odla-ai/db`,
219
+ build a client, and inject it; odla-ai talks to it through a structural interface.
220
+
221
+ The agent handshake (an agent never takes a platform secret):
222
+
223
+ ```ts
224
+ import { requestToken, init as odlaInit } from "@odla-ai/db";
225
+ import { provisionAgentApp, odlaDbKeyResolver, OdlaDbMemory, persistRun, runAgent, init } from "@odla-ai/ai";
226
+
227
+ // 1. Device-authorization handshake — the human supplies the endpoint and
228
+ // approves a short code in odla-db Studio; you get a scoped, revocable token.
229
+ const { token } = await requestToken({ endpoint: ODLA_URL, onCode: ({ userCode }) => console.log(userCode) });
230
+
231
+ // 2. Provision: create app, mint an app key, push the agent schema, and store the
232
+ // user's BYO provider keys as odla-db secrets (encrypted at rest).
233
+ const { appId, appKey } = await provisionAgentApp({
234
+ endpoint: ODLA_URL,
235
+ token, // operator/dev token — needed to WRITE secrets
236
+ providerKeys: { openai: OPENAI_KEY, anthropic: ANTHROPIC_KEY },
237
+ });
238
+
239
+ // 3. Runtime: read keys from secrets with the app key, persist memory + run traces.
240
+ const db = odlaInit({ appId, adminToken: appKey, endpoint: ODLA_URL });
241
+ const ai = init({ resolveKey: odlaDbKeyResolver(db) }); // keys come from odla-db secrets
242
+
243
+ const persona = { name: "assistant", model: "gpt-5", memory: new OdlaDbMemory({ db, sessionId: "user-42" }) };
244
+ const run = await runAgent(ai, persona, { input: "Pick up where we left off." });
245
+ await persistRun(run, { db, sessionId: "user-42" });
246
+ ```
247
+
248
+ **BYO keys as secrets** — the write/read split mirrors odla-db: storing a secret
249
+ is a *privileged* action (operator or `odla_dev_` token, via `provisionAgentApp` /
250
+ `putSecret`); reading it at request time uses the ordinary **read-only** app key
251
+ (`odlaDbKeyResolver` → `db.secrets.get`). Keys are AES-GCM-encrypted at rest and
252
+ never returned to browser clients. `OdlaDbMemory` persists conversation turns and
253
+ `persistRun` writes run traces, both as natural-key upserts (idempotent on retry).
254
+
255
+ ## Models & capabilities
256
+
257
+ The catalog maps a canonical model id → provider, native id, and capabilities.
258
+ The built-in `DEFAULT_CATALOG` covers current Anthropic, OpenAI, and Google
259
+ models; extend or override it via `buildCatalog` / `init({ catalog })`.
260
+
261
+ | | image in | audio in | tools | thinking | effort | web search | structured |
262
+ |---|---|---|---|---|---|---|---|
263
+ | Claude (opus/sonnet/fable) | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
264
+ | Claude Haiku | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
265
+ | GPT-5 line | ✅ | ❌ | ✅ | — | ✅ | — | ✅ |
266
+ | GPT-4o | ✅ | ❌ | ✅ | — | ❌ | — | ✅ |
267
+ | gpt-audio | ❌ | ✅ | ✅ | — | ❌ | — | ✅ |
268
+ | Gemini 2.5 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
269
+
270
+ > **Model ids drift.** The catalog is data — verify ids against each provider's
271
+ > current docs (the `claude-api` skill for Anthropic; `/v1/models` for OpenAI;
272
+ > Google GenAI docs for Gemini) rather than trusting them from memory.
273
+
274
+ ## Errors
275
+
276
+ Every error is an `OdlaAIError` subclass carrying a stable `code` — branch on
277
+ `err.code`, not message text: `ConfigError`, `AuthError`, `RateLimitError`,
278
+ `CapabilityError`, `ContextWindowError`, `InvalidRequestError`, `ProviderError`.
279
+
280
+ ## Develop
281
+
282
+ ```bash
283
+ npm run typecheck # tsc --noEmit (strict)
284
+ npm test # vitest — offline, mock-provider based
285
+ npm run build # tsup → dual ESM + CJS + d.ts
286
+ npm run smoke # live check against real providers (needs .dev.vars)
287
+ ```
288
+
289
+ For the live smoke, copy `.dev.vars.example` → `.dev.vars` and fill in whatever
290
+ keys you have; providers without a key are skipped.
@@ -0,0 +1,219 @@
1
+ import {
2
+ addUsage,
3
+ emptyUsage,
4
+ mapProviderError,
5
+ normalizeError
6
+ } from "./chunk-LANGYP65.js";
7
+
8
+ // src/providers/anthropic.ts
9
+ import Anthropic from "@anthropic-ai/sdk";
10
+ var AnthropicProvider = class {
11
+ id = "anthropic";
12
+ client;
13
+ /** `client` may be injected (tests, custom transport); otherwise built from key. */
14
+ constructor(apiKey, client) {
15
+ this.client = client ?? new Anthropic({ apiKey });
16
+ }
17
+ async create(spec, req) {
18
+ try {
19
+ let messages = toAnthropicMessages(req);
20
+ const content = [];
21
+ const usage = emptyUsage();
22
+ let stopReason = "end_turn";
23
+ let id = "";
24
+ for (let i = 0; i < 8; i++) {
25
+ const res = await this.client.messages.create(
26
+ buildParams(spec, req, messages)
27
+ );
28
+ id = res.id;
29
+ addUsage(usage, mapUsage(res.usage));
30
+ content.push(...mapContent(res.content));
31
+ stopReason = mapStop(res.stop_reason);
32
+ if (res.stop_reason === "pause_turn") {
33
+ messages = [...messages, { role: "assistant", content: res.content }];
34
+ continue;
35
+ }
36
+ break;
37
+ }
38
+ return { id, model: req.model, provider: "anthropic", role: "assistant", content, stopReason, usage };
39
+ } catch (err) {
40
+ mapProviderError(err, "anthropic");
41
+ }
42
+ }
43
+ async *stream(spec, req) {
44
+ try {
45
+ const events = this.client.messages.stream(
46
+ buildParams(spec, req, toAnthropicMessages(req))
47
+ );
48
+ for await (const raw of events) {
49
+ const ev = mapStreamEvent(raw, req);
50
+ if (ev) yield ev;
51
+ }
52
+ } catch (err) {
53
+ yield { type: "error", error: normalizeError(err, "anthropic").toShape() };
54
+ }
55
+ }
56
+ };
57
+ function buildParams(spec, req, messages) {
58
+ const params = {
59
+ model: spec.nativeId,
60
+ max_tokens: req.maxTokens,
61
+ messages
62
+ };
63
+ const system = toAnthropicSystem(req.system);
64
+ if (system !== void 0) params.system = system;
65
+ const tools = buildTools(req);
66
+ if (tools) params.tools = tools;
67
+ const toolChoice = mapToolChoice(req.toolChoice);
68
+ if (toolChoice) params.tool_choice = toolChoice;
69
+ if (req.stopSequences && req.stopSequences.length > 0) params.stop_sequences = req.stopSequences;
70
+ if (req.temperature !== void 0 && !spec.capabilities.effort) params.temperature = req.temperature;
71
+ if (req.thinking === "adaptive" && spec.capabilities.thinking) params.thinking = { type: "adaptive" };
72
+ const outputConfig = {};
73
+ if (req.effort && spec.capabilities.effort) outputConfig.effort = req.effort;
74
+ if (req.responseFormat && spec.capabilities.structuredOutput) {
75
+ outputConfig.format = { type: "json_schema", schema: req.responseFormat.schema };
76
+ }
77
+ if (Object.keys(outputConfig).length > 0) params.output_config = outputConfig;
78
+ if (req.providerExtras) Object.assign(params, req.providerExtras);
79
+ return params;
80
+ }
81
+ function toAnthropicSystem(system) {
82
+ if (system === void 0) return void 0;
83
+ if (typeof system === "string") return system;
84
+ return system.map((b) => ({
85
+ type: "text",
86
+ text: b.text,
87
+ ...b.cacheControl ? { cache_control: { type: "ephemeral" } } : {}
88
+ }));
89
+ }
90
+ function toAnthropicMessages(req) {
91
+ return req.messages.map((m) => ({
92
+ role: m.role,
93
+ content: typeof m.content === "string" ? m.content : m.content.map(anthropicBlock)
94
+ }));
95
+ }
96
+ function anthropicBlock(b) {
97
+ switch (b.type) {
98
+ case "text":
99
+ return {
100
+ type: "text",
101
+ text: b.text,
102
+ ...b.cacheControl ? { cache_control: { type: "ephemeral" } } : {}
103
+ };
104
+ case "image":
105
+ return {
106
+ type: "image",
107
+ source: b.source.type === "base64" ? { type: "base64", media_type: b.source.mediaType, data: b.source.data } : { type: "url", url: b.source.url }
108
+ };
109
+ case "document":
110
+ return {
111
+ type: "document",
112
+ source: b.source.type === "base64" ? { type: "base64", media_type: "application/pdf", data: b.source.data } : { type: "url", url: b.source.url }
113
+ };
114
+ case "tool_use":
115
+ return { type: "tool_use", id: b.id, name: b.name, input: b.input };
116
+ case "tool_result":
117
+ return {
118
+ type: "tool_result",
119
+ tool_use_id: b.toolUseId,
120
+ content: typeof b.content === "string" ? b.content : b.content.map(anthropicBlock),
121
+ ...b.isError ? { is_error: true } : {}
122
+ };
123
+ case "thinking":
124
+ return { type: "thinking", thinking: b.thinking, signature: b.signature ?? "" };
125
+ case "audio":
126
+ throw new Error("Anthropic does not support audio input");
127
+ }
128
+ }
129
+ function buildTools(req) {
130
+ const tools = [];
131
+ for (const t of req.tools ?? []) {
132
+ tools.push({ name: t.name, description: t.description, input_schema: t.inputSchema });
133
+ }
134
+ if (req.webSearch) tools.push({ type: "web_search_20260209", name: "web_search" });
135
+ return tools.length > 0 ? tools : void 0;
136
+ }
137
+ function mapToolChoice(tc) {
138
+ if (tc === void 0) return void 0;
139
+ if (tc === "auto") return { type: "auto" };
140
+ if (tc === "any") return { type: "any" };
141
+ if (tc === "none") return { type: "none" };
142
+ return { type: "tool", name: tc.name };
143
+ }
144
+ function mapContent(blocks) {
145
+ const out = [];
146
+ for (const b of blocks) {
147
+ if (b.type === "text") out.push({ type: "text", text: b.text });
148
+ else if (b.type === "tool_use") {
149
+ out.push({ type: "tool_use", id: b.id, name: b.name, input: b.input ?? {} });
150
+ } else if (b.type === "thinking") {
151
+ out.push({ type: "thinking", thinking: b.thinking, signature: b.signature });
152
+ }
153
+ }
154
+ return out;
155
+ }
156
+ function mapUsage(u) {
157
+ const usage = { inputTokens: u.input_tokens ?? 0, outputTokens: u.output_tokens ?? 0 };
158
+ if (u.cache_creation_input_tokens != null) usage.cacheCreationTokens = u.cache_creation_input_tokens;
159
+ if (u.cache_read_input_tokens != null) usage.cacheReadTokens = u.cache_read_input_tokens;
160
+ return usage;
161
+ }
162
+ function mapStop(reason) {
163
+ switch (reason) {
164
+ case "end_turn":
165
+ case "max_tokens":
166
+ case "stop_sequence":
167
+ case "tool_use":
168
+ case "pause_turn":
169
+ case "refusal":
170
+ return reason;
171
+ default:
172
+ return "end_turn";
173
+ }
174
+ }
175
+ function mapStreamEvent(raw, req) {
176
+ switch (raw.type) {
177
+ case "message_start":
178
+ return {
179
+ type: "message_start",
180
+ message: {
181
+ id: raw.message.id,
182
+ model: req.model,
183
+ provider: "anthropic",
184
+ role: "assistant",
185
+ content: [],
186
+ usage: mapUsage(raw.message.usage)
187
+ }
188
+ };
189
+ case "content_block_start": {
190
+ const block = mapStartBlock(raw.content_block);
191
+ return block ? { type: "content_block_start", index: raw.index, contentBlock: block } : void 0;
192
+ }
193
+ case "content_block_delta": {
194
+ const d = raw.delta;
195
+ if (d.type === "text_delta") return { type: "content_block_delta", index: raw.index, delta: { type: "text_delta", text: d.text } };
196
+ if (d.type === "thinking_delta") return { type: "content_block_delta", index: raw.index, delta: { type: "thinking_delta", thinking: d.thinking } };
197
+ if (d.type === "input_json_delta") return { type: "content_block_delta", index: raw.index, delta: { type: "input_json_delta", partialJson: d.partial_json } };
198
+ return void 0;
199
+ }
200
+ case "content_block_stop":
201
+ return { type: "content_block_stop", index: raw.index };
202
+ case "message_delta":
203
+ return { type: "message_delta", delta: { stopReason: mapStop(raw.delta.stop_reason) }, usage: mapUsage(raw.usage) };
204
+ case "message_stop":
205
+ return { type: "message_stop" };
206
+ default:
207
+ return void 0;
208
+ }
209
+ }
210
+ function mapStartBlock(cb) {
211
+ if (cb.type === "text") return { type: "text", text: cb.text };
212
+ if (cb.type === "tool_use") return { type: "tool_use", id: cb.id, name: cb.name, input: {} };
213
+ if (cb.type === "thinking") return { type: "thinking", thinking: cb.thinking, signature: cb.signature };
214
+ return void 0;
215
+ }
216
+ export {
217
+ AnthropicProvider
218
+ };
219
+ //# sourceMappingURL=anthropic-JXDXR7O7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/providers/anthropic.ts"],"sourcesContent":["// Anthropic (Claude) adapter. The canonical shape is Anthropic-flavored, so this\n// is the lowest-translation adapter. Ported from odla-kg's claude.ts patterns:\n// forced tool-call via tool_choice, web_search_20260209 with pause_turn\n// continuation, adaptive thinking + output_config.effort (never budget_tokens).\n//\n// The installed @anthropic-ai/sdk (0.70.x) doesn't yet type adaptive thinking,\n// output_config, or web_search_20260209 — those newer request fields are built\n// into a loose params object and cast at the call site (the wire accepts them),\n// exactly as odla-kg does for the web-search tool literal.\n\nimport Anthropic from \"@anthropic-ai/sdk\";\nimport type { ModelSpec } from \"../shape/capabilities\";\nimport type { Provider } from \"./types\";\nimport { mapProviderError, normalizeError } from \"./errors\";\nimport {\n addUsage,\n emptyUsage,\n type OracleContentBlock,\n type OracleEvent,\n type OracleRequest,\n type OracleResponse,\n type OracleStopReason,\n type OracleUsage,\n type TextBlock,\n type ToolChoice,\n} from \"../shape/index\";\n\nexport class AnthropicProvider implements Provider {\n readonly id = \"anthropic\" as const;\n private client: Anthropic;\n\n /** `client` may be injected (tests, custom transport); otherwise built from key. */\n constructor(apiKey: string, client?: Anthropic) {\n this.client = client ?? new Anthropic({ apiKey });\n }\n\n async create(spec: ModelSpec, req: OracleRequest): Promise<OracleResponse> {\n try {\n let messages = toAnthropicMessages(req);\n const content: OracleContentBlock[] = [];\n const usage = emptyUsage();\n let stopReason: OracleStopReason = \"end_turn\";\n let id = \"\";\n\n // Loop only to resolve server-side tools (pause_turn), max 8 hops. Client\n // tool_use / end_turn / etc. return immediately.\n for (let i = 0; i < 8; i++) {\n const res = await this.client.messages.create(\n buildParams(spec, req, messages) as unknown as Anthropic.MessageCreateParamsNonStreaming,\n );\n id = res.id;\n addUsage(usage, mapUsage(res.usage));\n content.push(...mapContent(res.content));\n stopReason = mapStop(res.stop_reason);\n if (res.stop_reason === \"pause_turn\") {\n messages = [...messages, { role: \"assistant\", content: res.content }];\n continue;\n }\n break;\n }\n\n return { id, model: req.model, provider: \"anthropic\", role: \"assistant\", content, stopReason, usage };\n } catch (err) {\n mapProviderError(err, \"anthropic\");\n }\n }\n\n async *stream(spec: ModelSpec, req: OracleRequest): AsyncIterable<OracleEvent> {\n try {\n const events = this.client.messages.stream(\n buildParams(spec, req, toAnthropicMessages(req)) as unknown as Anthropic.MessageStreamParams,\n );\n for await (const raw of events) {\n const ev = mapStreamEvent(raw, req);\n if (ev) yield ev;\n }\n } catch (err) {\n yield { type: \"error\", error: normalizeError(err, \"anthropic\").toShape() };\n }\n }\n}\n\n// ----- request mapping -----\n\nfunction buildParams(\n spec: ModelSpec,\n req: OracleRequest,\n messages: Anthropic.MessageParam[],\n): Record<string, unknown> {\n const params: Record<string, unknown> = {\n model: spec.nativeId,\n max_tokens: req.maxTokens,\n messages,\n };\n\n const system = toAnthropicSystem(req.system);\n if (system !== undefined) params.system = system;\n\n const tools = buildTools(req);\n if (tools) params.tools = tools;\n\n const toolChoice = mapToolChoice(req.toolChoice);\n if (toolChoice) params.tool_choice = toolChoice;\n\n if (req.stopSequences && req.stopSequences.length > 0) params.stop_sequences = req.stopSequences;\n\n // Sampling params are rejected on the frontier models (they carry effort);\n // only the non-effort tier (Haiku) accepts temperature.\n if (req.temperature !== undefined && !spec.capabilities.effort) params.temperature = req.temperature;\n\n if (req.thinking === \"adaptive\" && spec.capabilities.thinking) params.thinking = { type: \"adaptive\" };\n\n const outputConfig: Record<string, unknown> = {};\n if (req.effort && spec.capabilities.effort) outputConfig.effort = req.effort;\n if (req.responseFormat && spec.capabilities.structuredOutput) {\n outputConfig.format = { type: \"json_schema\", schema: req.responseFormat.schema };\n }\n if (Object.keys(outputConfig).length > 0) params.output_config = outputConfig;\n\n if (req.providerExtras) Object.assign(params, req.providerExtras);\n return params;\n}\n\nfunction toAnthropicSystem(system: OracleRequest[\"system\"]): string | Anthropic.TextBlockParam[] | undefined {\n if (system === undefined) return undefined;\n if (typeof system === \"string\") return system;\n return system.map((b: TextBlock) => ({\n type: \"text\" as const,\n text: b.text,\n ...(b.cacheControl ? { cache_control: { type: \"ephemeral\" as const } } : {}),\n }));\n}\n\nfunction toAnthropicMessages(req: OracleRequest): Anthropic.MessageParam[] {\n return req.messages.map((m) => ({\n role: m.role,\n content: typeof m.content === \"string\" ? m.content : m.content.map(anthropicBlock),\n }));\n}\n\nfunction anthropicBlock(b: OracleContentBlock): Anthropic.ContentBlockParam {\n switch (b.type) {\n case \"text\":\n return {\n type: \"text\",\n text: b.text,\n ...(b.cacheControl ? { cache_control: { type: \"ephemeral\" } } : {}),\n };\n case \"image\":\n return {\n type: \"image\",\n source:\n b.source.type === \"base64\"\n ? { type: \"base64\", media_type: b.source.mediaType, data: b.source.data }\n : { type: \"url\", url: b.source.url },\n } as Anthropic.ContentBlockParam;\n case \"document\":\n return {\n type: \"document\",\n source:\n b.source.type === \"base64\"\n ? { type: \"base64\", media_type: \"application/pdf\", data: b.source.data }\n : { type: \"url\", url: b.source.url },\n } as Anthropic.ContentBlockParam;\n case \"tool_use\":\n return { type: \"tool_use\", id: b.id, name: b.name, input: b.input };\n case \"tool_result\":\n return {\n type: \"tool_result\",\n tool_use_id: b.toolUseId,\n content:\n typeof b.content === \"string\"\n ? b.content\n : (b.content.map(anthropicBlock) as Anthropic.ToolResultBlockParam[\"content\"]),\n ...(b.isError ? { is_error: true } : {}),\n };\n case \"thinking\":\n return { type: \"thinking\", thinking: b.thinking, signature: b.signature ?? \"\" };\n case \"audio\":\n // Guarded by validateRequest; defensive in case validation is bypassed.\n throw new Error(\"Anthropic does not support audio input\");\n }\n}\n\nfunction buildTools(req: OracleRequest): unknown[] | undefined {\n const tools: unknown[] = [];\n for (const t of req.tools ?? []) {\n tools.push({ name: t.name, description: t.description, input_schema: t.inputSchema });\n }\n if (req.webSearch) tools.push({ type: \"web_search_20260209\", name: \"web_search\" });\n return tools.length > 0 ? tools : undefined;\n}\n\nfunction mapToolChoice(tc: ToolChoice | undefined): unknown | undefined {\n if (tc === undefined) return undefined;\n if (tc === \"auto\") return { type: \"auto\" };\n if (tc === \"any\") return { type: \"any\" };\n if (tc === \"none\") return { type: \"none\" };\n return { type: \"tool\", name: tc.name };\n}\n\n// ----- response mapping -----\n\nfunction mapContent(blocks: Anthropic.ContentBlock[]): OracleContentBlock[] {\n const out: OracleContentBlock[] = [];\n for (const b of blocks) {\n if (b.type === \"text\") out.push({ type: \"text\", text: b.text });\n else if (b.type === \"tool_use\") {\n out.push({ type: \"tool_use\", id: b.id, name: b.name, input: (b.input ?? {}) as Record<string, unknown> });\n } else if (b.type === \"thinking\") {\n out.push({ type: \"thinking\", thinking: b.thinking, signature: b.signature });\n }\n // server_tool_use / web_search_tool_result blocks are dropped — the answer\n // text lives in text blocks.\n }\n return out;\n}\n\nfunction mapUsage(u: Anthropic.Usage): OracleUsage {\n const usage: OracleUsage = { inputTokens: u.input_tokens ?? 0, outputTokens: u.output_tokens ?? 0 };\n if (u.cache_creation_input_tokens != null) usage.cacheCreationTokens = u.cache_creation_input_tokens;\n if (u.cache_read_input_tokens != null) usage.cacheReadTokens = u.cache_read_input_tokens;\n return usage;\n}\n\nfunction mapStop(reason: string | null | undefined): OracleStopReason {\n switch (reason) {\n case \"end_turn\":\n case \"max_tokens\":\n case \"stop_sequence\":\n case \"tool_use\":\n case \"pause_turn\":\n case \"refusal\":\n return reason;\n default:\n return \"end_turn\";\n }\n}\n\n// ----- stream mapping -----\n\nfunction mapStreamEvent(raw: Anthropic.RawMessageStreamEvent, req: OracleRequest): OracleEvent | undefined {\n switch (raw.type) {\n case \"message_start\":\n return {\n type: \"message_start\",\n message: {\n id: raw.message.id,\n model: req.model,\n provider: \"anthropic\",\n role: \"assistant\",\n content: [],\n usage: mapUsage(raw.message.usage),\n },\n };\n case \"content_block_start\": {\n const block = mapStartBlock(raw.content_block);\n return block ? { type: \"content_block_start\", index: raw.index, contentBlock: block } : undefined;\n }\n case \"content_block_delta\": {\n const d = raw.delta;\n if (d.type === \"text_delta\") return { type: \"content_block_delta\", index: raw.index, delta: { type: \"text_delta\", text: d.text } };\n if (d.type === \"thinking_delta\") return { type: \"content_block_delta\", index: raw.index, delta: { type: \"thinking_delta\", thinking: d.thinking } };\n if (d.type === \"input_json_delta\") return { type: \"content_block_delta\", index: raw.index, delta: { type: \"input_json_delta\", partialJson: d.partial_json } };\n return undefined;\n }\n case \"content_block_stop\":\n return { type: \"content_block_stop\", index: raw.index };\n case \"message_delta\":\n return { type: \"message_delta\", delta: { stopReason: mapStop(raw.delta.stop_reason) }, usage: mapUsage(raw.usage as Anthropic.Usage) };\n case \"message_stop\":\n return { type: \"message_stop\" };\n default:\n return undefined;\n }\n}\n\nfunction mapStartBlock(cb: Anthropic.ContentBlock): OracleContentBlock | undefined {\n if (cb.type === \"text\") return { type: \"text\", text: cb.text };\n if (cb.type === \"tool_use\") return { type: \"tool_use\", id: cb.id, name: cb.name, input: {} };\n if (cb.type === \"thinking\") return { type: \"thinking\", thinking: cb.thinking, signature: cb.signature };\n return undefined;\n}\n"],"mappings":"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","names":[]}