Package Exports
- @punktechnologies/sdk
- @punktechnologies/sdk/anthropic
- @punktechnologies/sdk/langchain
- @punktechnologies/sdk/openai
- @punktechnologies/sdk/openrouter
- @punktechnologies/sdk/vercel
- @punktechnologies/sdk/vercel-ai
Readme
@punktechnologies/sdk
Gateway Agnostic TypeScript SDK for AI agents using OpenAI, Anthropic, OpenRouter, Vercel AI SDK, LangChain, Claude Code, and compatible gateways.
npm i @punktechnologies/sdkCreate a runnable starter:
npm create @punktechnologies/punk-agent my-agent
# or
npx @punktechnologies/create-punk-agent my-agentPunk is the adaptive runtime for production AI agents. Point existing model traffic at the Punk gateway, then use this SDK where gateway traffic alone cannot see enough context: tool tracing, side-effect declarations, tool-result caching, semantic web fetches, feedback, receipts, evidence packets, MCP registry helpers, prompt ingest, and learning/artifact APIs.
Links: Agent activation, SDK docs, OpenRouter, OpenAI gateway guide, Vercel AI SDK, LangChain, Anthropic SDK, Claude Code.
This npm package is the client integration surface. Connect it to hosted Punk or a self-hosted Punk gateway.
Install
npm i @punktechnologies/sdk
# or
bun add @punktechnologies/sdkZero runtime dependencies. Requires Node 18+ or Bun and a running Punk gateway. new Punk() reads PUNK_BASE_URL, PUNK_API_KEY, PUNK_APP, PUNK_AGENT, and PUNK_SUBJECT; explicit constructor options still win. For local evaluation from the Punk repo:
bun install
bun run dev # gateway + dashboard + learning loop on http://localhost:4100For hosted trials, use baseUrl: "https://app.punktechnologies.com" with a tenant API key from the dashboard.
Choose Your Path
| Path | Best when | Start here |
|---|---|---|
| Gateway-only | You already use OpenAI, Anthropic, Vercel AI SDK, LangChain, or another compatible client. | Change baseURL and add X-Punk-* headers. |
| SDK client | You want typed helpers for chat, feedback, savings, runs, artifacts, SOM fetches, web sessions, and MCP registry calls. | Use new Punk(...). |
| Tool tracing | You need Punk to see tool calls, side-effect levels, and read-only tool-cache eligibility. | Wrap tools with traceTool(...). |
| Evidence reads | You need route explanations, receipts, or security/support evidence packets. | Call runDetail, receipt, or evidencePacket. |
| Starter app | You want a fresh runnable project with chat, traced tools, feedback, and route evidence. | npm create @punktechnologies/punk-agent. |
60-Second Start
1. Point existing model traffic at Punk.
You do not need this SDK for the core gateway value. OpenAI-style and Anthropic-style clients can talk to Punk by changing the gateway URL, and the SDK can generate the right config objects when you do use it.
import OpenAI from "openai";
import { Punk } from "@punktechnologies/sdk";
const punk = new Punk({ app: "my-app", agent: "my-bot", subject: "user-123" });
const client = new OpenAI(punk.openAIConfig());2. Use the SDK when you need the richer runtime surface.
import { Punk } from "@punktechnologies/sdk";
const result = await punk.gateway.chat({
model: "gpt-4o",
messages: [{ role: "user", content: "Classify this ticket: refund request" }],
});
console.log(result.content, result.route, result.runId, result.usage);
// route is usually "live" on the first call; eligible repeats can route
// through "exact_cache" and, once learned and proven, "artifact".3. Open the dashboard and inspect the run.
Open http://localhost:4100 locally, or https://app.punktechnologies.com for hosted trials. Every response carries a run id and route; the run detail explains why Punk chose that path and what it saved.
API tour
Construct once per app/agent identity. Omit options to read PUNK_BASE_URL, PUNK_API_KEY, PUNK_APP, PUNK_AGENT, and PUNK_SUBJECT from the environment.
const punk = new Punk({
baseUrl: "http://localhost:4100", // default
apiKey: process.env.PUNK_API_KEY, // only if the gateway requires bearer auth
app: "support-triage", // X-Punk-App
agent: "triage-bot", // X-Punk-Agent
subject: "user-123", // X-Punk-Subject (pseudonymous end user)
});Adapter config helpers
Use these helpers when your app already has a provider client:
new OpenAI(punk.openAIConfig());
new Anthropic(punk.anthropicConfig());
const aiSdkProvider = createOpenAICompatible(
punk.vercelOpenAICompatibleConfig({ name: "punk" })
);
const model = new ChatOpenAI({
model: "gpt-4o",
...punk.langChainConfig()
});identityHeaders() returns the X-Punk-* headers, with optional Authorization. All config helpers accept per-call overrides such as { app, agent, subject, baseUrl, apiKey }.
Stack-specific subpath imports expose the same dependency-free helpers without importing provider packages:
import { createPunkOpenAIConfig } from "@punktechnologies/sdk/openai";
import { createPunkAnthropicConfig } from "@punktechnologies/sdk/anthropic";
import { createPunkVercelAIConfig } from "@punktechnologies/sdk/vercel-ai";
import { createPunkLangChainConfig } from "@punktechnologies/sdk/langchain";
import { createPunkOpenRouterConfig, openRouterModel } from "@punktechnologies/sdk/openrouter";
new OpenAI(createPunkOpenAIConfig({ app: "support", agent: "triage" }));
new Anthropic(createPunkAnthropicConfig({ app: "support", agent: "triage" }));
const model = openRouterModel("google/gemini-2.5-flash");OpenRouter routing happens in the Punk gateway. The SDK helper only normalizes OpenRouter provider/model slugs and returns OpenAI-compatible client config for the gateway.
chat(params) / openai.chat(params) — OpenAI-style completions
const r = await punk.chat({
model: "gpt-4o",
messages: [{ role: "user", content: "hello" }],
temperature: 0,
});
r.content; // assistant text
r.runId; // from the x-punk-run-id response header — use it for tracing/feedback
r.route; // "live" | "exact_cache" | "artifact" | ... (x-punk-route header)
r.usage; // normalized input/output/total token counts when present
r.model; // response model when present
r.provider;// response provider when present
r.raw; // the full OpenAI-shaped response bodyEvery response carries a run id and the route Punk chose. punk.runDetail(r.runId) returns the full trace and the RouteExplanation — why this route, what was rejected, what it saved.
Streaming is built in:
for await (const chunk of punk.streamChat({
model: "gpt-4o",
messages: [{ role: "user", content: "Stream a support reply." }]
})) {
if (chunk.type === "delta") process.stdout.write(chunk.content);
}anthropic.messages(params) — Anthropic Messages
const msg = await punk.anthropic.messages({
model: "claude-sonnet-4-6",
max_tokens: 256,
messages: [{ role: "user", content: "What is a deterministic artifact?" }]
});
msg.content; // text blocks joined together
msg.contentBlocks; // original Anthropic content blocks
msg.runId;
msg.route;
msg.usage;Streaming Anthropic-shaped responses works the same way:
for await (const chunk of punk.streamMessages({
model: "claude-sonnet-4-6",
max_tokens: 256,
messages: [{ role: "user", content: "Stream a haiku about caching." }]
})) {
if (chunk.type === "delta") process.stdout.write(chunk.content);
}For Punk Chorus, use model: "punk/chorus" and add Chorus-specific routing fields to the same body. The SDK helper below uses the OpenAI-style chat wire; direct HTTP callers can use the same model id through supported gateway wires.
import { Punk, punkChorusChat } from "@punktechnologies/sdk";
const punk = new Punk({ app: "support", agent: "chorus-client" });
const r = await punk.chat(punkChorusChat({
messages: [{ role: "user", content: "Build a claim-graph answer with a receipt." }],
budget_limit_usd: 0.25,
latency_mode: "balanced",
quality_mode: "maximum_quality",
policy_profile: "regulated-support",
receipt_mode: "full",
circuit_mode: "learn",
shadow_mode: true,
chorus: { requestId: "req_123", workflowId: "wf_support" },
}));
const receipt = await punk.receipt(r.runId); // GET /api/v1/receipts/:id
// Current gateways may expose the same receipt-style material as an evidence packet:
const packet = await punk.evidencePacket(r.runId); // GET /api/v1/runs/:id/evidence-packetChorus uses one model id with per-request focus controls:
| Focus | SDK fields |
|---|---|
| Fast | latency_mode: "fast", optional quality_mode: "economy" |
| Balanced | latency_mode: "balanced", quality_mode: "balanced" |
| Deep reasoning | latency_mode: "deep", quality_mode: "frontier_optional" |
| Source-backed research | research_mode: "som", research_max_queries, research_max_sources |
| Maximum quality | latency_mode: "maximum_quality", quality_mode: "maximum_quality", optional sota_mix, live_panel_models, and live_synthesis_model |
| Private/local | local_only: true, optional allowed_model_classes |
| Shadow evaluation | shadow_mode: true, circuit_mode: "learn" |
Use receipt_mode: "off" to suppress receipt material. The gateway also accepts "none" as a compatibility alias. Use live_synthesis_required: true for benchmark or production gates where falling back to a mock or local path would be misleading.
traceTool(def) — declare tools with side-effect levels
Wrap a tool so each invocation is traced into its run, and read-only results flow through the tool-result cache:
const lookupAccount = punk.traceTool({
name: "crm.lookupAccount",
sideEffectLevel: 1, // read-only external
ttlSeconds: 300, // level <= 1 + TTL => tool-result cache participation
execute: async (args: { accountId: string }) => crm.get(args.accountId),
});
await punk.withRun(r, async () => {
const account = await lookupAccount({ accountId: "acct_42" });
});Side-effect levels (PRD §17):
| Level | Meaning | Example |
|---|---|---|
| 0 | Pure computation | parse, format, math |
| 1 | Read-only external | CRM read, search, fetch |
| 2 | Reversible/idempotent write | upsert with idempotency key |
| 3 | User-visible write | email, Slack, ticket creation |
| 4 | High-impact | payments, deletion, permissions |
Undeclared tools default to level 3 (conservative). Levels 0–1 with a TTL are cached per tenant/subject; levels ≥ 2 emit side_effect.planned before execution so replay and shadow runs can suppress them. traceTool uses an explicit { runId } when supplied, otherwise the active withRun(...) context. Without either, the tool still executes — just untraced. Cache and trace failures never break the tool call.
feedback(runId, rating, correction?) — close the loop
await punk.feedback(r.runId, 1); // thumbs up
await punk.feedback(r.runId, -1, "should be: billing"); // correctionFeedback feeds the learner: corrections count against pattern stability and artifact confidence.
fetchSom(url) — semantic web fetch (token savings)
Compiles a page to a Semantic Object Model instead of handing your model raw HTML:
const page = await punk.fetchSom("https://example.com/pricing");
page.som; // regions/elements with stable ids
page.context; // compact text ready for a prompt
page.tokensSavedEstimate; // raw-HTML tokens you didn't pay for
page.cached; // second fetch of the same URL hits the SOM cache
page.diff; // semantic diff vs. the previous snapshot, when bypassing cacheA typical marketing page compresses ~10–50x; the savings show up in punk.savings().somTokensSaved. Pass { bypassCache: true } to force a refetch and get a semantic diff (pricing changed ≠ footer changed).
Web sessions & actions — punk.web.*
Observation is half the loop; sessions close it. Open a stateful session, act on SOM element ids, get back a fresh SOM + semantic diff after every action:
const { sessionId, som, context } = await punk.web.openSession("https://example.com");
// click a link by its SOM element id (e_…) — navigates and recompiles
const link = som.regions.flatMap((r) => r.elements).find((e) => e.role === "link");
const r1 = await punk.web.act(sessionId, { action: "click", target: link.id });
r1.result.navigated; // true
r1.diff; // what changed, semantically weighted
// fill and submit a form (target a field for type/select, the r_form region for submit)
await punk.web.act(sessionId, { action: "type", target: "e_abc123", value: "k@example.com" });
await punk.web.act(sessionId, { action: "select", target: "e_def456", value: "pro" });
const submit = await punk.web.act(sessionId, { action: "submit", target: "r_form" });
submit.result.posted; // serialized fields sent by the form submission
await punk.web.closeSession(sessionId); // idle sessions auto-close after 5 minutesActions are protocol-level (link follows, urlencoded form submits — no JS engine) and policy-governed server-side:
| action | side-effect level | governed action | notes |
|---|---|---|---|
type, select |
0 | read:web |
mutate session-local form state only |
form-local click |
0 | read:web |
checkbox/radio/reset mutate session state only |
navigation click |
1 | read:web |
link navigation = a read of another page |
submit-button click |
3 | write:web |
same governance as submitting the form |
submit |
3 | write:web |
a real remote write — deniable/holdable by policy (403) |
Observe-mode keys can read (click/type/select) but never perform web writes. Every action is audited, and every navigation destination is SSRF-guarded by the gateway.
Read APIs
await punk.savings(); // SavingsSummary: runs, cost, saved USD/ms, hit rates
await punk.patterns(); // discovered patterns and their lifecycle state
await punk.artifacts(); // synthesized artifacts with confidence + evidence counts
await punk.artifactDetail(id); // artifact + replay/shadow evaluations + source pattern
await punk.runDetail(id); // run + full trace events + side-effect records
await punk.explain(id); // routeExplanation only
await punk.savingsForRun(id); // per-run cost/savings counters
await punk.sideEffectsForRun(id);
await punk.waitForRun(id); // poll until completed/failed/blocked
await punk.receipt(id); // Chorus receipt for a run
await punk.evidencePacket(id); // support/security evidence packet for a run
await punk.cacheStats(); // per-tier entries and hitsLearning lifecycle
const report = await punk.learningTick(); // force a learning pass (it also runs on a timer)
const artifact = await punk.promoteArtifact(id); // operator approval after replay+shadow proofPromotion is gated: an artifact needs passing replay evidence against historical traces and shadow agreement against live traffic before promoteArtifact succeeds. Rollback/quarantine are available via the API and dashboard.
Low-level
await punk.trace(runId, "tool.completed", { name: "x", result }); // append a trace event
await punk.ingestPrompt("claude-code", prompt); // side-load an observed prompt
await punk.toolCacheCheck("crm.lookupAccount", args); // manual cache check (degrades to miss)
await punk.toolCacheStore("crm.lookupAccount", args, result, 300); // manual store (failures swallowed)External MCP servers for workflow tool_call nodes are available through punk.mcp.listServers(), punk.mcp.createServer(...), and punk.mcp.testServer(id).
Headers reference
Request headers (set automatically by the SDK; set them yourself with raw OpenAI clients):
| Header | Meaning |
|---|---|
X-Punk-App |
Logical application name (groups runs and patterns) |
X-Punk-Agent |
Agent identity within the app |
X-Punk-Subject |
Pseudonymous end-user id — a cache-key safety dimension |
Authorization: Bearer … |
Only when the gateway sets PUNK_API_KEY |
Response headers on /v1/chat/completions:
| Header | Meaning |
|---|---|
x-punk-run-id |
The run this response belongs to — feed it to trace/feedback/runDetail |
x-punk-route |
The route served: live, exact_cache, tool_cache, artifact, blocked, … |
Error behavior
All methods throw Error("Punk API <METHOD> <path> failed: <status> <statusText> — <body>") on non-2xx — except telemetry and caching, which degrade silently: trace appends inside traceTool never fail the tool call, toolCacheCheck degrades to a miss, toolCacheStore is fire-and-forget. The gateway itself fails open: if an optimized route errors, the request falls back to the live provider.
More
- Punk in 30 Minutes
- Agent Activation
- SDK Reference
- OpenAI-Compatible AI Gateway
- OpenRouter
- Vercel AI SDK
- LangChain
- Anthropic SDK
- Claude Code
- Agent Observability & Tool Caching
- examples/ — agent activation, SDK tool loop, OpenAI SDK, Anthropic SDK, OpenRouter, Vercel AI SDK, LangChain, Claude Code, raw curl