# keel-sdk

TypeScript SDK for the [Keel](https://keelapi.com) AI governance API.

Keel lets you issue permits before AI calls, enforce policies, track usage, and audit decisions — across any provider.

Keel is built and published by Keel API, Inc.

> **⚠️ Keel is currently in private beta.** You'll need a Keel account and API key to use this SDK.
> [Sign up for early access →](https://dashboard.keelapi.com/signup)

## One-Line Provider Migration

Add Keel governance to your existing AI code with a single import change — no other code modifications required.

**OpenAI:**

```ts
// BEFORE:
import OpenAI from "openai";

// AFTER:
import { OpenAI } from "keel-sdk/providers/openai";
```

**Anthropic:**

```ts
// BEFORE:
import Anthropic from "@anthropic-ai/sdk";

// AFTER:
import { Anthropic } from "keel-sdk/providers/anthropic";
```

**Google (Gemini):**

```ts
// BEFORE:
import { GoogleGenerativeAI } from "@google/generative-ai";

// AFTER:
import { GoogleGenerativeAI } from "keel-sdk/providers/google";
```

**xAI (Grok):**

```ts
// BEFORE:
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.x.ai/v1", apiKey: "..." });

// AFTER:
import { Grok } from "keel-sdk/providers/xai";
const client = new Grok();
```

**Meta (Llama):**

```ts
// BEFORE:
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.llama-api.com", apiKey: "..." });

// AFTER:
import { Llama } from "keel-sdk/providers/meta";
const client = new Llama();
```

The rest of your code stays identical. Under the hood, each call goes through Keel's managed proxy; the API evaluates the permit and records usage in that same request path. If a permit is denied, a `KeelError` with status 403 and code `permit_denied` is thrown.

Set three env vars and you're done:

```bash
export KEEL_BASE_URL="https://api.keelapi.com"
export KEEL_API_KEY="keel_sk_..."
export KEEL_PROJECT_ID="prj_..."
```

Optionally pass a subject identity to attribute proxy activity to a specific user:

```ts
const client = new OpenAI({
  keelSubject: { type: "user", id: "usr_42" },
});
```

Streaming works transparently — pass `stream: true` and iterate as usual.

See [examples/provider-swap.ts](examples/provider-swap.ts) for a full working example.

## Install

```bash
npm install keel-sdk
```

## Setup

```ts
import { KeelClient } from "keel-sdk";

const client = new KeelClient({
  baseUrl: process.env.KEEL_BASE_URL!,
  apiKey: process.env.KEEL_API_KEY!,
});
```

## Request Lifecycle

Every request processed by Keel follows a consistent high-level flow:

- **Evaluate:** identity, policy, and budget constraints are checked
- **Decide:** a permit decision is issued — allow, deny, or constrain
- **Execute:** the provider call occurs only if permitted
- **Record:** usage, cost, and governance events are captured

Requests are only executed if explicitly permitted.

## Permits

Request a permit before making an AI call:

```ts
const permit = await client.permits.create({
  project_id: "proj_123",
  idempotency_key: crypto.randomUUID(),
  subject: { type: "user", id: "usr_123" },
  action: { name: "ai.generate" },
  resource: {
    type: "request",
    id: "req_123",
    attributes: {
      provider: "openai",
      model: "gpt-4o-mini",
      estimated_input_tokens: 200,
      estimated_output_tokens: 500,
    },
  },
});

if (permit.decision === "allow") {
  // proceed with AI call
}
```

### Dry run

```ts
const result = await client.permits.dryRun(permitRequest);
```

### List and get

```ts
const list = await client.permits.list({ project_id: "proj_123", limit: 50 });
const envelopes = await client.permits.list({ view: "envelope", type: "mcp_tool" });
const permit = await client.permits.get("permit_id", { view: "envelope" });
```

### Report usage for permit-first calls

Use this only when your application creates a permit and then calls the provider directly. Do not call it after `client.proxy.*()` or the provider drop-in wrappers; Keel records usage internally for those managed paths. The usage endpoint requires an admin-capable key.

```ts
await client.permits.reportUsage("permit_id", {
  actual_input_tokens: 180,
  actual_output_tokens: 420,
  cost_usd_micros: 1200,
  verification: {
    method: "provider_receipt",
    provider_request_id: "req_provider_123",
    receipt_json: providerReceipt,
  },
});

await client.permits.verifyUsage("permit_id", {
  method: "provider_receipt",
  status: "verified",
  provider_request_id: "req_provider_123",
});
```

### Attestation, evidence, lineage

```ts
await client.permits.attest("permit_id", {
  attestor: "reviewer@example.com",
  attestation_type: "approve",
  evidence_url: "https://example.com/review/123",
});
await client.permits.addEvidence("permit_id", {
  evidence_type: "hash",
  evidence_value: "abc123",
  label: "response_hash",
  attached_by: "audit-system",
});
const evidence = await client.permits.listEvidence("permit_id");
const lineage = await client.permits.lineage("permit_id");
const bundle = await client.permits.bundle("permit_id");
```

## Workflows

Declare a workflow up front; Keel enforces the cap and produces a signed audit trail.

Use `runInWorkflow(id, fn)` to bind every SDK request in the callback to the
declared workflow:

```ts
import { runInWorkflow } from "keel-sdk";

const declaration = await client.workflows.declare(
  "support_triage_2026_05_13",
  {
    expected_calls: 10,
    max_calls: 15,
    expected_model: "gpt-4o-mini",
    expected_input_tokens_per_call: 800,
    expected_output_tokens_per_call: 300,
  },
);

await runInWorkflow(declaration.workflow_id, async () => {
  await client.executions.create({
    operation: "generate.text",
    messages: [{ role: "user", content: "Summarize these tickets." }],
    routing: { provider: "openai", model: "gpt-4o-mini" },
  });
});

await client.workflows.complete(declaration.workflow_id);
```

If you are on TypeScript 5.2 or newer, `workflow(id)` also works as a
disposable context:

```ts
import { workflow } from "keel-sdk";

{
  using activeWorkflow = workflow("support_triage_2026_05_13");

  await client.executions.create({
    operation: "generate.text",
    messages: [{ role: "user", content: "Draft a customer reply." }],
    routing: { provider: "openai", model: "gpt-4o-mini" },
  });
}
```

The SDK middleware auto-injects `X-Keel-Workflow-Id` while a workflow context is
active. Keep one workflow context active per call chain; nested workflow
contexts throw in v1.

Use `client.workflows.amend(...)` to adjust an active declaration with
`if_match_version`, and `client.workflows.complete(...)` when the run is done.
A fuller runnable sample lives in
[examples/workflows/declareAndRun.ts](examples/workflows/declareAndRun.ts).
See the [API reference](https://docs.keelapi.com/api-reference) for the full
workflow request and response contracts.

Workflows are available on Business+ plans.

## Compliance exports

Create and fetch signed export jobs for audit and verifier workflows:

```ts
const exportJob = await client.complianceExports.create(
  {
    export_type: "full_audit",
    format: "jsonl",
    filters: { include_replay_evidence: true },
  },
  { include_chain_entries: true },
);

const exports = await client.complianceExports.list();
const readyExport = await client.complianceExports.get(exportJob.export_id);
```

## Executions

Run a model synchronously:

```ts
const result = await client.executions.create({
  operation: "generate.text",
  messages: [{ role: "user", content: "Summarize this document." }],
  routing: { provider: "openai", model: "gpt-4o-mini" },
});
```

Stream tokens as they arrive:

```ts
for await (const event of client.executions.stream({
  operation: "generate.text",
  messages: [{ role: "user", content: "Write a poem." }],
  routing: { provider: "openai", model: "gpt-4o-mini" },
})) {
  if (event.event_type === "content.delta") process.stdout.write(event.data.delta.text);
  if (event.event_type === "done") console.log("\nFinished.");
}
```

## Execute (unified)

```ts
const result = await client.execute.run({
  model: "gpt-4o-mini",
  input: { text: "Translate to Spanish: Hello world" },
  provider: "openai",
});
```

## Proxy

Pass provider-native requests through to providers with Keel governance applied:

```ts
const response = await client.proxy.openai({
  model: "gpt-4o-mini",
  messages: [{ role: "user", content: "Hello" }],
});

// Also: client.proxy.anthropic(), .google(), .xai(), .meta()
```

Proxy routes are intentionally provider-specific. OpenAI and Anthropic expose
the broadest public routing controls; Google, xAI, and Meta proxy routing is
narrower and follows the API route contract. Use `client.execute` or
`client.executions` when you need provider-neutral routing or cross-provider
fallback.

## Jobs

Submit async jobs and poll for results:

```ts
const job = await client.jobs.create({
  permit: { /* PermitRequest */ },
  provider_payload: {
    messages: [{ role: "user", content: "Analyze this dataset." }],
  },
  metadata: {},
});

const status = await client.jobs.get(job.job_id);
// status.status: "submitted" | "queued" | "processing" | "completed" | "failed"
```

## API Keys

```ts
const key = await client.apiKeys.create();
const keys = await client.apiKeys.list();
await client.apiKeys.revoke(key.id);
```

## Request Timeline

```ts
const timeline = await client.requests.timeline("request_id");
```

## Error Handling

```ts
import { KeelError } from "keel-sdk";

try {
  await client.permits.create(request);
} catch (err) {
  if (err instanceof KeelError) {
    console.error(err.status);  // HTTP status code
    console.error(err.code);    // e.g. "permit_denied"
    console.error(err.message); // human-readable message
    console.error(err.field);   // field that caused the error, if any
  }
}
```

## Automatic Retry

Enable automatic retry with exponential backoff for transient errors:

```ts
import { KeelClient, DEFAULT_RETRY_CONFIG } from "keel-sdk";

const client = new KeelClient({
  baseUrl: process.env.KEEL_BASE_URL!,
  apiKey: process.env.KEEL_API_KEY!,
  retryConfig: DEFAULT_RETRY_CONFIG,
});
```

The default configuration retries up to 3 times with exponential backoff (500ms initial delay, 2x multiplier, 30s max) on status codes 408, 429, 500, 502, 503, and 504.

Customize the retry behavior:

```ts
import { KeelClient } from "keel-sdk";
import type { RetryConfig } from "keel-sdk";

const retryConfig: RetryConfig = {
  maxRetries: 5,
  initialDelayMs: 1000,
  maxDelayMs: 60_000,
  backoffMultiplier: 3.0,
  retryableStatusCodes: [429, 500, 502, 503, 504],
};

const client = new KeelClient({
  baseUrl: process.env.KEEL_BASE_URL!,
  apiKey: process.env.KEEL_API_KEY!,
  retryConfig,
});
```

Retry applies to all non-streaming requests. Streaming requests (`executions.stream()`) are never retried. When the server sends a `Retry-After` header, the SDK respects it as a minimum delay before the next attempt.

### Rate-Limit Throttling (HTTP 429)

When the Keel API throttles a request it returns HTTP 429 with a `Retry-After` header. The SDK automatically retries the request (up to `maxRetries` times, default 3) after the server-specified delay. If the header is absent, the SDK falls back to the `retry_after_seconds` value in the response body (default 30 s).

After all retries are exhausted a `ThrottledError` is thrown:

```ts
import { ThrottledError } from "keel-sdk";

try {
  await client.permits.create(request);
} catch (err) {
  if (err instanceof ThrottledError) {
    console.log(err.retryAfterSeconds); // seconds the server asked to wait
    console.log(err.reasonCode);        // e.g. "budget.rate_limit_throttled"
    console.log(err.permitId);          // permit ID, if available
  }
}
```

`ThrottledError` extends `KeelError`, so existing `catch (err instanceof KeelError)` blocks continue to work. Only HTTP 429 produces this error — policy denials (403) raise a plain `KeelError` and are never retried.

## Per-Request Timeout

Override the global timeout for individual requests by passing a `RequestOptions` object:

```ts
// Global timeout is 30 seconds (default)
const client = new KeelClient({
  baseUrl: process.env.KEEL_BASE_URL!,
  apiKey: process.env.KEEL_API_KEY!,
});

// This specific permit creation gets 30 seconds
const permit = await client.permits.create(permitRequest, { timeoutMs: 30_000 });

// Streaming calls can use a longer timeout
const stream = await client.executions.stream(
  { permit_id: permit.permit_id, operation: "generate.text", messages },
  { timeoutMs: 120_000 },
);

// Per-request timeout works on all sub-clients:
// client.permits, client.executions, client.execute, client.proxy, client.jobs, client.requests
```

For streaming, the effective timeout is `timeoutMs * 6` (same multiplier as the global timeout). When no per-request timeout is provided, the global `timeoutMs` from client options is used.

## Freshness Headers

`X-Keel-Timestamp` and `X-Keel-Nonce` are sent on every request **by default**,
including from the provider wrappers. Hosted Keel and any deployment running
with a non-dev `APP_ENV` require them on execution routes and on
`POST /v1/permits`; without them those routes return `401 request_not_fresh`.
Deployments that do not enforce freshness ignore the headers.

A fresh nonce is generated per attempt, so automatic retries are not rejected as
replays.

```ts
// Default: headers are sent.
const client = new KeelClient({
  baseUrl: process.env.KEEL_BASE_URL!,
  apiKey: process.env.KEEL_API_KEY!,
});

// Supply your own sources, or opt out entirely.
const custom = new KeelClient({
  baseUrl: process.env.KEEL_BASE_URL!,
  apiKey: process.env.KEEL_API_KEY!,
  requestFreshness: { enabled: true, nonceFn: () => crypto.randomUUID() },
});

const optedOut = new KeelClient({
  baseUrl: process.env.KEEL_BASE_URL!,
  apiKey: process.env.KEEL_API_KEY!,
  requestFreshness: false,
});
```

## Governance Metadata

Every response body carries the decision metadata Keel stamps in `x-keel-*`
response headers, so you can get the permit id for a specific call without a
second request. The metadata is attached as a non-enumerable `_keel` property,
so serialization and equality are unchanged.

```ts
import { keelGovernanceOf } from "keel-sdk";

const response = await client.chat.completions.create({ ... });

response.choices[0].message.content;   // unchanged
const governance = keelGovernanceOf(response);
governance?.permitId;                  // the Permit for this call
governance?.decision;                  // allow | deny | challenge | throttle
governance?.requestId;                 // quote this in support requests
```

## License

MIT
