@capitalthought/anthropic
v0.3.0
Published
Shared Anthropic SDK wrapper for Capital Thought projects — model registry, cost cap, agent harness, storage-adapter pattern.
Readme
@capitalthought/anthropic
Shared Anthropic SDK wrapper for Capital Thought projects — model registry, cost cap, agent harness, storage-adapter pattern.
Why
Across the portfolio, the Anthropic SDK is currently wrapped (badly) in four different places. Each wrap is missing different parts of the playbook:
| Repo | Has | Missing |
|---|---|---|
| pitch2 | Cost tracker (enforceAiCap / recordAiUsage); tool-use agent harness; model registry | n/a — canonical impl |
| mikey | Model registry (just shipped) + health check | Cost cap, agent harness |
| highfives | Model registry + health check | Cost cap, agent harness |
| multipov, bpm, actualjob, doorman, personalize, … | n/a | Bare new Anthropic() calls — no wrapper, no cost cap, no model registry |
This package consolidates the canonical patterns and exposes them via a storage-agnostic interface. Workers consumers and Vercel/Node consumers use the same API; the storage adapter at the seam is the only thing that differs per runtime.
Phase 1 — what's in this commit
Stubs + contracts + a working in-memory adapter for tests. No consumer changes yet. This phase commits zero runtime risk.
| Module | Status |
|---|---|
| models.ts — CLAUDE_MODELS registry, extendModels, getModel | ✅ shipped |
| health.ts — checkClaudeModels() health probe | ✅ shipped |
| cost-tracker.ts — createCostTracker() (per-user + global caps) | ✅ shipped |
| agent.ts — runAgent() tool-use harness | 🟡 contract defined, impl in Phase 2 |
| adapters/memory.ts — in-memory cost adapter for tests | ✅ shipped |
| adapters/postgres.ts — Postgres cost adapter (Vercel/pitch2) | 🟡 stub; impl in Phase 2 |
| adapters/kv.ts — Cloudflare KV cost adapter (Workers) | 🟡 stub; impl in Phase 3 |
Migration phases
- Phase 1 (this scaffold). Create the package + contracts + memory adapter + tests. No consumer changes.
- Phase 2. Lift
pitch2/src/lib/ai-cost-tracker.ts+pitch2/src/lib/pitchbot/agent.ts+pitch2/src/lib/claude/models.tsinto the package. pitch2 becomes the package's first consumer (just imports from@capitalthought/anthropicinstead of local files). - Phase 3. Port the KV adapter for Workers. Migrate
mikeyto the package (dropsmikey/src/shared/claude-models.ts, gainsenforceAiCapfor free → closes mikey's security review High #6 finding). - Phase 4. Migrate
highfives,multipov,bpm,doorman,personalize, etc.
Each phase is a separate PR. Each consumer migrates on its own timeline.
Design decisions (2026-05-03)
Multi-tenant scoping (optional orgId — added 0.3.0)
Every cost bucket can optionally be scoped to a tenant identifier. Pass orgId on recordAiUsage and enforceAiCap so a user shared across two orgs (e.g. a fractional COO with one email in two tenants) gets isolated counters per tenant. Omit orgId and the historical per-user shape is preserved unchanged.
await costs.recordAiUsage({
userId: "[email protected]",
orgId: "org-uuid", // ← new, optional
route: "responder",
model: MIKEY_MODELS.RESPONDER,
inputTokens: 100,
outputTokens: 200,
});
const cap = await costs.enforceAiCap({
userId: "[email protected]",
orgId: "org-uuid", // ← new, optional; pair with orgId above
route: "responder",
});KV bucket shape becomes <prefix>:<orgId>:<userId>:<YYYY-MM-DD> when orgId is supplied. The Postgres adapter currently ignores orgId — pitch2 is single-tenant; multi-tenant Postgres consumers should add an org_id text column and PR a similar threading through record()'s insert + the two select predicates.
Per-app caps, not global cross-app
Per-app caps for v1 — each consumer plugs in its own storage adapter and gets its own $5/user-day, $50/user-month, $1000/day-app ceiling. The "shared cap across all apps" benefit (a single $1k/day-company ceiling that applies to every app's calls combined) is deferred until any single app exhausts its per-app ceiling at scale — none do today.
If we ever need cross-app aggregation, the adapter pattern lets us point multiple consumers at one storage backend. The contract doesn't change.
Storage-adapter pattern
The package itself is storage-agnostic. Cost tracker requires a CostStorageAdapter at construction time — the adapter handles record, perUserTotals, globalTotals. Workers consumers wire up the KV adapter; Vercel/Postgres consumers wire up the Postgres adapter; tests use the in-memory adapter. Same package code runs everywhere.
Configurable agent caps with safe-by-default floors
runAgent() defaults to 4 tool calls / 20s wall / 800 output tokens. Each consumer can override (e.g. mikey's responder might want maxToolCalls: 0 = single-shot, no agent loop, while pitchbot runs the full 4-tool loop). Defaults set the safe-by-default floor; overrides are for when you genuinely know better.
Model registry pinning
HAIKU is pinned to the dated snapshot (claude-haiku-4-5-20251001) per ~/icloud/Claude/Model Version Management.md. The unpinned alias (claude-haiku-4-5) returns the latest snapshot which can shift between deploys. Test enforces this at src/__tests__/models.test.ts.
OPUS and SONNET use the major.minor aliases (claude-opus-4-7, claude-sonnet-4-6) because Anthropic doesn't ship snapshot variants for them at present. Revisit if/when they do.
Usage (once Phase 2 lands)
import {
CLAUDE_MODELS,
createCostTracker,
runAgent,
extendModels,
} from "@capitalthought/anthropic";
import { createMemoryAdapter } from "@capitalthought/anthropic/adapters/memory";
import Anthropic from "@anthropic-ai/sdk";
const MIKEY_MODELS = extendModels({
CLASSIFIER: CLAUDE_MODELS.HAIKU,
RESPONDER: CLAUDE_MODELS.SONNET,
});
const costs = createCostTracker({
adapter: createMemoryAdapter(), // or KV / Postgres in real consumers
// defaults: $5/user-day, $50/user-month, $1000/day-app
});
const cap = await costs.enforceAiCap({ userId: "u1", route: "responder" });
if (!cap.allowed) {
// bail with cap.reason: "per_user_daily" | "per_user_monthly" | "global_daily_cap"
}
// ... call Anthropic ...
await costs.recordAiUsage({
userId: "u1",
route: "responder",
model: MIKEY_MODELS.RESPONDER,
inputTokens: response.usage.input_tokens,
outputTokens: response.usage.output_tokens,
});Scripts
npm run check # tsc --noEmit
npm run build # tsc → dist/
npm run test # vitest runRelated
~/icloud/Claude/Model Version Management.md— pinning playbookpitch2/src/lib/ai-cost-tracker.ts— canonical cost-tracker impl (lift target for Phase 2)pitch2/src/lib/pitchbot/agent.ts— canonical tool-use agent harness (lift target for Phase 2)mikey/src/shared/claude-models.ts— first port of the model registry pattern (drops in Phase 3)
