# 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 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 automatically requests a governance permit, proxies the request through Keel, and reports token usage. 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 to attribute usage 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!,
});
```

## 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 permit = await client.permits.get("permit_id");
```

### Report usage

```ts
await client.permits.reportUsage("permit_id", {
  actual_input_tokens: 180,
  actual_output_tokens: 420,
});
```

### 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");
```

## 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 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()
```

## 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.

## 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

For replay-protected endpoints, enable freshness headers:

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

This adds `X-Keel-Timestamp` and `X-Keel-Nonce` to every request.

## License

MIT
