agentic-assistant 0.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ Added
6
+
7
+ - `agentic-assistant/client`, a typed fetch-based client for the HTTP API: `health`, `tools`, `chat` (an async generator of events), `approvals`, `approve`, and `deny`. It has an injectable `fetch`, a base URL, and a typed `AssistantApiError`.
8
+ - A demo Worker entry (`dist/demo.js`, built from `src/demo.ts`), `wrangler.demo.jsonc`, and `npm run deploy:demo`. It serves a demo page, `/health`, and the `/api/v1` routes with bearer auth. With no model key, chat answers 503 `not_configured`.
9
+ - `createStaticTokenAuth`, an `agentic-identity` `Auth` for a fixed map of bearer tokens.
10
+ - Chat responses carry the header `x-ai-generated: true`.
11
+ - A small schema checker for hosts that refuse code generation, such as Workers. It fails closed on keywords it does not support.
12
+
13
+ Removed
14
+
15
+ - The default export of `agentic-assistant/worker` (a stub that answered `/health` only). Use `createWorker`.
16
+
17
+ Changed
18
+
19
+ - The package needs Node.js 22 or newer. `agentic-identity` needs Node 22, so `engines.node` is now `>=22`. It was `>=18`.
20
+ - The `agentic-identity` dependency is now `^0.4.0`. It was `^0.3.0`. The exported names did not change.
21
+
3
22
  ## 0.2.0
4
23
 
5
24
  Added
package/README.md CHANGED
@@ -7,8 +7,9 @@
7
7
 
8
8
  An AI assistant that acts inside your apps with the user's own permissions.
9
9
 
10
- > Status: early. The core loop, the tool sources, approvals, the React panel, the CLI, and the MCP server work.
11
- > There is no hosted HTTP API yet.
10
+ > Status: early. The core loop, the tool sources, approvals, the React panel, the CLI, the MCP server, and the hosted HTTP API work.
11
+
12
+ A demo Worker runs at https://agentic-assistant-demo.hello-fd1.workers.dev. `GET /` shows a demo page and `GET /health` answers 200. The `/api/v1` routes need a bearer key, so without one they answer 401. The demo has no model key set. A signed-in `POST /api/v1/chat` answers 503 with `not_configured`. The demo never fakes an answer.
12
13
 
13
14
  ## Install
14
15
 
@@ -16,7 +17,7 @@ An AI assistant that acts inside your apps with the user's own permissions.
16
17
  npm install agentic-assistant
17
18
  ```
18
19
 
19
- You need Node.js 18 or newer. The React panel needs `react` 18 or newer.
20
+ You need Node.js 22 or newer. The React panel needs `react` 18 or newer.
20
21
 
21
22
  To build from source:
22
23
 
@@ -173,7 +174,9 @@ export default {
173
174
  };
174
175
  ```
175
176
 
176
- Every route is under `/api/v1` and needs `Authorization: Bearer <key>`. The key resolves to an `agentic-identity` principal. A person, an agent, and a token that acts for a user all work. A missing, bad, or odd credential is a 401.
177
+ To deploy the demo Worker yourself, set the secrets `ASSISTANT_TOKENS` (a JSON map of a bearer token of 32 bytes or more to `{"id":"ana","scopes":["demo:read"]}`) and `MODEL_KEY`, set the var `MODEL_URL` (or `MODEL_USE_DEFAULT_ENDPOINT=1` to call the US-hosted `api.anthropic.com`), then run `npm run deploy:demo`. Without `MODEL_KEY`, chat answers 503. `agentic-assistant/worker` also exports `createStaticTokenAuth`, an `agentic-identity` `Auth` that reads bearer tokens from a fixed map.
178
+
179
+ Every route is under `/api/v1` and needs `Authorization: Bearer <key>`. The key resolves to an `agentic-identity` principal. A person, an agent, and a token that acts for a user all work. A missing, bad, or odd credential is a 401. Every chat response has the header `x-ai-generated: true`.
177
180
 
178
181
  | Route | What it does |
179
182
  |---|---|
@@ -185,6 +188,32 @@ Every route is under `/api/v1` and needs `Authorization: Bearer <key>`. The key
185
188
 
186
189
  The assistant holds the caller's scopes and no more. A chat turn waits at a risky call until another principal decides. The stream stays open for that time. A principal that asked for the turn never decides on its own proposal: an agent gets a 403 for both approve and deny. The same holds for an agent it made and for the agent that made it. A person who asks may decide, because the assistant proposed the call. Another agent may decide only when it holds `assistant:admin` and the scope of the tool. A principal with another owner sees a 404. The list hides a call whose tool the caller has no scope for. If a client hangs up, the run stops and its pending calls go. If the key is revoked or its scopes change during a run, the run stops before its next tool call.
187
190
 
191
+ ## Use the HTTP client
192
+
193
+ `agentic-assistant/client` is a typed client for the hosted API routes. It uses only `fetch`, so it runs in Workers and Node. In a browser it works on the same origin only, because the API sends no CORS headers.
194
+
195
+ ```ts
196
+ import { createClient, AssistantApiError } from "agentic-assistant/client";
197
+
198
+ const assistant = createClient({ baseUrl: "https://assistant.example.com", token });
199
+ const tools = await assistant.tools();
200
+ for await (const event of assistant.chat("Delete note n1")) {
201
+ if (event.type === "text_delta") process.stdout.write(event.text);
202
+ }
203
+ try {
204
+ const [pending] = await assistant.approvals();
205
+ if (pending?.canDecide) await assistant.approve(pending.id, pending.digest);
206
+ } catch (error) {
207
+ if (error instanceof AssistantApiError) console.error(error.code, error.status);
208
+ }
209
+ ```
210
+
211
+ - The methods match the routes: `health`, `tools`, `chat`, `approvals`, `approve`, `deny`. Each takes an optional `{ signal }` as its last argument.
212
+ - `chat` is an async generator of the same events as `runAssistant`, ending with `done`. It sends the request on the first read. A run that fails on the server ends with an `error` event. A stream that ends with no `done` or `error` throws `UNKNOWN`: the run did not finish. Stop reading to stop the run. The stream stays open while a risky call waits, so decide from another call.
213
+ - `baseUrl` is the Worker URL: `https`, or `http` on `localhost`, `127.0.0.1`, or `[::1]`, with no query, hash, or credentials. A path prefix is for a host that strips it before the Worker. Pass `fetch` to use your own. `health` sends no token.
214
+ - A failed call throws `AssistantApiError`. `code` is `INVALID_INPUT`, `UNAUTHENTICATED`, `FORBIDDEN`, `NOT_FOUND`, `METHOD_NOT_ALLOWED`, `CONFLICT`, `RATE_LIMITED`, `NOT_CONFIGURED`, `INTERNAL`, or `UNKNOWN`. `status` is the HTTP status. `retryAfter` is set for a 429. A `200` reply with the wrong shape fails as `UNKNOWN`.
215
+ - The client never follows a redirect and never retries. A decision that you abort after the client sends it can still apply: read `approvals()` before you try again.
216
+
188
217
  ## AI transparency
189
218
 
190
219
  The package marks what the assistant makes as AI-generated. This follows the idea of EU AI Act Article 50.
@@ -232,7 +261,8 @@ The record says who made the content. It does not say the content is true.
232
261
  ## Limits
233
262
 
234
263
  - The hosted API keeps pending approvals in the memory of one Worker instance. A decision must reach the instance that holds the chat stream. Run it on one instance or behind sticky routing until a shared queue exists. It has no per-IP limit, only a cap of four chat turns at once per principal.
235
- - The tests run the API under Node with a fake model. They do not run it under `wrangler dev`. The sample Worker in `wrangler.jsonc` answers `/health` only.
264
+ - The tests run the API under Node with a fake model. They do not run it under `wrangler dev`. The demo Worker was checked by hand under `wrangler dev` and on workers.dev.
265
+ - Workers refuse code generation, so there a tool schema is checked by a small built-in checker, not by Ajv. It supports type, properties, required, additionalProperties, enum, const, items, length and size bounds, and anyOf, oneOf, allOf. It accepts annotation keywords such as `description` and `format` but does not check `format`, as the Ajv setup does not. It refuses a tool whose schema has any other keyword, such as `$ref`.
236
266
  - The CLI reads one message, or a short conversation on a terminal. It does not keep a conversation between runs.
237
267
  - The panel is tested in a simulated browser (jsdom). Contrast is checked from the CSS values. Run a screen reader and a real browser before you ship it to people who need it.
238
268
  - The panel shows plain text, not Markdown.
@@ -1,4 +1,4 @@
1
- import { S as Scope, u as ToolRisk, s as ToolDefinition, R as RunLimits, t as ToolRegistry, d as ApprovalGate, h as AssistantEvent } from './loop-42giKLVa.js';
1
+ import { S as Scope, u as ToolRisk, s as ToolDefinition, o as RunLimits, t as ToolRegistry, g as ApprovalGate, A as AssistantEvent } from './loop-D4qWbRu6.js';
2
2
  import { P as Pricing, M as ModelProvider, c as Message } from './types-DGH8FS56.js';
3
3
 
4
4
  type McpTransportConfig = {
@@ -1,6 +1,6 @@
1
1
  // src/version.ts
2
2
  import { createRequire } from "module";
3
- var VERSION = true ? "0.2.0" : readPackageVersion();
3
+ var VERSION = true ? "0.3.0" : readPackageVersion();
4
4
 
5
5
  // src/approvals.ts
6
6
  import { createHash, randomUUID } from "crypto";
@@ -216,6 +216,166 @@ function cutText(text, max) {
216
216
 
217
217
  // src/tools.ts
218
218
  import Ajv2020Module from "ajv/dist/2020.js";
219
+
220
+ // src/schema-fallback.ts
221
+ var ANNOTATIONS = /* @__PURE__ */ new Set(["$schema", "$id", "$comment", "title", "description", "default", "examples", "deprecated", "readOnly", "writeOnly", "format"]);
222
+ var KEYWORDS = /* @__PURE__ */ new Set([
223
+ "type",
224
+ "properties",
225
+ "required",
226
+ "additionalProperties",
227
+ "enum",
228
+ "const",
229
+ "items",
230
+ "minItems",
231
+ "maxItems",
232
+ "minLength",
233
+ "maxLength",
234
+ "minimum",
235
+ "maximum",
236
+ "exclusiveMinimum",
237
+ "exclusiveMaximum",
238
+ "anyOf",
239
+ "oneOf",
240
+ "allOf",
241
+ "uniqueItems"
242
+ ]);
243
+ var MAX_DEPTH = 64;
244
+ var TYPES = ["string", "number", "integer", "boolean", "null", "object", "array"];
245
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
246
+ function typeOf(value) {
247
+ if (value === null) return "null";
248
+ if (Array.isArray(value)) return "array";
249
+ return typeof value;
250
+ }
251
+ function isType(value, type) {
252
+ if (type === "integer") return typeof value === "number" && Number.isInteger(value);
253
+ if (type === "number") return typeof value === "number" && Number.isFinite(value);
254
+ return typeOf(value) === type;
255
+ }
256
+ function canonical(value) {
257
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
258
+ if (isObject(value)) {
259
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
260
+ }
261
+ return JSON.stringify(value);
262
+ }
263
+ var same = (a, b) => canonical(a) === canonical(b);
264
+ function assertSupported(schema, depth = 0) {
265
+ if (typeof schema === "boolean") return;
266
+ if (!isObject(schema)) throw new Error("a schema must be an object");
267
+ if (depth > MAX_DEPTH) throw new Error("the schema is nested too deeply");
268
+ for (const [key, value] of Object.entries(schema)) {
269
+ if (ANNOTATIONS.has(key)) continue;
270
+ if (!KEYWORDS.has(key)) throw new Error(`the keyword "${key}" is not supported without code generation`);
271
+ if (key === "type") {
272
+ const types = Array.isArray(value) ? value : [value];
273
+ if (types.some((t) => typeof t !== "string" || !TYPES.includes(t))) throw new Error("a bad type");
274
+ } else if (key === "properties") {
275
+ if (!isObject(value)) throw new Error("properties must be an object");
276
+ for (const child of Object.values(value)) assertSupported(child, depth + 1);
277
+ } else if (key === "items" || key === "additionalProperties") assertSupported(value, depth + 1);
278
+ else if (key === "anyOf" || key === "oneOf" || key === "allOf") {
279
+ if (!Array.isArray(value)) throw new Error(`${key} must be a list`);
280
+ for (const child of value) assertSupported(child, depth + 1);
281
+ } else if (key === "required") {
282
+ if (!Array.isArray(value) || value.some((name) => typeof name !== "string")) throw new Error("required must be a list of names");
283
+ } else if (key === "enum") {
284
+ if (!Array.isArray(value)) throw new Error("enum must be a list");
285
+ } else if (key === "uniqueItems") {
286
+ if (typeof value !== "boolean") throw new Error("uniqueItems must be true or false");
287
+ } else if (key !== "const" && typeof value !== "number") throw new Error(`${key} must be a number`);
288
+ }
289
+ }
290
+ function check(schema, value, path, depth) {
291
+ if (depth > MAX_DEPTH) throw new Error("too deep");
292
+ if (schema === true) return void 0;
293
+ if (schema === false) return { instancePath: path, message: "must not be present", params: {} };
294
+ const s = schema;
295
+ const fail = (message, params = {}) => ({ instancePath: path, message, params });
296
+ if (s.type !== void 0) {
297
+ const types = Array.isArray(s.type) ? s.type : [s.type];
298
+ if (!types.some((t) => isType(value, t))) return fail(`must be ${types.join(" or ")}`);
299
+ }
300
+ if ("const" in s && !same(s.const, value)) return fail("must be equal to constant");
301
+ if (Array.isArray(s.enum) && !s.enum.some((option) => same(option, value))) return fail("must be equal to one of the allowed values");
302
+ if (typeof value === "string") {
303
+ if (typeof s.minLength === "number" && [...value].length < s.minLength) return fail(`must NOT have fewer than ${s.minLength} characters`);
304
+ if (typeof s.maxLength === "number" && [...value].length > s.maxLength) return fail(`must NOT have more than ${s.maxLength} characters`);
305
+ }
306
+ if (typeof value === "number") {
307
+ if (typeof s.minimum === "number" && value < s.minimum) return fail(`must be >= ${s.minimum}`);
308
+ if (typeof s.maximum === "number" && value > s.maximum) return fail(`must be <= ${s.maximum}`);
309
+ if (typeof s.exclusiveMinimum === "number" && value <= s.exclusiveMinimum) return fail(`must be > ${s.exclusiveMinimum}`);
310
+ if (typeof s.exclusiveMaximum === "number" && value >= s.exclusiveMaximum) return fail(`must be < ${s.exclusiveMaximum}`);
311
+ }
312
+ if (Array.isArray(value)) {
313
+ if (typeof s.minItems === "number" && value.length < s.minItems) return fail(`must NOT have fewer than ${s.minItems} items`);
314
+ if (typeof s.maxItems === "number" && value.length > s.maxItems) return fail(`must NOT have more than ${s.maxItems} items`);
315
+ if (s.uniqueItems === true) {
316
+ const seen = /* @__PURE__ */ new Set();
317
+ for (const item of value) {
318
+ const text = canonical(item);
319
+ if (seen.has(text)) return fail("must NOT have duplicate items");
320
+ seen.add(text);
321
+ }
322
+ }
323
+ if (s.items !== void 0) {
324
+ for (let i = 0; i < value.length; i++) {
325
+ const error = check(s.items, value[i], `${path}/${i}`, depth + 1);
326
+ if (error) return error;
327
+ }
328
+ }
329
+ }
330
+ if (isObject(value)) {
331
+ for (const name of s.required ?? []) {
332
+ if (!Object.hasOwn(value, name)) return fail(`must have required property '${name}'`, { missingProperty: name });
333
+ }
334
+ const properties = isObject(s.properties) ? s.properties : {};
335
+ for (const [name, child] of Object.entries(value)) {
336
+ const key = `${path}/${name.replace(/~/g, "~0").replace(/\//g, "~1")}`;
337
+ if (Object.hasOwn(properties, name)) {
338
+ const error = check(properties[name], child, key, depth + 1);
339
+ if (error) return error;
340
+ } else if (s.additionalProperties === false) {
341
+ return fail("must NOT have additional properties", { additionalProperty: name });
342
+ } else if (s.additionalProperties !== void 0) {
343
+ const error = check(s.additionalProperties, child, key, depth + 1);
344
+ if (error) return error;
345
+ }
346
+ }
347
+ }
348
+ if (Array.isArray(s.allOf)) {
349
+ for (const child of s.allOf) {
350
+ const error = check(child, value, path, depth + 1);
351
+ if (error) return error;
352
+ }
353
+ }
354
+ if (Array.isArray(s.anyOf) && !s.anyOf.some((child) => check(child, value, path, depth + 1) === void 0)) return fail("must match a schema in anyOf");
355
+ if (Array.isArray(s.oneOf) && s.oneOf.filter((child) => check(child, value, path, depth + 1) === void 0).length !== 1) {
356
+ return fail("must match exactly one schema in oneOf");
357
+ }
358
+ return void 0;
359
+ }
360
+ function compileFallback(schema) {
361
+ assertSupported(schema);
362
+ const validate2 = (input) => {
363
+ const error = check(schema, input, "", 0);
364
+ validate2.errors = error ? [error] : null;
365
+ return error === void 0;
366
+ };
367
+ return validate2;
368
+ }
369
+ function canGenerateCode() {
370
+ try {
371
+ new Function("return 1")();
372
+ return true;
373
+ } catch {
374
+ return false;
375
+ }
376
+ }
377
+
378
+ // src/tools.ts
219
379
  function defineTool(tool) {
220
380
  return tool;
221
381
  }
@@ -233,6 +393,18 @@ var ToolRegistry = class {
233
393
  tools = /* @__PURE__ */ new Map();
234
394
  // One validator per registry, so an `$id` in one tool's schema cannot clash with another registry's.
235
395
  ajv = new Ajv2020({ strict: false, logger: false, allErrors: false, coerceTypes: false, useDefaults: false, removeAdditional: false });
396
+ /**
397
+ * Ajv builds validators with `new Function`. Workers refuse that inside a request, though not while a module loads.
398
+ * So try Ajv first, and use the small checker when this host refuses code generation. A bad schema still throws.
399
+ */
400
+ compile(schema) {
401
+ try {
402
+ return this.ajv.compile(schema);
403
+ } catch (error) {
404
+ if (canGenerateCode()) throw error;
405
+ return compileFallback(schema);
406
+ }
407
+ }
236
408
  register(tool) {
237
409
  if (!NAME_PATTERN.test(tool.name)) throw new Error(`invalid tool name "${tool.name}"`);
238
410
  if (this.tools.has(tool.name)) throw new Error(`tool "${tool.name}" is already registered`);
@@ -245,7 +417,7 @@ var ToolRegistry = class {
245
417
  const { $schema: _draft, $id: _id, ...schema } = tool.inputSchema;
246
418
  let validate2;
247
419
  try {
248
- validate2 = this.ajv.compile(schema);
420
+ validate2 = this.compile(schema);
249
421
  } catch (error) {
250
422
  throw new Error(`tool "${tool.name}" has an invalid input schema: ${error instanceof Error ? error.message : String(error)}`);
251
423
  }
@@ -1,6 +1,11 @@
1
1
  import {
2
2
  ScriptedProvider
3
3
  } from "./chunk-HYT732TN.js";
4
+ import {
5
+ AnthropicProvider,
6
+ checkBaseUrl,
7
+ isLoopback
8
+ } from "./chunk-DXTIEMOT.js";
4
9
  import {
5
10
  PrincipalError,
6
11
  ProviderError,
@@ -12,37 +17,12 @@ import {
12
17
  digestCall,
13
18
  printable,
14
19
  runAssistant
15
- } from "./chunk-T4ZMDOB4.js";
20
+ } from "./chunk-3LKTJJ75.js";
16
21
 
17
22
  // src/sources/mcp.ts
18
23
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
19
24
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
20
25
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
21
-
22
- // src/providers/url.ts
23
- function isLoopback(url) {
24
- return ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
25
- }
26
- function checkBaseUrl(baseUrl) {
27
- let url;
28
- try {
29
- url = new URL(baseUrl);
30
- } catch {
31
- throw new ProviderError("baseUrl is not a valid URL");
32
- }
33
- if (url.username !== "" || url.password !== "") {
34
- throw new ProviderError("baseUrl must not carry credentials; pass apiKey instead");
35
- }
36
- if (url.search !== "" || url.hash !== "") {
37
- throw new ProviderError("baseUrl must not carry a query string or a fragment");
38
- }
39
- if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback(url))) {
40
- throw new ProviderError("baseUrl must use https, except for localhost");
41
- }
42
- return { url, text: baseUrl.replace(/\/+$/, "") };
43
- }
44
-
45
- // src/sources/mcp.ts
46
26
  var MAX_PAGES = 20;
47
27
  var MAX_SCHEMA_CHARS = 2e4;
48
28
  function positive(name, value, fallback, max = Number.MAX_SAFE_INTEGER) {
@@ -180,112 +160,6 @@ async function connectMcpTools(options) {
180
160
  }
181
161
  }
182
162
 
183
- // src/providers/anthropic.ts
184
- import Anthropic from "@anthropic-ai/sdk";
185
- var DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-5-5";
186
- var PRICING_CHECKED_ON = "2026-10-29";
187
- var KNOWN_PRICING = Object.freeze({
188
- "claude-sonnet-5-5": Object.freeze({ inputPerMTok: 2, outputPerMTok: 10 }),
189
- "claude-opus-5-5": Object.freeze({ inputPerMTok: 4, outputPerMTok: 20 })
190
- });
191
- var STOP_REASONS = {
192
- end_turn: "end_turn",
193
- stop_sequence: "end_turn",
194
- tool_use: "tool_use",
195
- max_tokens: "max_tokens",
196
- refusal: "refusal"
197
- };
198
- function toAnthropicMessages(messages) {
199
- return messages.map((message) => ({
200
- role: message.role,
201
- content: message.content.flatMap((block) => {
202
- switch (block.type) {
203
- case "text":
204
- return block.text.length > 0 ? [{ type: "text", text: block.text }] : [];
205
- case "tool_use":
206
- return [{ type: "tool_use", id: block.id, name: block.name, input: block.input }];
207
- case "tool_result":
208
- return [{ type: "tool_result", tool_use_id: block.toolUseId, content: block.content, is_error: block.isError ?? false }];
209
- case "provider_block":
210
- return block.provider === "anthropic" ? [block.raw] : [];
211
- }
212
- })
213
- }));
214
- }
215
- function fromAnthropicContent(content) {
216
- return content.map((block) => {
217
- if (block.type === "text") return { type: "text", text: block.text };
218
- if (block.type === "tool_use") return { type: "tool_use", id: block.id, name: block.name, input: block.input };
219
- return { type: "provider_block", provider: "anthropic", raw: block };
220
- });
221
- }
222
- var AnthropicProvider = class {
223
- name = "anthropic";
224
- model;
225
- pricing;
226
- client;
227
- effort;
228
- constructor(options = {}) {
229
- this.model = options.model ?? DEFAULT_ANTHROPIC_MODEL;
230
- this.pricing = options.pricing ?? KNOWN_PRICING[this.model];
231
- this.effort = options.effort;
232
- if (options.client !== void 0) {
233
- this.client = options.client;
234
- return;
235
- }
236
- if (options.baseUrl === void 0 && options.useDefaultEndpoint !== true) {
237
- throw new ProviderError("set baseUrl, or set useDefaultEndpoint: true to call api.anthropic.com, a US-hosted endpoint");
238
- }
239
- const apiKey = options.apiKey ?? process.env.ANTHROPIC_API_KEY;
240
- if (typeof apiKey !== "string" || apiKey.length === 0) {
241
- throw new ProviderError("apiKey is required: pass it, or set ANTHROPIC_API_KEY");
242
- }
243
- this.client = new Anthropic({
244
- apiKey,
245
- // Always set, so ANTHROPIC_BASE_URL in the environment cannot redirect the key.
246
- baseURL: options.baseUrl !== void 0 ? checkBaseUrl(options.baseUrl).text : "https://api.anthropic.com"
247
- });
248
- }
249
- async *stream(request) {
250
- const stream = this.client.messages.stream(
251
- {
252
- model: this.model,
253
- max_tokens: request.maxOutputTokens,
254
- ...request.system !== void 0 ? { system: request.system } : {},
255
- messages: toAnthropicMessages(request.messages),
256
- ...request.tools.length > 0 ? {
257
- tools: request.tools.map((tool) => ({
258
- name: tool.name,
259
- description: tool.description,
260
- input_schema: tool.inputSchema
261
- }))
262
- } : {},
263
- ...this.effort !== void 0 ? { output_config: { effort: this.effort } } : {}
264
- },
265
- { signal: request.signal }
266
- );
267
- for await (const event of stream) {
268
- if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
269
- yield { type: "text_delta", text: event.delta.text };
270
- }
271
- }
272
- const message = await stream.finalMessage();
273
- const usage = message.usage;
274
- if (typeof usage?.input_tokens !== "number" || typeof usage.output_tokens !== "number") {
275
- throw new ProviderError("anthropic response carried no token usage");
276
- }
277
- yield {
278
- type: "message",
279
- content: fromAnthropicContent(message.content),
280
- stopReason: STOP_REASONS[message.stop_reason ?? ""] ?? "other",
281
- usage: {
282
- inputTokens: usage.input_tokens + (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0),
283
- outputTokens: usage.output_tokens
284
- }
285
- };
286
- }
287
- };
288
-
289
163
  // src/providers/openai-compatible.ts
290
164
  import { randomUUID } from "crypto";
291
165
  var FINISH_REASONS = {
@@ -685,9 +559,6 @@ function readTextFile(path) {
685
559
 
686
560
  export {
687
561
  connectMcpTools,
688
- DEFAULT_ANTHROPIC_MODEL,
689
- PRICING_CHECKED_ON,
690
- AnthropicProvider,
691
562
  OpenAICompatibleProvider,
692
563
  FallbackProvider,
693
564
  ConfigError,
@@ -0,0 +1,142 @@
1
+ import {
2
+ ProviderError
3
+ } from "./chunk-3LKTJJ75.js";
4
+
5
+ // src/providers/anthropic.ts
6
+ import Anthropic from "@anthropic-ai/sdk";
7
+
8
+ // src/providers/url.ts
9
+ function isLoopback(url) {
10
+ return ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
11
+ }
12
+ function checkBaseUrl(baseUrl) {
13
+ let url;
14
+ try {
15
+ url = new URL(baseUrl);
16
+ } catch {
17
+ throw new ProviderError("baseUrl is not a valid URL");
18
+ }
19
+ if (url.username !== "" || url.password !== "") {
20
+ throw new ProviderError("baseUrl must not carry credentials; pass apiKey instead");
21
+ }
22
+ if (url.search !== "" || url.hash !== "") {
23
+ throw new ProviderError("baseUrl must not carry a query string or a fragment");
24
+ }
25
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback(url))) {
26
+ throw new ProviderError("baseUrl must use https, except for localhost");
27
+ }
28
+ return { url, text: baseUrl.replace(/\/+$/, "") };
29
+ }
30
+
31
+ // src/providers/anthropic.ts
32
+ var DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-5-5";
33
+ var PRICING_CHECKED_ON = "2026-10-29";
34
+ var KNOWN_PRICING = Object.freeze({
35
+ "claude-sonnet-5-5": Object.freeze({ inputPerMTok: 2, outputPerMTok: 10 }),
36
+ "claude-opus-5-5": Object.freeze({ inputPerMTok: 4, outputPerMTok: 20 })
37
+ });
38
+ var STOP_REASONS = {
39
+ end_turn: "end_turn",
40
+ stop_sequence: "end_turn",
41
+ tool_use: "tool_use",
42
+ max_tokens: "max_tokens",
43
+ refusal: "refusal"
44
+ };
45
+ function toAnthropicMessages(messages) {
46
+ return messages.map((message) => ({
47
+ role: message.role,
48
+ content: message.content.flatMap((block) => {
49
+ switch (block.type) {
50
+ case "text":
51
+ return block.text.length > 0 ? [{ type: "text", text: block.text }] : [];
52
+ case "tool_use":
53
+ return [{ type: "tool_use", id: block.id, name: block.name, input: block.input }];
54
+ case "tool_result":
55
+ return [{ type: "tool_result", tool_use_id: block.toolUseId, content: block.content, is_error: block.isError ?? false }];
56
+ case "provider_block":
57
+ return block.provider === "anthropic" ? [block.raw] : [];
58
+ }
59
+ })
60
+ }));
61
+ }
62
+ function fromAnthropicContent(content) {
63
+ return content.map((block) => {
64
+ if (block.type === "text") return { type: "text", text: block.text };
65
+ if (block.type === "tool_use") return { type: "tool_use", id: block.id, name: block.name, input: block.input };
66
+ return { type: "provider_block", provider: "anthropic", raw: block };
67
+ });
68
+ }
69
+ var AnthropicProvider = class {
70
+ name = "anthropic";
71
+ model;
72
+ pricing;
73
+ client;
74
+ effort;
75
+ constructor(options = {}) {
76
+ this.model = options.model ?? DEFAULT_ANTHROPIC_MODEL;
77
+ this.pricing = options.pricing ?? KNOWN_PRICING[this.model];
78
+ this.effort = options.effort;
79
+ if (options.client !== void 0) {
80
+ this.client = options.client;
81
+ return;
82
+ }
83
+ if (options.baseUrl === void 0 && options.useDefaultEndpoint !== true) {
84
+ throw new ProviderError("set baseUrl, or set useDefaultEndpoint: true to call api.anthropic.com, a US-hosted endpoint");
85
+ }
86
+ const apiKey = options.apiKey ?? process.env.ANTHROPIC_API_KEY;
87
+ if (typeof apiKey !== "string" || apiKey.length === 0) {
88
+ throw new ProviderError("apiKey is required: pass it, or set ANTHROPIC_API_KEY");
89
+ }
90
+ this.client = new Anthropic({
91
+ apiKey,
92
+ // Always set, so ANTHROPIC_BASE_URL in the environment cannot redirect the key.
93
+ baseURL: options.baseUrl !== void 0 ? checkBaseUrl(options.baseUrl).text : "https://api.anthropic.com"
94
+ });
95
+ }
96
+ async *stream(request) {
97
+ const stream = this.client.messages.stream(
98
+ {
99
+ model: this.model,
100
+ max_tokens: request.maxOutputTokens,
101
+ ...request.system !== void 0 ? { system: request.system } : {},
102
+ messages: toAnthropicMessages(request.messages),
103
+ ...request.tools.length > 0 ? {
104
+ tools: request.tools.map((tool) => ({
105
+ name: tool.name,
106
+ description: tool.description,
107
+ input_schema: tool.inputSchema
108
+ }))
109
+ } : {},
110
+ ...this.effort !== void 0 ? { output_config: { effort: this.effort } } : {}
111
+ },
112
+ { signal: request.signal }
113
+ );
114
+ for await (const event of stream) {
115
+ if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
116
+ yield { type: "text_delta", text: event.delta.text };
117
+ }
118
+ }
119
+ const message = await stream.finalMessage();
120
+ const usage = message.usage;
121
+ if (typeof usage?.input_tokens !== "number" || typeof usage.output_tokens !== "number") {
122
+ throw new ProviderError("anthropic response carried no token usage");
123
+ }
124
+ yield {
125
+ type: "message",
126
+ content: fromAnthropicContent(message.content),
127
+ stopReason: STOP_REASONS[message.stop_reason ?? ""] ?? "other",
128
+ usage: {
129
+ inputTokens: usage.input_tokens + (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0),
130
+ outputTokens: usage.output_tokens
131
+ }
132
+ };
133
+ }
134
+ };
135
+
136
+ export {
137
+ isLoopback,
138
+ checkBaseUrl,
139
+ DEFAULT_ANTHROPIC_MODEL,
140
+ PRICING_CHECKED_ON,
141
+ AnthropicProvider
142
+ };