Skip to content
Back to skills

Openrouter Sdk Patterns

ASecurity

'Build reusable OpenRouter client wrappers with retries, typing, and

  • 2,719 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 21, 2026
ai-agentstypescriptpythonbashnodeapiperformance

Works with

  • claude code
  • cli
  • api

Security analysis

A100/100

Pro scans all 5 files and shows the line behind each finding

Scanned September 21, 2026

npx -y skills add jeremylongshore/claude-code-plugins-plus --skill openrouter-sdk-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Openrouter Sdk Patterns?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Openrouter Sdk Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/jeremylongshore-openrouter-sdk-patterns-9c601202/badge)](https://www.skillsdirectory.com/skills/jeremylongshore-openrouter-sdk-patterns-9c601202)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: openrouter-sdk-patterns
description: 'Build reusable OpenRouter client wrappers with retries, typing, and
  middleware. Use when creating SDKs or client libraries. Triggers: ''openrouter sdk'',
  ''openrouter client wrapper'', ''openrouter patterns'', ''openrouter library''.

  '
allowed-tools: Read, Write, Edit, Grep, Bash(python3:*), Bash(node:*)
version: 1.20.0
license: MIT
author: Jeremy Longshore <jeremy@intentsolutions.io>
tags:
- saas
- openrouter
- sdk
- patterns
compatibility: Designed for Claude Code
---
# OpenRouter SDK Patterns

## Overview

Build production-grade OpenRouter client wrappers using the OpenAI SDK. The OpenAI Python/TypeScript SDKs work natively with OpenRouter by changing `base_url` to `https://openrouter.ai/api/v1`. This skill covers typed wrappers, retry strategies, middleware, and reusable patterns.

## Prerequisites

- An OpenRouter API key (`sk-or-v1-...`) exported as `OPENROUTER_API_KEY` — see the `openrouter-install-auth` skill for setup
- Python 3.8+ with the OpenAI SDK plus `requests` (used for the `/auth/key` credits check and `/generation` cost lookups), or Node.js 18+ with the OpenAI SDK
- Optional: `tenacity` if you want the custom retry decorator beyond the SDK's built-in backoff
- An app name and URL to send as `HTTP-Referer` / `X-Title` default headers for dashboard attribution

## Instructions

1. Start from the Python: Production Client Wrapper (or the TypeScript variant): point the OpenAI SDK at `base_url="https://openrouter.ai/api/v1"`, read `OPENROUTER_API_KEY` from the environment, and set the `HTTP-Referer` / `X-Title` default headers in the constructor.
2. Return typed results from every call — the `CompletionResult` dataclass/interface captures `content`, the served `model`, `prompt_tokens`/`completion_tokens`, `generation_id`, and `latency_ms`.
3. Tune the SDK's built-in retries per the Retry Strategy section (`max_retries`, `timeout` in the constructor); add the `tenacity` decorator only when you need retry behavior beyond the SDK's 429/5xx/connection handling.
4. Layer cross-cutting concerns via the Middleware Pattern — `with_cost_tracking` queries `GET /api/v1/generation?id=` after each request and accumulates a session cost total.
5. Surface remaining credits and rate limits with `check_credits()`, which calls `GET /api/v1/auth/key` with the same key.
6. Map SDK exceptions using the Error Handling table, then apply the Enterprise Considerations (single central wrapper, dependency injection for tests, SLA-based `max_retries`).

## Python: Production Client Wrapper

```python
import os, time, hashlib, json, logging
from dataclasses import dataclass
from typing import Optional
from openai import OpenAI, APIError, RateLimitError, APITimeoutError

log = logging.getLogger("openrouter")

@dataclass
class CompletionResult:
    content: str
    model: str
    prompt_tokens: int
    completion_tokens: int
    generation_id: str
    latency_ms: float

class OpenRouterClient:
    def __init__(
        self,
        api_key: Optional[str] = None,
        app_name: str = "my-app",
        app_url: str = "https://my-app.com",
        max_retries: int = 3,
        timeout: float = 60.0,
    ):
        self.client = OpenAI(
            base_url="https://openrouter.ai/api/v1",
            api_key=api_key or os.environ["OPENROUTER_API_KEY"],
            max_retries=max_retries,  # Built-in SDK retry with backoff
            timeout=timeout,
            default_headers={
                "HTTP-Referer": app_url,
                "X-Title": app_name,
            },
        )
        self._cache: dict[str, CompletionResult] = {}

    def complete(
        self,
        prompt: str,
        model: str = "anthropic/claude-3.5-sonnet",
        system: str = "",
        max_tokens: int = 1024,
        temperature: float = 0.7,
        cache: bool = False,
        **extra_params,
    ) -> CompletionResult:
        messages = []
        if system:
            messages.append({"role": "system", "content": system})
        messages.append({"role": "user", "content": prompt})

        # Optional caching (deterministic requests only)
        cache_key = None
        if cache and temperature == 0:
            cache_key = hashlib.sha256(
                json.dumps({"model": model, "messages": messages, "max_tokens": max_tokens}).encode()
            ).hexdigest()
            if cache_key in self._cache:
                log.debug(f"Cache hit: {cache_key[:12]}")
                return self._cache[cache_key]

        start = time.monotonic()
        response = self.client.chat.completions.create(
            model=model,
            messages=messages,
            max_tokens=max_tokens,
            temperature=temperature,
            **extra_params,
        )
        latency = (time.monotonic() - start) * 1000

        result = CompletionResult(
            content=response.choices[0].message.content or "",
            model=response.model,
            prompt_tokens=response.usage.prompt_tokens,
            completion_tokens=response.usage.completion_tokens,
            generation_id=response.id,
            latency_ms=round(latency, 1),
        )

        log.info(f"[{result.model}] {result.prompt_tokens}+{result.completion_tokens} tokens, {result.latency_ms}ms")

        if cache_key:
            self._cache[cache_key] = result
        return result

    def check_credits(self) -> dict:
        """Check remaining credits and rate limits."""
        import requests
        resp = requests.get(
            "https://openrouter.ai/api/v1/auth/key",
            headers={"Authorization": f"Bearer {self.client.api_key}"},
        )
        return resp.json()["data"]

# Usage
or_client = OpenRouterClient(app_name="my-saas")
result = or_client.complete("Explain recursion", model="openai/gpt-4o-mini", max_tokens=200)
print(f"{result.content}\n---\n{result.model} | {result.latency_ms}ms | {result.prompt_tokens}+{result.completion_tokens} tokens")
```

## TypeScript: Production Client Wrapper

```typescript
import OpenAI from "openai";

interface CompletionResult {
  content: string;
  model: string;
  promptTokens: number;
  completionTokens: number;
  generationId: string;
  latencyMs: number;
}

class OpenRouterClient {
  private client: OpenAI;

  constructor(opts: { apiKey?: string; appName?: string; appUrl?: string } = {}) {
    this.client = new OpenAI({
      baseURL: "https://openrouter.ai/api/v1",
      apiKey: opts.apiKey ?? process.env.OPENROUTER_API_KEY,
      maxRetries: 3,
      timeout: 60_000,
      defaultHeaders: {
        "HTTP-Referer": opts.appUrl ?? "https://my-app.com",
        "X-Title": opts.appName ?? "My App",
      },
    });
  }

  async complete(
    prompt: string,
    opts: { model?: string; system?: string; maxTokens?: number; temperature?: number } = {}
  ): Promise<CompletionResult> {
    const messages: OpenAI.ChatCompletionMessageParam[] = [];
    if (opts.system) messages.push({ role: "system", content: opts.system });
    messages.push({ role: "user", content: prompt });

    const start = performance.now();
    const res = await this.client.chat.completions.create({
      model: opts.model ?? "anthropic/claude-3.5-sonnet",
      messages,
      max_tokens: opts.maxTokens ?? 1024,
      temperature: opts.temperature ?? 0.7,
    });
    const latency = Math.round(performance.now() - start);

    return {
      content: res.choices[0].message.content ?? "",
      model: res.model,
      promptTokens: res.usage?.prompt_tokens ?? 0,
      completionTokens: res.usage?.completion_tokens ?? 0,
      generationId: res.id,
      latencyMs: latency,
    };
  }
}

// Usage
const or = new OpenRouterClient({ appName: "my-saas" });
const result = await or.complete("Explain recursion", { model: "openai/gpt-4o-mini", maxTokens: 200 });
console.log(result.content, `\n${result.model} | ${result.latencyMs}ms`);
```

## Retry Strategy

The OpenAI SDK has built-in retries with exponential backoff for:

- 429 (rate limit) -- respects `Retry-After` header
- 5xx (server errors) -- retries with backoff
- Connection errors -- retries on network failures

```python
# Configure via constructor
client = OpenAI(
    base_url="https://openrouter.ai/api/v1",
    api_key="sk-or-v1-...",
    max_retries=5,           # Default is 2
    timeout=120.0,           # Per-request timeout in seconds
)
```

For custom retry logic beyond the SDK:

```python
import tenacity

@tenacity.retry(
    retry=tenacity.retry_if_exception_type((RateLimitError, APITimeoutError)),
    wait=tenacity.wait_exponential(min=1, max=60),
    stop=tenacity.stop_after_attempt(5),
    before_sleep=lambda state: log.warning(f"Retry {state.attempt_number}: {state.outcome.exception()}"),
)
def robust_complete(client, **kwargs):
    return client.chat.completions.create(**kwargs)
```

## Middleware Pattern

```python
from functools import wraps
from typing import Callable

def with_cost_tracking(fn: Callable) -> Callable:
    """Middleware that logs cost per request."""
    total_cost = {"value": 0.0}

    @wraps(fn)
    def wrapper(*args, **kwargs):
        result = fn(*args, **kwargs)
        # Query generation cost asynchronously
        import requests
        gen = requests.get(
            f"https://openrouter.ai/api/v1/generation?id={result.id}",
            headers={"Authorization": f"Bearer {args[0].api_key}"},
        ).json()
        cost = float(gen.get("data", {}).get("total_cost", 0))
        total_cost["value"] += cost
        log.info(f"Request cost: ${cost:.6f} | Session total: ${total_cost['value']:.4f}")
        return result
    wrapper.total_cost = total_cost
    return wrapper
```

## Output

- A typed `CompletionResult` per call: `content`, the `model` that actually served the request, `prompt_tokens`/`completion_tokens`, the `gen-...` `generation_id`, and `latency_ms`
- A structured log line per request, e.g. `[openai/gpt-4o-mini] 12+87 tokens, 843.2ms`
- Cost-tracking middleware output: per-request cost plus a running session total, e.g. `Request cost: $0.000123 | Session total: $0.0045`
- A credits dict from `check_credits()` with usage and limit data from `/api/v1/auth/key`

## Examples

Instantiate the wrapper once and make a typed call:

```python
or_client = OpenRouterClient(app_name="my-saas")
result = or_client.complete("Explain recursion", model="openai/gpt-4o-mini", max_tokens=200)
print(f"{result.model} | {result.latency_ms}ms | {result.prompt_tokens}+{result.completion_tokens} tokens")
# openai/gpt-4o-mini | 812.4ms | 11+142 tokens
```

The reference implementation also shows task helpers (`summarize`, `classify`) built on the same wrapper. More worked examples: `references/examples.md`.

## Error Handling

| Exception | HTTP | Cause | Fix |
|-----------|------|-------|-----|
| `AuthenticationError` | 401 | Bad API key | Check `OPENROUTER_API_KEY` |
| `RateLimitError` | 429 | Too many requests | SDK auto-retries; increase `max_retries` |
| `APITimeoutError` | -- | Response too slow | Increase `timeout`; use streaming |
| `BadRequestError` | 400 | Invalid params | Check model ID, messages format |

## Enterprise Considerations

- Centralize all OpenRouter calls through a single client wrapper for consistent logging, retries, and cost tracking
- Type all response shapes with dataclasses/interfaces for compile-time safety
- Use dependency injection to swap between OpenRouter and direct provider clients in tests
- Set `max_retries` based on your SLA (2 for interactive, 5 for batch)
- Wrap middleware in try/catch so instrumentation never breaks the main request flow

## References

- Examples | Errors
- [OpenRouter SDK](https://openrouter.ai/sdk) | [API Reference](https://openrouter.ai/docs/api/reference/overview)

Files in this skill

  • SKILL.md11.5 KB
  • references/errors.md478 B
  • references/examples.md4.5 KB
  • references/implementation.md13.9 KB
  • references/python-with-openai-sdk.md1.6 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.