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, and Claude Code through Punk's hosted gateway.
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 Punk's hosted gateway and control plane.
Install
npm i @punktechnologies/sdk
# or
bun add @punktechnologies/sdkZero runtime dependencies. Requires Node 18+ or Bun, Punk hosted gateway access, and a tenant API key from the dashboard. new Punk() reads PUNK_BASE_URL, PUNK_API_KEY, PUNK_APP, PUNK_AGENT, and PUNK_SUBJECT; explicit constructor options still win. The default baseUrl is https://app.punktechnologies.com.
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 https://app.punktechnologies.com. 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: "https://app.punktechnologies.com", // default
apiKey: process.env.PUNK_API_KEY, // tenant API key from the hosted dashboard
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 … |
Hosted Punk tenant 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