{"_id":"@bmichaellee/ai-provider","_rev":"3-8c11ba1d0bc7f97b005f64531315af95","name":"@bmichaellee/ai-provider","dist-tags":{"latest":"0.3.0"},"versions":{"0.1.1":{"name":"@bmichaellee/ai-provider","version":"0.1.1","keywords":["llm","ai","provider","claude","anthropic","claude-code","tool-use"],"license":"MIT","_id":"@bmichaellee/ai-provider@0.1.1","maintainers":[{"name":"bmichaellee","email":"brandonmlee85@gmail.com"}],"homepage":"https://github.com/bmichaellee/ai-provider#readme","bugs":{"url":"https://github.com/bmichaellee/ai-provider/issues"},"dist":{"shasum":"a58b8bf11721b7258803f06639e62cf3cf018507","tarball":"https://registry.npmjs.org/@bmichaellee/ai-provider/-/ai-provider-0.1.1.tgz","fileCount":17,"integrity":"sha512-r3Ek+7ctsdBhJaRrF/OsbDVPfDQIH7VHIXIbuKyVZ3QKr6QqDC04nkYeMa7Ow0osmQhX12T9/UrKIzbt+wCPbQ==","signatures":[{"sig":"MEQCIDh/z4XzQZp4/sc8oDRYOhR0fr1/XUjhDOhBm9TMCW61AiBlnUwMXmEWmO65e891cUlyCukOGI7bFkTUi4kvJUma8g==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":22779},"type":"module","module":"dist/index.js","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"bd9e043c66a5e1cac7f7e0332aff121b4485a0a4","scripts":{"test":"vitest run --project pkg","build":"rm -rf dist && bun build src/index.ts --outdir dist --target node --packages external && tsc -p tsconfig.build.json","test:full":"vitest run","typecheck":"tsc -p tsconfig.json","postpublish":"git checkout -- package.json","prepublishOnly":"bun run build && npm pkg set --json exports='{\".\":{\"types\":\"./dist/index.d.ts\",\"default\":\"./dist/index.js\"}}' && npm pkg set module=dist/index.js"},"_npmUser":{"name":"bmichaellee","email":"brandonmlee85@gmail.com"},"repository":{"url":"git+https://github.com/bmichaellee/ai-provider.git","type":"git"},"_npmVersion":"11.12.1","description":"LLM client with interchangeable backends behind one provider-agnostic interface. Ships two Claude backends: the Anthropic API, or a local Claude Code install for keyless development and testing.","directories":{},"_nodeVersion":"24.15.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"zod":"^4.4.3","vitest":"4.1.7","typescript":"^6.0.3","@types/node":"^24.0.0","@anthropic-ai/sdk":"^0.104.1","@anthropic-ai/claude-agent-sdk":"^0.3.195"},"peerDependencies":{"zod":"^4.0.0","@anthropic-ai/sdk":">=0.104.1","@anthropic-ai/claude-agent-sdk":">=0.3.195"},"peerDependenciesMeta":{"@anthropic-ai/claude-agent-sdk":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/ai-provider_0.1.1_1784441595386_0.4921829001968896","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@bmichaellee/ai-provider","version":"0.2.0","keywords":["llm","ai","provider","claude","anthropic","claude-code","tool-use"],"license":"MIT","_id":"@bmichaellee/ai-provider@0.2.0","maintainers":[{"name":"bmichaellee","email":"brandonmlee85@gmail.com"}],"homepage":"https://github.com/bmichaellee/ai-provider#readme","bugs":{"url":"https://github.com/bmichaellee/ai-provider/issues"},"dist":{"shasum":"046d8b24f88a5aecb13d95e3591616842dd5799d","tarball":"https://registry.npmjs.org/@bmichaellee/ai-provider/-/ai-provider-0.2.0.tgz","fileCount":17,"integrity":"sha512-Bu8LASZGxbfPZKIrXU46qiQFgIInvbly2Oa1s5DklSRC038aiujiEueiWoA+EPYBvPVgKTnAnCFSxl9TAphG8Q==","signatures":[{"sig":"MEUCIQCH5E26i3Tu5VKM2pUc703qe3gcuXKODLlo6wzY/1pXdQIgYZGOnuNh7EJ3PMf0+rRGsmtCp6RHdTcDIgQ5FnZBhY0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bmichaellee%2fai-provider@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":35667},"type":"module","module":"dist/index.js","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"6c1d52f23197a6860c639c1d3171106fd84186c9","scripts":{"test":"vitest run --project pkg","build":"rm -rf dist && bun build src/index.ts --outdir dist --target node --packages external && tsc -p tsconfig.build.json","test:full":"vitest run","typecheck":"tsc -p tsconfig.json","postpublish":"git checkout -- package.json","prepublishOnly":"bun run build && npm pkg set --json exports='{\".\":{\"types\":\"./dist/index.d.ts\",\"default\":\"./dist/index.js\"}}' && npm pkg set module=dist/index.js"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:456dce51-87c3-4ffa-8c6b-2a61777e4299"}},"repository":{"url":"git+https://github.com/bmichaellee/ai-provider.git","type":"git"},"_npmVersion":"12.0.1","description":"LLM client with interchangeable backends behind one provider-agnostic interface. Ships two Claude backends: the Anthropic API, or a local Claude Code install for keyless development and testing.","directories":{},"_nodeVersion":"24.18.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"zod":"^4.4.3","vitest":"4.1.7","typescript":"^6.0.3","@types/node":"^24.0.0","@anthropic-ai/sdk":"^0.104.1","@anthropic-ai/claude-agent-sdk":"^0.3.195"},"peerDependencies":{"zod":"^4.0.0","@anthropic-ai/sdk":">=0.104.1","@anthropic-ai/claude-agent-sdk":">=0.3.195"},"peerDependenciesMeta":{"@anthropic-ai/claude-agent-sdk":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/ai-provider_0.2.0_1784947245493_0.9261559657360898","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@bmichaellee/ai-provider","version":"0.3.0","description":"LLM client with interchangeable backends behind one provider-agnostic interface. Ships two Claude backends: the Anthropic API, or a local Claude Code install for keyless development and testing.","keywords":["llm","ai","provider","claude","anthropic","claude-code","tool-use"],"type":"module","module":"dist/index.js","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"license":"MIT","publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/bmichaellee/ai-provider.git"},"scripts":{"typecheck":"tsc -p tsconfig.json","test":"vitest run --project pkg","test:full":"vitest run","build":"rm -rf dist && bun build src/index.ts --outdir dist --target node --packages external && tsc -p tsconfig.build.json","prepublishOnly":"bun run build && npm pkg set --json exports='{\".\":{\"types\":\"./dist/index.d.ts\",\"default\":\"./dist/index.js\"}}' && npm pkg set module=dist/index.js","postpublish":"git checkout -- package.json"},"peerDependencies":{"@anthropic-ai/claude-agent-sdk":">=0.3.220","@anthropic-ai/sdk":">=0.115.0","zod":"^4.0.0"},"peerDependenciesMeta":{"@anthropic-ai/claude-agent-sdk":{"optional":true}},"devDependencies":{"@anthropic-ai/claude-agent-sdk":"^0.3.220","@anthropic-ai/sdk":"^0.115.0","@types/node":"^24.0.0","typescript":"^7.0.2","vitest":"4.1.10","zod":"^4.4.3"},"gitHead":"48fd70a74412bdf7f62ea1f6d86c02161b8c971d","_id":"@bmichaellee/ai-provider@0.3.0","bugs":{"url":"https://github.com/bmichaellee/ai-provider/issues"},"homepage":"https://github.com/bmichaellee/ai-provider#readme","_nodeVersion":"24.18.0","_npmVersion":"12.0.1","dist":{"integrity":"sha512-9IQq9DkYTnx3P3VnJXJK6eqLM9uTDeWXLBJ4PdP5Oc4Pn+JxFn2D7xsDUkUMlLrhxhVOjoMmcOdL+9IbUgsSAg==","shasum":"9eaf58b714c001721dab3e40e1f4b17cfa01def5","tarball":"https://registry.npmjs.org/@bmichaellee/ai-provider/-/ai-provider-0.3.0.tgz","fileCount":28,"unpackedSize":56077,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bmichaellee%2fai-provider@0.3.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQD+lQy90RkKJSrLz2CmIgH21tjtPcGb72c1k/IO1GCvawIhAM0FkVme8TE/SoW4cjaNETt2hreQqiY5jqaKMVOOKnhj"}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:456dce51-87c3-4ffa-8c6b-2a61777e4299"}},"directories":{},"maintainers":[{"name":"bmichaellee","email":"brandonmlee85@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/ai-provider_0.3.0_1785040034252_0.9616798640054429"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-19T06:13:15.267Z","modified":"2026-07-26T04:27:14.688Z","0.1.1":"2026-07-19T06:13:15.512Z","0.2.0":"2026-07-25T02:40:45.631Z","0.3.0":"2026-07-26T04:27:14.391Z"},"bugs":{"url":"https://github.com/bmichaellee/ai-provider/issues"},"license":"MIT","homepage":"https://github.com/bmichaellee/ai-provider#readme","keywords":["llm","ai","provider","claude","anthropic","claude-code","tool-use"],"repository":{"type":"git","url":"git+https://github.com/bmichaellee/ai-provider.git"},"description":"LLM client with interchangeable backends behind one provider-agnostic interface. Ships two Claude backends: the Anthropic API, or a local Claude Code install for keyless development and testing.","maintainers":[{"name":"bmichaellee","email":"brandonmlee85@gmail.com"}],"readme":"# @bmichaellee/ai-provider\n\nAn LLM client with interchangeable backends behind one provider-agnostic\ninterface. Calling code targets `AIProvider` — messages in, text segments out,\nwith tools and streaming callbacks — and never a vendor SDK, so backends for\nother model families can be added without touching call sites. Two Claude\nbackends ship today:\n\n- **`anthropic`** — the Anthropic API via `@anthropic-ai/sdk`, with its own\n  sequential, order-preserving tool loop.\n- **`local`** — a local Claude Code install via `@anthropic-ai/claude-agent-sdk`,\n  with tools exposed as an in-process MCP server. No API key, no per-token cost:\n  ideal for development and testing.\n\n```ts\nimport { createProvider } from \"@bmichaellee/ai-provider\";\n\nconst ai = createProvider();\n\nconst segments = await ai.sendMessage([{ role: \"user\", content: \"Hello!\" }], {\n  system: \"Be helpful.\",\n  onText: (segment) => process.stdout.write(segment),\n});\n\nawait ai.destroy();\n```\n\n## Backend selection\n\n`createProvider()` with no arguments keeps things automatic: if\n`ANTHROPIC_API_KEY` is set (or `apiKey` is passed), you get the Anthropic\nbackend; otherwise the local one. Every choice can also be explicit:\n\n```ts\ncreateProvider(); // env-sniff\ncreateProvider({ apiKey: key }); // Anthropic, key without env\ncreateProvider({ backend: \"anthropic\" }); // force API (key from env)\ncreateProvider({ backend: \"local\" }); // force local Claude Code\n```\n\nThe local backend requires the optional peer `@anthropic-ai/claude-agent-sdk`\n**and** a logged-in Claude Code install on the machine. It is a development-machine\nbackend: headless servers and CI runners without a Claude Code login cannot use it.\n\nEvery client exposes which backend it is as `providerKind` (`\"anthropic\"` or\n`\"local\"`), and each usage event echoes it. Check\n`provider.providerKind === \"local\"` for the keyless development backend instead\nof sniffing `ANTHROPIC_API_KEY` yourself — anything other than `\"local\"` is a\nmetered API.\n\nWhere you need the answer but hold no provider — a health check, a startup\nbanner — `resolveProviderKind()` applies the same rules without constructing\none:\n\n```ts\nimport { resolveProviderKind } from \"@bmichaellee/ai-provider\";\n\nresolveProviderKind(); // \"anthropic\" | \"local\", from the same env/config rules\nresolveProviderKind({ backend: \"local\" }); // \"local\"\n```\n\n## Models\n\n`ClaudeModels` names one model per family, and each name always points at the\nlatest release of that family:\n\n```ts\nClaudeModels.Sonnet; // claude-sonnet-5\nClaudeModels.Opus; // claude-opus-5\nClaudeModels.Haiku; // claude-haiku-4-5\nClaudeModels.Fable; // claude-fable-5\n```\n\nBecause the names track the latest release, upgrading this package can change\nwhich model a name resolves to — see the changelog for any release that moves\none. Superseded models stay selectable by literal (`\"claude-opus-4-8\"` is still\na valid `ClaudeModel` with a full catalog entry); they simply lose their named\nconstant, so pinning is explicit.\n\nModel ids from a config file, a request body, or a database row are just\nstrings. Validate them with `claudeModelSchema`, and size budgets with the\ntotal lookups, which fall back to the default model rather than returning\n`undefined` for a model the catalog has not heard of:\n\n```ts\nclaudeModelSchema.parse(row.model); // throws on an unknown id\ncontextWindowFor(row.model); // always a positive number\nmaxTokensFor(row.model);\nsupportsEffort(row.model);\n```\n\n## Tools\n\nA tool is a name, a description, a zod v4 object schema, and a `run` function\nreturning a string:\n\n```ts\nimport { z } from \"zod\";\nimport type { ToolSpec } from \"@bmichaellee/ai-provider\";\n\nconst roll: ToolSpec = {\n  name: \"roll_die\",\n  description: \"Roll a die with the given number of sides.\",\n  schema: z.object({ sides: z.number().int().min(2) }),\n  run: async ({ sides }) => String(1 + Math.floor(Math.random() * sides)),\n};\n```\n\nTools receive a `ToolContext` as their second argument:\n\n- `ctx.stop` — set it to `true` to end the conversation turn: no further model\n  turns run. On the `local` backend, model text later in the stream is suppressed\n  too; on the `anthropic` backend, text the model already placed after the tool\n  call _within the same message_ is still emitted. If the turn ends with no text\n  at all, the optional `stopText` send option is emitted instead (nothing is\n  emitted by default).\n- `ctx.app` — an app-defined context object, passed through verbatim from the\n  `context` send option. Type it by parameterizing: `ToolSpec<Input, MyContext>`.\n\n```ts\nawait ai.sendMessage(messages, {\n  tools: [roll],\n  context: { session }, // arrives as ctx.app\n  stopText: \"[The conversation ends.]\", // emitted on a silent stop\n  onUsage: (usage) => console.log(usage.percentUsed, \"% of context used\"),\n});\n```\n\n## Usage reporting\n\n`onUsage` fires once per underlying model API call, on both backends. Each\n`ContextUsage` describes that single call: `inputTokens` and the cache fields\nare the prompt actually sent, `outputTokens` is that call's completion, and\n`percentUsed` is `(inputTokens + cacheCreationInputTokens +\ncacheReadInputTokens) / contextWindow * 100`, rounded to one decimal — the\nlive fullness of the context window for that request, on a 0–100 scale. A\nsingle `sendMessage()` may perform several calls (one per tool-loop\niteration, numbered by `iteration`), so you may receive several events; the\n**last** event's `percentUsed` is the current window fullness. Never sum\ntoken fields or `percentUsed` across events to estimate fullness. On the\nlocal backend, API calls made by subagents (e.g. the built-in Task tool) are\nexcluded — they describe a subagent's context, not yours; their spend still\nshows up in `onTurnUsage` totals.\n\nFor the turn's total spend, use `onTurnUsage`. It fires at most once per\n`sendMessage()`, after the final model call, with token counts accumulated\nacross every call in the turn — and, on the local backend, the\nprovider-reported `costUSD` and authoritative `contextWindow`. `TurnUsage`\nintentionally has no `percentUsed`: cumulative tokens divided by the window is\nnot a meaningful fullness number. Window fullness lives in `onUsage`; turn\ncost lives in `onTurnUsage`.\n\nOn the local backend, every `ContextUsage` **and** `TurnUsage` also carries\n`sessionOverheadTokens` (the exported `LOCAL_SESSION_OVERHEAD_TOKENS`\nestimate, ~65k): Claude Code's built-in tool schemas and machinery occupy that\nmuch of each call's context beyond what you supplied. Budget app content\nagainst `contextWindow - sessionOverheadTokens`. It is a per-call figure on\nboth events — never multiply it by `iterationCount`.\n\nEvery field above is also exported as a runtime schema — `contextUsageSchema`,\n`turnUsageSchema`, `toolActivitySchema` — so a consumer that needs to validate\nor re-publish these events can derive from them rather than hand-mirroring the\ntypes and re-breaking on every field this package adds.\n\n## When the model declines\n\nBoth backends throw `RefusalError` when the model refuses a request, rather\nthan resolving with an empty segment list that reads as a successful turn with\nnothing to say. On the local backend, a refusal the Claude Code harness\nretried successfully on a fallback model is not a refusal from your side and\ndoes not throw.\n\n```ts\nimport { isRefusalError } from \"@bmichaellee/ai-provider\";\n\ntry {\n  await ai.sendMessage(messages);\n} catch (error) {\n  if (isRefusalError(error)) {\n    // error.category  — \"cyber\" | \"bio\" | ... | null, open-ended\n    // error.explanation — provider prose, display only\n    // error.providerKind, error.model\n  }\n}\n```\n\n`onTurnUsage` fires before the throw: the calls were made and, on a metered\nbackend, billed. Prefer `isRefusalError()` to `instanceof RefusalError` at a\npackage boundary — a consumer that resolves two copies of this package gets\ntwo distinct classes, and `instanceof` quietly returns false for one of them.\n\n## The local backend keeps its built-in tools\n\n`allowedTools` allowlists the app's MCP tools; it does not — and deliberately\ndoes not try to — disable the local Claude Code session's built-ins\n(WebSearch, Read, Bash, ...). They stay live **by design**: tool suppression\nbreeds refusal; honesty works. The session is told, truthfully, what it is\nhelping with, and it plays along.\n\nThe consumer-side half of that contract is an app-supplied honest preamble via\n`system`, telling the session what it is really doing. For example, a\ndevelopment harness might ship:\n\n```ts\nconst system =\n  \"You are narrating for a game engine under development. Your replies are \" +\n  \"parsed as story text, so keep working notes out of the reply itself. It \" +\n  \"is fine to use your tools; report anything unusual through them, not in \" +\n  \"the narration.\";\n```\n\nBuilt-in tool runs surface through the `onToolActivity` send option instead of\nbeing silently absorbed: a `start` event when the session invokes a tool (name\nplus a one-line input summary), an `end` event when its result lands, and a\n`commentary` event carrying any model text that shared a message with the tool\ncall (\"let the search finish\"). That commentary is diverted out of the\nreturned segments and `onText` — it is working narration about the tool run,\nnot content — so transcripts stay clean whether or not you listen:\n\n```ts\nawait ai.sendMessage(messages, {\n  onToolActivity: (a) => console.log(`[${a.phase}] ${a.summary}`),\n});\n```\n\nMulti-turn conversations are sent to the local session as a closed\n`<conversation-transcript>` block plus a standing system instruction to write\nonly the assistant's next turn — so the model has no dangling `user:` prefix\nto complete and cannot drift into answering an invented user message.\n\n## Other providers\n\nToday the package is Claude through and through: both backends are Claude, and\nthe exported model catalog (`ClaudeModels`, `ClaudeMaxTokens`,\n`ClaudeContextWindow`, `ClaudeEffort`) is Claude's. The source layout draws\nthe line: everything Claude-specific — both clients and the model catalog —\nlives in `src/providers/anthropic/`, and the root of `src/` is\nprovider-agnostic. A new provider is a sibling directory under\n`src/providers/` — an `AIProvider` implementation exported through the\n`providers/` barrel, plus a `ProviderBackend` entry in `createProvider` —\nwith its model catalog sitting alongside the Claude one. Existing call sites\nkeep working unchanged.\n\n## Environment\n\n- `ANTHROPIC_API_KEY` — selects and authenticates the Anthropic backend when no\n  explicit config is given.\n- `DEBUG_TOOL_CALLS` — when set, logs every tool invocation (name, input, result)\n  to the console.\n\n## Peer dependencies\n\n- `zod` ^4 (tool schemas and the exported runtime schemas; v4's\n  `z.toJSONSchema` is used)\n- `@anthropic-ai/sdk` >=0.115.0 (Anthropic backend)\n- `@anthropic-ai/claude-agent-sdk` >=0.3.220 — optional; only needed for the\n  local backend\n\nThe floors are the versions this package is built and tested against. The\nagent-SDK floor in particular is not cosmetic: releases before 0.3.220 report a\n200,000-token context window for Claude Opus 5, which this package would\nforward through `onTurnUsage` as authoritative.\n\n## Releasing\n\nPublishing to npm runs through the `Publish` GitHub Actions workflow\n(`.github/workflows/publish.yml`), dispatched manually from `main` with a\n`bump` input (`patch`/`minor`/`major`). It bumps this package's `version`,\ncommits and pushes that bump to `main` as `github-actions[bot]`, runs\n`npm publish` (which builds `dist/` via `prepublishOnly`), and tags the\nrelease `v<x.y.z>`. There's no manual publish path — always go through the\nworkflow so the version bump, the npm release, and the git tag stay in sync.\n","readmeFilename":"README.md"}