{"_id":"@doppel-llm-test/sdk","_rev":"3-00fb5026d28d1bb758a102a1a33c6ccb","name":"@doppel-llm-test/sdk","dist-tags":{"latest":"1.2.2"},"versions":{"1.1.0":{"name":"@doppel-llm-test/sdk","version":"1.1.0","keywords":["doppel","sdk","api","llm","shadow","testing"],"license":"MIT","_id":"@doppel-llm-test/sdk@1.1.0","maintainers":[{"name":"doppel-test","email":"doppel.impact.test@gmail.com"}],"dist":{"shasum":"de90e5b0f5d06591d9a8228b06baf401deb1452b","tarball":"https://registry.npmjs.org/@doppel-llm-test/sdk/-/sdk-1.1.0.tgz","fileCount":8,"integrity":"sha512-1cLOK+Z8TFuHEb3MMrt6sY8g5bWMcRe2Ah9VJ9Tf+6bZoDlZ964jVmROV/D2iamw2YQ7rljgW2xLOdnILCjr9A==","signatures":[{"sig":"MEQCICaH37a9+d/vGZsEibqUz+vAlKFazURERJs1khIAAvwdAiBhDDRLRhEknZEWXyp+6HvME0DlS0BL79iQ94fJMVN48A==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":40302},"main":"./dist/index.cjs","type":"module","_from":"file:doppel-llm-test-sdk-1.1.0.tgz","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"dev":"tsup --watch","lint":"eslint src","test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"doppel-test","email":"doppel.impact.test@gmail.com"},"_resolved":"/tmp/35e862c5a19851bc909b961286ce5017/doppel-llm-test-sdk-1.1.0.tgz","_integrity":"sha512-1cLOK+Z8TFuHEb3MMrt6sY8g5bWMcRe2Ah9VJ9Tf+6bZoDlZ964jVmROV/D2iamw2YQ7rljgW2xLOdnILCjr9A==","_npmVersion":"10.8.2","description":"Official Doppel JavaScript / TypeScript SDK","directories":{},"_nodeVersion":"20.20.2","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^4.0.0","typescript":"^6.0.0"},"peerDependencies":{"openai":">=4.0.0","@anthropic-ai/sdk":">=0.20.0"},"peerDependenciesMeta":{"openai":{"optional":true},"@anthropic-ai/sdk":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/sdk_1.1.0_1780244977659_0.13364662321280485","host":"s3://npm-registry-packages-npm-production"}},"1.2.1":{"name":"@doppel-llm-test/sdk","version":"1.2.1","keywords":["doppel","sdk","api","llm","shadow","testing"],"license":"MIT","_id":"@doppel-llm-test/sdk@1.2.1","maintainers":[{"name":"doppel-test","email":"doppel.impact.test@gmail.com"}],"dist":{"shasum":"813b6769e0cdbe4ceb1f5990b9cbad3784c4956e","tarball":"https://registry.npmjs.org/@doppel-llm-test/sdk/-/sdk-1.2.1.tgz","fileCount":8,"integrity":"sha512-Kkzx4r+c2DgJe2t1xGzTxYYdDVijVI0yLpqgUFOvWaYk81TeQsQ8yuwP+BTtsQzGXe0QqbXCKgjBQ7HBTfV+yg==","signatures":[{"sig":"MEUCIH1HGrh0tQQcd0G4NE6VDBSLD8JTdzauYuhCbEG4TXzqAiEA4DjJchoMj9WojuCrydZu6xXVcdp7AJUuia15mC8tL60=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":161928},"main":"./dist/index.cjs","type":"module","_from":"file:doppel-llm-test-sdk-1.2.1.tgz","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=22.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"dev":"tsup --watch","lint":"eslint src","test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"doppel-test","email":"doppel.impact.test@gmail.com"},"_resolved":"/tmp/2dc5093126fabce46ff90630c0f2119f/doppel-llm-test-sdk-1.2.1.tgz","_integrity":"sha512-Kkzx4r+c2DgJe2t1xGzTxYYdDVijVI0yLpqgUFOvWaYk81TeQsQ8yuwP+BTtsQzGXe0QqbXCKgjBQ7HBTfV+yg==","_npmVersion":"10.8.2","description":"Official Doppel JavaScript / TypeScript SDK","directories":{},"_nodeVersion":"20.20.2","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^4.0.0","typescript":"^6.0.0","@types/node":"^22.19.21"},"peerDependencies":{"openai":">=4.0.0","@google/genai":">=0.3.0","@anthropic-ai/sdk":">=0.20.0","@google/generative-ai":">=0.20.0"},"peerDependenciesMeta":{"openai":{"optional":true},"@google/genai":{"optional":true},"@anthropic-ai/sdk":{"optional":true},"@google/generative-ai":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/sdk_1.2.1_1781719465052_0.017338998852876264","host":"s3://npm-registry-packages-npm-production"}},"1.2.2":{"name":"@doppel-llm-test/sdk","version":"1.2.2","description":"Official Doppel JavaScript / TypeScript SDK","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"keywords":["doppel","sdk","api","llm","shadow","testing"],"license":"MIT","engines":{"node":">=22.0.0"},"peerDependencies":{"@anthropic-ai/sdk":">=0.20.0","@google/genai":">=0.3.0","@google/generative-ai":">=0.20.0","openai":">=4.0.0"},"peerDependenciesMeta":{"openai":{"optional":true},"@anthropic-ai/sdk":{"optional":true},"@google/genai":{"optional":true},"@google/generative-ai":{"optional":true}},"devDependencies":{"@types/node":"^22.19.21","tsup":"^8.0.0","typescript":"^6.0.0","vitest":"^4.0.0"},"scripts":{"dev":"tsup --watch","build":"tsup","test":"vitest run","test:watch":"vitest","typecheck":"tsc --noEmit","lint":"eslint src"},"_id":"@doppel-llm-test/sdk@1.2.2","_integrity":"sha512-M/ksGtKmgo2aaENTS6lfBbxi5MDEktSPH9mW6I8T04MqeFsmvZYp0/04qETJnBvY7PCXmkgOnO9lTVIxJsIlwQ==","_resolved":"/tmp/734103c809d4189529616cd6a61efa5d/doppel-llm-test-sdk-1.2.2.tgz","_from":"file:doppel-llm-test-sdk-1.2.2.tgz","_nodeVersion":"20.20.2","_npmVersion":"10.8.2","dist":{"integrity":"sha512-M/ksGtKmgo2aaENTS6lfBbxi5MDEktSPH9mW6I8T04MqeFsmvZYp0/04qETJnBvY7PCXmkgOnO9lTVIxJsIlwQ==","shasum":"74a4f1455305d14355203314db645c26d6e14756","tarball":"https://registry.npmjs.org/@doppel-llm-test/sdk/-/sdk-1.2.2.tgz","fileCount":8,"unpackedSize":163614,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDCvyfdmbZaEdkUN49eKZW9pjz1Lxb217MvfWIGNLmcSwIhANFNaH0w80WERxB7WnGJTU2Qrw5YassTLlr1s85p1qaE"}]},"_npmUser":{"name":"doppel-test","email":"doppel.impact.test@gmail.com"},"directories":{},"maintainers":[{"name":"doppel-test","email":"doppel.impact.test@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/sdk_1.2.2_1781860181466_0.9440048301646751"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-31T16:29:37.540Z","modified":"2026-06-19T09:09:41.738Z","1.1.0":"2026-05-31T16:29:37.798Z","1.2.1":"2026-06-17T18:04:25.199Z","1.2.2":"2026-06-19T09:09:41.598Z"},"license":"MIT","keywords":["doppel","sdk","api","llm","shadow","testing"],"description":"Official Doppel JavaScript / TypeScript SDK","maintainers":[{"name":"doppel-test","email":"doppel.impact.test@gmail.com"}],"readme":"# @doppel-llm-test/sdk — JavaScript / TypeScript\n\nOfficial Doppel SDK for Node.js. Transparently intercepts OpenAI, Anthropic, Google (Gemini) and OpenRouter LLM calls to capture run data — prompt, response, tokens, latency — and send it to your Doppel server, which then runs the same prompt through a **shadow model** for comparison.\n\n## Requirements\n\n- Node.js >= 22\n- One or more provider SDKs (whichever you use):\n  - `openai` >= 4.0.0\n  - `@anthropic-ai/sdk` >= 0.20.0\n  - `@google/genai` >= 0.3.0 **or** `@google/generative-ai` >= 0.20.0\n  - OpenRouter needs no extra dependency — it uses the `openai` SDK pointed at `https://openrouter.ai/api/v1`\n\n---\n\n## Installation\n\n```bash\nnpm install @doppel-llm-test/sdk\n# or\npnpm add @doppel-llm-test/sdk\n```\n\nInstall the LLM provider SDK(s) you use:\n\n```bash\nnpm install openai                 # if using OpenAI\nnpm install @anthropic-ai/sdk      # if using Anthropic\nnpm install @google/genai          # if using Google Gemini (new SDK)\n# or the legacy Google SDK:\nnpm install @google/generative-ai  # if using Google Gemini (legacy SDK)\n```\n\n---\n\n## Quick start\n\nThe recommended setup keeps secrets out of your code by reading them from the\nenvironment. Set your API key once:\n\n```bash\nexport DOPPEL_API_KEY=\"dp_live_sk_...\"\n```\n\n```ts\nimport { DoppelClient } from '@doppel-llm-test/sdk'\nimport OpenAI from 'openai'\n\n// apiKey is read from DOPPEL_API_KEY; serverUrl defaults to https://api.doppel.in\nconst doppel = new DoppelClient()\n\nconst openai = doppel.wrapOpenAI(new OpenAI())\n\n// Use exactly as before — nothing changes in your app code\nconst response = await openai.chat.completions.create({\n  model: 'gpt-4o',\n  messages: [{ role: 'user', content: 'Hello!' }],\n})\n```\n\n---\n\n## Configuration\n\n`new DoppelClient(options?)` accepts the options below. Every option can also be\nsupplied through an environment variable, so in most projects you can call\n`new DoppelClient()` with no arguments at all.\n\n| Option | Type | Required | Resolved from (in priority order) | Description |\n|---|---|---|---|---|\n| `apiKey` | `string` | **Yes** | `apiKey` option ▸ `DOPPEL_API_KEY` env | Your Doppel project API key. |\n| `shadowModel` | `string` | No | `shadowModel` option | Model used to replay captured calls (e.g. `'gpt-4o-mini'`). See [Shadow model](#shadow-model) below. |\n| `serverUrl` | `string` | No | `serverUrl` option ▸ `DOPPEL_SERVER_URL` env ▸ `https://api.doppel.in` | Doppel backend base URL. |\n| `debug` | `boolean` | No | `debug` option ▸ `DOPPEL_DEBUG` env | Verbose `[doppel-sdk]` logs per captured run. Off by default. Delivery failures are always warned about regardless. |\n\nIf no API key can be resolved from either the option or the environment, the\nconstructor throws immediately with a descriptive error.\n\n### API key\n\nRequired. Resolved in the following order:\n\n1. **Recommended — environment variable.** Set `DOPPEL_API_KEY` and construct the\n   client with no arguments:\n\n   ```bash\n   export DOPPEL_API_KEY=\"dp_live_sk_...\"\n   ```\n\n   ```ts\n   const doppel = new DoppelClient()\n   ```\n\n2. **Inline option.** Pass it explicitly (useful when you manage secrets yourself):\n\n   ```ts\n   // Read it from the environment yourself...\n   const doppel = new DoppelClient({ apiKey: process.env.DOPPEL_API_KEY })\n\n   // ...or pass a literal value\n   const doppel = new DoppelClient({ apiKey: 'dp_live_sk_...' })\n   ```\n\n### Shadow model\n\nCompletely optional.\n\n- **Provided** — captured calls run against the shadow model immediately, with no\n  further action needed:\n\n  ```ts\n  const doppel = new DoppelClient({ shadowModel: 'gpt-4o-mini' })\n  ```\n\n- **Omitted** — calls are still captured, but stay pending until you pick a shadow\n  model for them in the [Doppel dashboard](https://api.doppel.in). They only run\n  once a model is selected there.\n\n### Server URL\n\nOptional. Resolved in the following order:\n\n1. **Default — production.** When nothing is configured, the SDK targets\n   `https://api.doppel.in`.\n\n2. **Recommended — environment variable.** Set `DOPPEL_SERVER_URL` to point the SDK\n   at a different environment (staging, QA, local) without touching code:\n\n   ```bash\n   export DOPPEL_SERVER_URL=\"https://stage.doppel.in\"\n   ```\n\n3. **Inline option.** Override per client instance:\n\n   ```ts\n   // e.g. https://<HOST-URL>-stage  or  https://<HOST-URL>-qa\n   const doppel = new DoppelClient({ serverUrl: 'https://<HOST-URL>-[stage, QA]' })\n   ```\n\n> **Environment variables at a glance**\n>\n> | Variable | Purpose | Default |\n> |---|---|---|\n> | `DOPPEL_API_KEY` | Project API key (required) | — |\n> | `DOPPEL_SERVER_URL` | Doppel backend base URL | `https://api.doppel.in` |\n> | `DOPPEL_DEBUG` | Verbose logging (`1`/`true`) | `false` |\n\n---\n\n## Usage\n\n### Wrapping OpenAI\n\n```ts\nimport { DoppelClient } from '@doppel-llm-test/sdk'\nimport OpenAI from 'openai'\n\n// Reads DOPPEL_API_KEY from the environment; shadowModel is optional\nconst doppel = new DoppelClient({ shadowModel: 'gpt-4o-mini' })\n\n// Pass your existing OpenAI instance — it is modified in-place and returned\nconst openai = doppel.wrapOpenAI(new OpenAI({ apiKey: process.env.OPENAI_API_KEY }))\n\nconst response = await openai.chat.completions.create({\n  model: 'gpt-4o',\n  messages: [\n    { role: 'system', content: 'You are a helpful assistant.' },\n    { role: 'user', content: 'Summarise the history of the internet.' },\n  ],\n  temperature: 0.7,\n})\n\nconsole.log(response.choices[0].message.content)\n```\n\n**What gets captured per call:**\n\n| Field | Description |\n|---|---|\n| `model` | Primary model used |\n| `shadowModel` | Shadow model configured |\n| `messages` | Full message array sent |\n| `temperature` | Temperature parameter (if provided) |\n| `answer` | First choice text from the response |\n| `promptTokens` | Tokens used for the prompt |\n| `completionTokens` | Tokens used for the completion |\n| `totalTokens` | Total tokens used |\n| `durationMs` | End-to-end latency in milliseconds |\n| `timestamp` | ISO timestamp of the call |\n\n---\n\n### Wrapping Anthropic\n\n```ts\nimport { DoppelClient } from '@doppel-llm-test/sdk'\nimport Anthropic from '@anthropic-ai/sdk'\n\n// Reads DOPPEL_API_KEY from the environment; shadowModel is optional\nconst doppel = new DoppelClient({ shadowModel: 'claude-3-haiku-20240307' })\n\nconst anthropic = doppel.wrapAnthropic(new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }))\n\nconst response = await anthropic.messages.create({\n  model: 'claude-3-5-sonnet-20241022',\n  max_tokens: 1024,\n  messages: [{ role: 'user', content: 'Explain quantum entanglement simply.' }],\n})\n\nconsole.log(response.content[0].text)\n```\n\n**What gets captured per call:**\n\n| Field | Description |\n|---|---|\n| `model` | Primary model used |\n| `shadowModel` | Shadow model configured |\n| `prompt` | Concatenated message content |\n| `primaryResponse.output` | Full text from all `text` content blocks |\n| `primaryResponse.latencyMs` | End-to-end latency in milliseconds |\n| `primaryResponse.tokens` | Total tokens (input + output) |\n\n---\n\n### Wrapping Google (Gemini)\n\n`wrapGoogle()` supports **both** Google SDKs — the client shape is auto-detected,\nso you don't pick a variant.\n\n**New SDK — `@google/genai`:**\n\n```ts\nimport { DoppelClient } from '@doppel-llm-test/sdk'\nimport { GoogleGenAI } from '@google/genai'\n\nconst doppel = new DoppelClient({ shadowModel: 'gemini-1.5-flash' })\n\nconst ai = doppel.wrapGoogle(new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }))\n\nconst response = await ai.models.generateContent({\n  model: 'gemini-2.5-flash',\n  contents: 'Explain quantum entanglement simply.',\n})\n\nconsole.log(response.text)\n```\n\n**Legacy SDK — `@google/generative-ai`:**\n\n```ts\nimport { DoppelClient } from '@doppel-llm-test/sdk'\nimport { GoogleGenerativeAI } from '@google/generative-ai'\n\nconst doppel = new DoppelClient({ shadowModel: 'gemini-1.5-flash' })\n\n// Wrap the top-level client — every model it returns is intercepted\nconst genAI = doppel.wrapGoogle(new GoogleGenerativeAI(process.env.GEMINI_API_KEY!))\n\nconst model = genAI.getGenerativeModel({ model: 'gemini-1.5-pro' })\nconst result = await model.generateContent('Explain quantum entanglement simply.')\n\nconsole.log(result.response.text())\n```\n\n**What gets captured per call:**\n\n| Field | Description |\n|---|---|\n| `provider` | Always `'google'` (distinguishes Gemini runs server-side) |\n| `model` | Primary model used |\n| `shadowModel` | Shadow model configured |\n| `prompt` | Flattened `contents` (string / parts / content blocks) |\n| `answer` | Generated text output |\n| `promptTokens` / `completionTokens` / `totalTokens` | Token counts from `usageMetadata` |\n| `durationMs` | End-to-end latency in milliseconds |\n\n---\n\n### Wrapping OpenRouter\n\nOpenRouter is OpenAI-wire-compatible, so you call it with the `openai` SDK pointed\nat the OpenRouter base URL. `wrapOpenRouter()` attributes each run to the **model\nauthor** (derived from the namespaced model id — `anthropic`, `meta-llama`, `x-ai`,\n…), passes namespaced model ids through unchanged, and captures OpenRouter's\n**real USD cost**.\n\n> **Note on keys:** Doppel only needs `DOPPEL_API_KEY`. The `apiKey` below is your\n> **own OpenRouter key**, used by your `openai` client to make the actual call —\n> the same key you'd use without Doppel. The SDK never reads or sends it; it only\n> wraps the client you already build. (The `openai` SDK falls back to\n> `OPENAI_API_KEY`, not `OPENROUTER_API_KEY`, so it must be passed explicitly here.)\n\n```ts\nimport { DoppelClient } from '@doppel-llm-test/sdk'\nimport OpenAI from 'openai'\n\nconst doppel = new DoppelClient({ shadowModel: 'openai/gpt-4o-mini' })\n\n// This is your OpenRouter client — your key, your call. Doppel just wraps it.\nconst openrouter = doppel.wrapOpenRouter(new OpenAI({\n  baseURL: 'https://openrouter.ai/api/v1',\n  apiKey: process.env.OPENROUTER_API_KEY,\n}))\n\nconst response = await openrouter.chat.completions.create({\n  model: 'anthropic/claude-opus-4.8',\n  messages: [{ role: 'user', content: 'Explain quantum entanglement simply.' }],\n  // Optional: enable OpenRouter usage accounting to capture real USD cost\n  usage: { include: true },\n} as any)\n\nconsole.log(response.choices[0].message.content)\n```\n\n> The interceptor **does not** modify your request. Cost is captured only when\n> OpenRouter returns `usage.cost` — i.e. when you enable usage accounting\n> (`usage: { include: true }`) yourself.\n\n**What gets captured per call:**\n\n| Field | Description |\n|---|---|\n| `provider` | Transport marker, always `'openrouter'` |\n| `vendor` | **Model author** derived from the namespace (e.g. `anthropic`, `meta-llama`, `x-ai`). The run is attributed to this in Doppel. |\n| `model` | Fully-qualified model id (e.g. `anthropic/claude-opus-4.8`) |\n| `shadowModel` | Shadow model configured |\n| `messages` | Full message array sent |\n| `answer` | First choice text from the response |\n| `promptTokens` / `completionTokens` / `totalTokens` | Token counts |\n| `durationMs` | End-to-end latency in milliseconds |\n| `cost` | Real USD cost from `usage.cost` — present only when usage accounting is enabled |\n\n> **One interceptor covers every OpenRouter model.** Because every OpenRouter model\n> id is namespaced as `<author>/<model>`, the SDK derives the **author** (`anthropic`,\n> `meta-llama`, `google`, `x-ai`, `qwen`, `deepseek`, … any of OpenRouter's authors)\n> and Doppel attributes the run to it. You can then filter/group runs by author —\n> no per-vendor SDK or configuration needed.\n\n#### Use fully-qualified model ids\n\nAlways use **fully-qualified** OpenRouter ids — `<author>/<model>` — for both the\nprimary `model` **and** the `shadowModel`. OpenRouter *does* auto-resolve some\nwell-known bare names (`gpt-4o-mini` works), but a bare id is still a problem for two\nreasons:\n\n1. **Attribution is lost.** The SDK reads the author from the `/` in the id. A bare\n   `gpt-4o-mini` has no namespace, so the run is attributed to `openrouter` instead of\n   `openai`, and you lose per-author grouping.\n2. **Replay is fragile.** A bare id is replayed as `openrouter/<model>`, which is\n   invalid (`400 — not a valid model ID`). Doppel infers the author for common\n   families as a safety net, but an unrecognised bare name will still fail on replay.\n\nSo qualify both ids:\n\n```ts\n// ✅ correct — both are fully qualified\nconst doppel = new DoppelClient({ shadowModel: 'openai/gpt-4o-mini' })\nawait openrouter.chat.completions.create({\n  model: 'anthropic/claude-opus-4.8',\n  messages,\n})\n\n// ❌ wrong — bare shadow id, invalid for OpenRouter\nconst doppel = new DoppelClient({ shadowModel: 'gpt-4o-mini' })\n```\n\n#### Ready-to-use model ids\n\nA starting set of fully-qualified ids you can copy. The authoritative, always-current\nlist is OpenRouter's catalog — `GET https://openrouter.ai/api/v1/models` (or\n[openrouter.ai/models](https://openrouter.ai/models)) — model availability and version\nsuffixes change over time.\n\n```text\n# OpenAI\nopenai/gpt-4o-mini\nopenai/gpt-5.4-mini\nopenai/gpt-5.5\n\n# Anthropic\nanthropic/claude-opus-4.8\nanthropic/claude-opus-4.8-fast\nanthropic/claude-opus-4.7\n\n# Google\ngoogle/gemini-3.5-flash\ngoogle/gemini-3.1-flash-lite\ngoogle/gemma-4-31b-it\n\n# DeepSeek\ndeepseek/deepseek-v4-pro\ndeepseek/deepseek-v4-flash\n\n# Qwen (Alibaba)\nqwen/qwen3.7-max\nqwen/qwen3.6-flash\nqwen/qwen3.6-27b\n\n# xAI\nx-ai/grok-4.3\nx-ai/grok-4.20\n\n# Mistral\nmistralai/mistral-medium-3-5\nmistralai/mistral-small-2603\n\n# Others\nmoonshotai/kimi-k2.6\nminimax/minimax-m2.7\nz-ai/glm-5.1\nxiaomi/mimo-v2.5\nibm-granite/granite-4.1-8b\ntencent/hy3-preview\narcee-ai/trinity-large-thinking\n\n# Free tier (rate-limited)\nmeta-llama/llama-3.3-70b-instruct:free\nnvidia/nemotron-3-ultra-550b-a55b:free\nnex-agi/nex-n2-pro:free\n```\n\n---\n\n### Passing session metadata (OpenAI)\n\nAttach optional metadata via the `_impactMeta` field. It is extracted and forwarded to Doppel — it is **never sent to OpenAI**:\n\n```ts\nconst response = await openai.chat.completions.create({\n  model: 'gpt-4o',\n  messages: [{ role: 'user', content: userQuestion }],\n  _impactMeta: {\n    sessionId: 'user-abc-123',   // group runs by session\n    question: userQuestion,       // the raw user question\n    pdfCharsSent: 4200,           // any custom numeric/string data\n  },\n} as any)\n```\n\n| Metadata field | Type | Description |\n|---|---|---|\n| `sessionId` | `string` | Groups multiple runs under one session in the dashboard |\n| `question` | `string` | The original user question (useful if messages contains extra context) |\n| `pdfCharsSent` | `number` | Character count of any document context injected into the prompt |\n\n---\n\n### Standalone wrap functions\n\nUse the low-level functions directly if you prefer not to instantiate `DoppelClient`:\n\n```ts\nimport { wrapOpenAI, wrapAnthropic, wrapGoogle, wrapOpenRouter } from '@doppel-llm-test/sdk'\nimport type { ImpactConfig } from '@doppel-llm-test/sdk'\n\n// The standalone functions take a fully-resolved config (no env-var fallback).\nconst config: ImpactConfig = {\n  apiKey: process.env.DOPPEL_API_KEY!,\n  shadowModel: 'gpt-4o-mini',         // optional\n  serverUrl: 'https://api.doppel.in', // optional, defaults to production\n  debug: false,                       // optional\n}\n\nconst openai = wrapOpenAI(new OpenAI(), config)\nconst anthropic = wrapAnthropic(new Anthropic(), config)\nconst ai = wrapGoogle(new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }), config)\nconst openrouter = wrapOpenRouter(new OpenAI({ baseURL: 'https://openrouter.ai/api/v1', apiKey: process.env.OPENROUTER_API_KEY }), config)\n```\n\n---\n\n## How it works\n\n```\nYour app\n  │\n  ▼\nopenai.chat.completions.create(params)   ← intercepted by wrapOpenAI\n  │\n  ├─► Original OpenAI API call (unchanged) ──► response returned to your app\n  │\n  └─► Fire-and-forget POST /runs to Doppel server\n            │\n            └─► Server runs same prompt through shadowModel\n                         │\n                         └─► Results available in Doppel dashboard\n```\n\nKey properties:\n- **Zero added latency** — the Doppel POST is sent after your response is received, never before\n- **Never throws** — capture and network errors are caught and logged; your call always returns the original SDK response\n- **Pass-through** — the original SDK response is always returned unchanged\n- **Idempotent** — wrapping the same client more than once is a no-op; runs are never double-sent\n- **No extra dependencies** — provider SDKs (openai / @anthropic-ai/sdk / @google/genai / @google/generative-ai) are optional peer dependencies\n\n---\n\n## Streaming\n\nStreaming calls (`stream: true`) are **passed through untouched and not captured** —\nthe streamed response is returned to your app exactly as the provider sent it.\nCapturing a stream would require buffering it, so it is intentionally skipped for\nOpenAI, Anthropic and Google. For Gemini, streaming goes through a separate method\n(`generateContentStream`) which is left untouched; only `generateContent` is\ncaptured. Non-streaming calls are captured as normal.\n\n---\n\n## Serverless & short-lived processes\n\nRuns are delivered fire-and-forget, so in environments that may freeze or exit\nimmediately after returning a response (AWS Lambda, edge functions, CLI scripts),\nflush pending runs before the process ends:\n\n```ts\nconst response = await openai.chat.completions.create({ /* ... */ })\n\nawait doppel.flush() // ensures captured runs are sent before exit\nreturn response\n```\n\nIn long-running servers you usually don't need `flush()` — deliveries complete in\nthe background.\n\n---\n\n## API reference\n\n### `new DoppelClient(options?)`\n\nCreates a new Doppel client. All options are optional in code because they can be\nresolved from environment variables — see [Configuration](#configuration).\n\n```ts\n// Minimal: everything from the environment\nconst doppel = new DoppelClient()\n\n// Or with explicit overrides\nconst doppel = new DoppelClient({\n  apiKey: 'dp_live_sk_...',           // optional, falls back to DOPPEL_API_KEY\n  shadowModel: 'gpt-4o-mini',         // optional, otherwise chosen in the dashboard\n  serverUrl: 'https://api.doppel.in', // optional, falls back to DOPPEL_SERVER_URL\n})\n```\n\nThrows immediately if no `apiKey` can be resolved from the options or the\n`DOPPEL_API_KEY` environment variable. `shadowModel` and `serverUrl` are optional.\n\n### `doppel.wrapOpenAI(client)`\n\nWraps an OpenAI client. Returns the same instance with `chat.completions.create` intercepted.\n\n```ts\nconst openai = doppel.wrapOpenAI(new OpenAI())\n```\n\n### `doppel.wrapAnthropic(client)`\n\nWraps an Anthropic client. Returns the same instance with `messages.create` intercepted.\n\n```ts\nconst anthropic = doppel.wrapAnthropic(new Anthropic())\n```\n\n### `doppel.wrapGoogle(client)`\n\nWraps a Google Gemini client. Auto-detects and supports both `@google/genai` (new)\nand `@google/generative-ai` (legacy). Returns the same instance with\n`generateContent` intercepted. Throws if the client is neither shape.\n\n```ts\nconst ai = doppel.wrapGoogle(new GoogleGenAI({ apiKey }))        // new SDK\nconst genAI = doppel.wrapGoogle(new GoogleGenerativeAI(apiKey))  // legacy SDK\n```\n\n### `doppel.wrapOpenRouter(client)`\n\nWraps an OpenRouter client (the `openai` SDK pointed at the OpenRouter base URL).\nReturns the same instance with `chat.completions.create` intercepted, tagging runs\nas `provider: 'openrouter'` and capturing real USD cost when reported.\n\n```ts\nconst openrouter = doppel.wrapOpenRouter(new OpenAI({\n  baseURL: 'https://openrouter.ai/api/v1',\n  apiKey: process.env.OPENROUTER_API_KEY,\n}))\n```\n\n### `doppel.flush()`\n\nAwaits all pending run deliveries. Call it before a short-lived process exits so\ncaptured runs are not dropped. Never rejects. Returns `Promise<void>`.\n\n```ts\nawait doppel.flush()\n```\n\nA standalone `flush()` is also exported for use with the low-level wrap functions:\n\n```ts\nimport { flush } from '@doppel-llm-test/sdk'\nawait flush()\n```\n\n---\n\n## TypeScript\n\nAll types are exported:\n\n```ts\nimport type {\n  DoppelClientConfig, // DoppelClient constructor options (all fields optional)\n  ImpactConfig,       // Fully-resolved internal config used by the interceptors\n  RunPayload,         // Raw payload shape sent to /runs\n  ChatParams,         // OpenAI request params\n  ChatResponse,       // OpenAI response shape\n  AnthropicParams,    // Anthropic request params\n  AnthropicResponse,  // Anthropic response shape\n  GoogleParams,       // Google (Gemini) request params\n  GoogleResponse,     // Google (Gemini) response shape\n} from '@doppel-llm-test/sdk'\n```\n","readmeFilename":"README.md"}