@punktechnologies/sdk
v0.2.6
Published
Gateway Agnostic SDK for Punk's hosted AI gateway: OpenAI, Anthropic, OpenRouter, Vercel AI SDK, LangChain, agent tracing, tool caching, and governance.
Maintainers
Keywords
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.
Use Punk when you want the model call to stay familiar, but the runtime to become observable, governed, cache-aware, and cheaper over time.
Start a trial or create a tenant API key: https://punktechnologies.com/signup
npm i @punktechnologies/sdkCreate a runnable starter:
npm create @punktechnologies/punk-agent@latest my-agent
# or
npx @punktechnologies/create-punk-agent@latest 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; it is not a replacement for the hosted Punk service.
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@latest. |
| Smoke test | You want the smallest hosted access check before instrumenting your app. | Run examples/hosted-smoke-test.ts from the repo. |
60-Second Start
1. Set hosted Punk access.
export PUNK_API_KEY=pk_... # from https://app.punktechnologies.com
export PUNK_APP=my-app
export PUNK_AGENT=my-agent
export PUNK_SUBJECT=user-123Provider keys belong in Punk's hosted dashboard/BYOK vault. Your app uses a Punk tenant key.
2. 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());3. Use the SDK when you need the richer runtime surface.
import { Punk } from "@punktechnologies/sdk";
const result = await punk.gateway.chat({
model: "gpt-4o-mini",
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".4. 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
requestTimeoutMs: 30_000, // default for evidence, trace, and control-plane calls
app: "support-triage", // X-Punk-App
agent: "triage-bot", // X-Punk-Agent
subject: "user-123", // X-Punk-Subject (pseudonymous end user)
});The request deadline covers the full response body and can be customized with
requestTimeoutMs up to 15 minutes. Model calls retain their explicit signal
cancellation behavior.
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-mini",
...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 }. Extra headers may add tracing or transport metadata, but authentication and Punk identity headers must use those dedicated options and cannot be overridden generically.
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-mini",
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.finishReason; // "stop" | "length" | "tool_calls" | provider-compatible value
r.raw; // the full OpenAI-shaped response bodyWhen a response carries tool calls, the typed SDK accepts the native OpenAI continuation transcript directly:
const answer = await punk.chat({
model: "gpt-4o-mini",
messages: [
{ role: "user", content: "Look up account acct_42" },
{ role: "assistant", content: null, tool_calls: r.toolCalls },
{
role: "tool",
tool_call_id: r.toolCalls[0]!.id,
content: JSON.stringify({ plan: "enterprise" }),
},
],
});Every 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.
Buffered chat and anthropic.messages calls have a 150-second end-to-end deadline so a stalled gateway cannot leave agent work pending forever. Set bufferedTimeoutMs on an individual call when the workload needs a tighter bound. Streaming calls remain caller-controlled through signal.
Non-2xx responses throw a public PunkApiResponseError. Its status, method, and path fields are always present; runId, route, and retryAfterSeconds are populated when the gateway provides that evidence. This lets callers correlate blocked runs and schedule rate-limit retries without parsing an error message.
import { PunkApiResponseError } from "@punktechnologies/sdk";
try {
await punk.chat({ model: "gpt-4o-mini", messages });
} catch (error) {
if (error instanceof PunkApiResponseError && error.status === 429) {
scheduleRetry(error.retryAfterSeconds ?? 1);
}
throw error;
}Use cacheBypass: true when a call must skip response-cache serving but may refresh Punk's caches. Use cacheNoStore: true for sensitive or one-off calls that must neither consume nor populate exact-response, tool-plan, or semantic-response caches. Both controls stay in request headers and are never forwarded in provider JSON.
Streaming is built in:
for await (const chunk of punk.streamChat({
model: "gpt-4o-mini",
messages: [{ role: "user", content: "Stream a support reply." }]
})) {
if (chunk.type === "delta") process.stdout.write(chunk.content);
if (chunk.type === "done" && chunk.finishReason === "length") handleTruncation();
}anthropic.messages(params) — Anthropic Messages
const msg = await punk.anthropic.messages({
model: "claude-sonnet-4-6",
max_tokens: 256,
betas: ["your-beta-feature-token"], // optional; sent as anthropic-beta, never in JSON
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;
msg.stopReason;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);
if (chunk.type === "done" && chunk.stopReason === "max_tokens") handleTruncation();
}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
schemaFp: "crm-account-v2",
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 can be cached only when Punk resolves one exact current server-owned authority for the authenticated API key used by the SDK cache endpoints. Immutable execution authority includes principal type, app, agent, subject, current grants, connector installation, credential revision, and tool contract. The opaque cache partition layers current local policy state onto that authority, so policy changes cannot reuse earlier entries. Callers cannot provide or override either value. Missing, ambiguous, revoked, externally governed, or non-cacheable authority degrades to a cache miss and no-op store. Persisted Buzz principals participate in managed workflow execution authority, but Buzz workflow execution does not currently perform tool-result cache lookup or storage. The old permissionScopeFp option and positional helper argument remain accepted as deprecated SDK inputs but are ignored and never sent. 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, untraced and uncached. Cache and trace failures never break the tool call.
Write tools can add idempotencyKey: (args) => string to record a stable, non-secret key as structured planned, executed, suppressed, and completion evidence. The resolver must return a trimmed string of at most 120 characters. Punk records the key but does not inject it into the external request; the handler must send the same key to the provider.
feedback(runId, rating, correction?) — close the loop
await punk.feedback(r.runId, 1); // thumbs up (stored as 5/5)
await punk.feedback(r.runId, -1, "should be: billing"); // correctionFeedback feeds the learner: corrections count against pattern stability and artifact confidence.
Use named application metrics when your product can measure a concrete run outcome:
await punk.feedbackMetrics(r.runId, {
metrics: {
resolution: { score: true, reason: "ticket resolved" },
latency_ms: 120,
},
optimize: "max",
tags: { cohort: "support" },
source: "customer-outcome",
evaluator: { name: "support-score", version: "1" },
});The gateway bounds and normalizes metric names, nesting, values, tags, and provenance before appending them to the run's trace ledger. Metric evidence does not bypass replay, shadow, policy, or promotion gates.
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: "[email protected]" });
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.models.list(); // typed gateway model selectors; no model run is created
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.listRuns({ limit: 25, status: "completed" }); // bounded run page + filters/facets
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); // compact routeExplanation read; no full trace download
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, governed LangChain, Claude Code, raw curl
