@comradeweb/ai-kit
v1.0.0
Published
Anthropic SDK wrapper with prompt caching, structured output via Zod, cost recording through a pluggable port, and a generic AI-extraction step for @comradeweb/pipeline-kit.
Maintainers
Readme
ai-kit
Thin Anthropic-SDK wrapper for TypeScript + an extraction step for @comradeweb/pipeline-kit. Owns the auth, default model, prompt-cache placement, and cost-attribution sink; stays out of feature-specific scraping or prompt construction.
What you get:
AnthropicClientService—.complete()accepts a Zod schema and returns typedparsed_output, withcache_control: ephemeralplaced on the system block automatically.UsageRecord+AiUsageRecorderPort— flat per-call accounting shape and the optional sink the kit writes to (fire-and-forget).extractWithAiStep— generic pipeline step factory: pull HTML / screenshot evidence from a Page-like accessor, call Claude, hand the typed result toonResult.ANTI_HALLUCINATION_RULES+SELECTOR_STABILITY_RULES— reusable prompt fragments encoding the "ground every value in evidence" + "return a stable CSS selector" disciplines.computeCost— token-to-USD math against a hand-maintained model price table.
Installation
pnpm add @comradeweb/ai-kit @anthropic-ai/sdk @comradeweb/pipeline-kit zod@anthropic-ai/sdk, @nestjs/common, zod, and @comradeweb/pipeline-kit are peer dependencies.
Quick start
import { AnthropicClientService } from '@comradeweb/ai-kit';
import { z } from 'zod';
const client = new AnthropicClientService({ apiKey: process.env.ANTHROPIC_API_KEY! });
const result = await client.complete({
systemPrompt: 'You extract metrics from HTML excerpts. Return null when uncertain.',
userPrompt: `Find domain rating in this HTML: ${html}`,
schema: z.object({
value: z.number().nullable(),
evidence: z.string().nullable(),
confidence: z.number().min(0).max(1),
}),
meta: { pipelineRunId: 'run-1', pipelineName: 'extract-dr', stepName: 'fallback' },
});
// result is typed: { value: number | null; evidence: string | null; confidence: number }NestJS wiring
import { AnthropicModule, AI_USAGE_RECORDER_PORT } from '@comradeweb/ai-kit';
@Module({
imports: [
AnthropicModule.register({
apiKey: process.env.ANTHROPIC_API_KEY!,
defaultModel: 'claude-sonnet-4-6',
defaultEffort: 'low',
}),
],
providers: [
// Optional: bind a cost-recording sink. The kit injects this @Optional()
// so dev / smoke-test setups can skip it.
{ provide: AI_USAGE_RECORDER_PORT, useClass: MyMongoUsageRecorder },
],
})
export class AiModule {}MyMongoUsageRecorder implements AiUsageRecorderPort — one method, fire-and-forget:
@Injectable()
export class MyMongoUsageRecorder implements AiUsageRecorderPort {
constructor(@InjectModel(/*…*/) private model: Model<AiUsageDoc>) {}
async record(rec: UsageRecord): Promise<void> {
// Map UsageRecord → your domain shape (or persist directly).
// MUST NOT throw — log and return.
try { await this.model.create(rec); } catch (e) { /* log */ }
}
}extractWithAiStep — the pipeline integration
The step is the fallback path when deterministic CSS extraction returns null. Pipelines try CSS first (cheap, fast), then fall through to the AI step (slow, expensive, ground-truth):
import { extractWithAiStep, ANTI_HALLUCINATION_RULES, SELECTOR_STABILITY_RULES } from '@comradeweb/ai-kit';
import { z } from 'zod';
const SCHEMA = z.object({
value: z.number().nullable(),
selector: z.string().nullable(),
evidence: z.string().nullable(),
confidence: z.number().min(0).max(1),
});
const extractMetric = extractWithAiStep<MyCtx, z.infer<typeof SCHEMA>>(client, {
stepName: 'extract-metric-ai',
schema: SCHEMA,
systemPrompt: `
You extract a numeric metric from HTML excerpts.
${ANTI_HALLUCINATION_RULES}
${SELECTOR_STABILITY_RULES}
`,
buildUserPrompt: async (ctx) => `Find the value in this HTML:\n\n${await ctx.page.content()}`,
// Tell the step where the Page-like object lives in your context.
getPage: (ctx) => ctx.page,
evidence: 'html',
region: { selector: 'div[data-region="metric"]' }, // cuts token cost
onResult: async (ctx, result) => {
if (result.value != null) {
ctx.data['extract-metric-ai'] = result;
if (result.selector) await ctx.selectorCache.persist(result.selector);
}
},
});Why getPage instead of assuming ctx.page: the kit deliberately doesn't depend on @comradeweb/browser-service-kit (or any other browser kit). Any context type with a Page-like object — at any key, any shape — works as long as getPage returns something with content() / screenshot() / locator() methods. This breaks the kit-to-kit cycle and keeps ai-kit browser-agnostic.
Prompt caching — and why the split matters
The kit places cache_control: { type: 'ephemeral' } on the system block automatically. The split between systemPrompt (stable, cacheable) and userPrompt (volatile, per-request) is load-bearing:
- Put stable instructions in
systemPrompt: role definition, output contract, anti-hallucination rules, selector-stability rules. - Put volatile inputs in
userPrompt: the actual HTML, screenshot, question.
Mismatched stability defeats the 5-minute ephemeral cache. A typical sustained extraction workload reads 80–95% of system tokens from cache and pays ~10× less per call after the first hit. Putting per-request HTML in the system prompt erases that gain.
Cost recording — what gets written
Every complete() call (success OR failure with a usage object) emits a flat UsageRecord:
interface UsageRecord {
pipelineRunId: string | null; // from req.meta
pipelineName: string | null;
stepName: string | null;
callerLabel: string | null;
model: string; // resolved (default + override)
inputTokens: number;
outputTokens: number;
cacheReadInputTokens: number; // 5-min ephemeral cache reads
cacheCreationInputTokens:number; // cache writes (first hit)
costUsd: number | null; // null → model not in price table
durationMs: number;
succeeded: boolean;
}The recorder is the only place the kit emits side-effects beyond the API call itself. Hosts that want cost dashboards persist these to Mongo / Postgres / Snowflake; hosts that don't can omit the binding (the kit injects it @Optional()).
costUsd: null is a deliberate signal — when a model name isn't in the kit's price table (pricing.ts), the kit refuses to fabricate numbers. Hosts can flag such rows as "untracked spend" in their dashboards.
Concepts
| Type / Symbol | Purpose |
| --- | --- |
| AnthropicClientService | The SDK wrapper. Inject and call .complete(req). |
| AnthropicCompleteRequest<T> | { systemPrompt, userPrompt, schema, imagePng?, model?, effort?, maxTokens?, meta? }. |
| AnthropicCallMeta | Cost-attribution context: { pipelineRunId, pipelineName, stepName, callerLabel }. |
| AnthropicOptions | Module options: { apiKey, defaultModel?, defaultEffort? }. |
| AnthropicModule.register / registerAsync | Standard Nest dynamic module. |
| UsageRecord | Flat per-call accounting shape. |
| AiUsageRecorderPort / AI_USAGE_RECORDER_PORT | Optional sink for UsageRecords; one method, fire-and-forget. |
| extractWithAiStep(client, options) | Pipeline step factory: HTML/screenshot → Claude → typed result. |
| AiEvidencePage | Structural Page-like interface — content(), screenshot(), locator(). |
| ANTI_HALLUCINATION_RULES | Prompt fragment: ground every value, permission to fail, evidence quoting. |
| SELECTOR_STABILITY_RULES | Prompt fragment: tiered selector strategy (test attrs → ARIA → class-substring → text anchors → ID → bare structural). |
| computeCost(model, usage) | Token-to-USD math against pricing.ts. Returns null for untracked models. |
API reference
- Module
AnthropicModule.register({ apiKey, defaultModel?, defaultEffort? })AnthropicModule.registerAsync({ useFactory, inject?, imports? })
- Service
AnthropicClientService—new AnthropicClientService(options, recorder?);complete<T>(req): Promise<T>
- Step utilities
extractWithAiStep<TCtx, TResult>(client, options): Step<TCtx>
- Types
AnthropicOptions,AnthropicCompleteRequest<T>,AnthropicCallMetaUsageRecord,AiUsageRecorderPortExtractWithAiStepOptions<TCtx, TResult>,AiEvidencePage,AiEvidenceMode
- Prompt fragments
ANTI_HALLUCINATION_RULES,SELECTOR_STABILITY_RULES
- Utilities
computeCost(model, usage),knownModels
License
MIT © Vyacheslav D.
Contributing
See CONTRIBUTING.md for the developer workflow, test layout, and release process.
