# PassingRight — Full Documentation > PassingRight is an open-source, OpenAI-compatible API gateway for routing, managing, and analyzing requests across LLM providers. Use one API key, track usage and cost, configure caching and guardrails, and self-host or use the managed cloud. Current models and pricing: https://passingright.io/models API base URL: https://api.passingright.io/v1 · Docs: https://docs.passingright-staging.sandbloc.com · Site: https://passingright.io This file concatenates the full text of every documentation page below. # Introduction to PassingRight URL: https://docs.passingright-staging.sandbloc.com/ PassingRight is an open-source API gateway that sits between your applications and LLM providers like OpenAI, Anthropic, Google AI Studio, and more. It provides a unified, OpenAI-compatible API interface with built-in cost tracking, caching, and intelligent routing. ## How it works [#how-it-works] Point your existing SDK at `https://api.passingright.io/v1` (or your self-hosted instance) and authenticate with an PassingRight API key — no code rewrites. The gateway routes each request to the right provider, tracks tokens and cost per model, provider, project, and API key, and fails over to a healthy provider when one errors. Pay per-token with prepaid credits at provider list rates, or bring your own provider keys (BYOK) for free. ## Why use a gateway? [#why-use-a-gateway] * **One integration** — switch models or providers by changing a model string, not your code. * **Cost visibility** — usage analytics and cost breakdowns across every provider in one dashboard. * **Reliability** — automatic provider failover when a provider fails, plus opt-in response caching per project. * **No lock-in** — open source (AGPLv3), self-hostable, and OpenAI-compatible end to end. ## Features [#features] All features are documented under https://docs.passingright-staging.sandbloc.com/features; each feature page is included in full in this file. ## AI Tooling [#ai-tooling] PassingRight is built to work seamlessly with AI agents and development tools. AI tooling: https://docs.passingright-staging.sandbloc.com/llms.txt (docs index for LLMs), https://docs.passingright-staging.sandbloc.com/llms-full.txt (this file), https://docs.passingright-staging.sandbloc.com/developers/mcp (MCP server), https://docs.passingright-staging.sandbloc.com/guides/agent-skills (agent skills), and https://passingright.io/templates (templates and agents). ## Next Steps [#next-steps] * [**Quickstart**](https://docs.passingright-staging.sandbloc.com/quick-start) — Get up and running in minutes * [**Overview**](https://docs.passingright-staging.sandbloc.com/overview) — Learn more about what PassingRight offers * [**Self-Host**](https://docs.passingright-staging.sandbloc.com/self-host) — Deploy on your own infrastructure # Overview URL: https://docs.passingright-staging.sandbloc.com/overview PassingRight is an open-source API gateway for Large Language Models (LLMs). It acts as a middleware between your applications and various LLM providers, allowing you to: * Route requests to multiple LLM providers (OpenAI, Anthropic, Google AI Studio, and others) * Manage API keys for different providers in one place * Track token usage and costs across all your LLM interactions * Analyze performance metrics to optimize your LLM usage ## Analyzing Your LLM Requests [#analyzing-your-llm-requests] PassingRight provides detailed insights into your LLM usage: * **Usage Metrics**: Track the number of requests, tokens used, and response times * **Cost Analysis**: Monitor spending across different models and providers * **Performance Tracking**: Identify patterns and optimize your prompts based on actual usage data * **Breakdown by Model**: Compare different models' performance and cost-effectiveness All this data is automatically collected and presented in an intuitive dashboard, helping you make informed decisions about your LLM strategy. ## Getting Started [#getting-started] Using PassingRight is simple. Just swap out your current LLM provider URL with the PassingRight API endpoint: ```bash curl -X POST https://api.passingright.io/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -d '{ "model": "gpt-4o", "messages": [ {"role": "user", "content": "Hello, how are you?"} ] }' ``` PassingRight maintains compatibility with the OpenAI API format, making migration seamless. Note that unknown or unsupported request parameters (for example `stop`, `seed`, `logprobs`, or `logit_bias`) are accepted and silently ignored rather than rejected, so a request carrying them still succeeds — the parameter just has no effect. ## Hosted vs. Self-Hosted [#hosted-vs-self-hosted] You can use PassingRight in two ways: * **Hosted Version**: For immediate use without setup, visit [passingright.io](https://passingright.io) to create an account and get an API key. * **Self-Hosted**: Deploy PassingRight on your own infrastructure for complete control over your data and configuration. The self-hosted version offers additional customization options and ensures your LLM traffic never leaves your infrastructure if desired. # Quickstart URL: https://docs.passingright-staging.sandbloc.com/quick-start Welcome to **PassingRight**—a single drop‑in endpoint that lets you call today’s best large‑language models while keeping **your existing code** and development workflow intact. > **TL;DR** — Point your HTTP requests to `https://api.passingright.io/v1/…`, supply your `LLM_GATEWAY_API_KEY`, and you’re done. *** ## 1 · Get an API key [#1--get-an-api-key] 1. Sign in to the dashboard. 2. Create a new Project → *Copy the key*. 3. Export it in your shell (or a `.env` file): ```bash export LLM_GATEWAY_API_KEY="llmgtwy_XXXXXXXXXXXXXXXX" ``` *** ## 2 · Pick your language [#2--pick-your-language] ```bash curl -X POST https://api.passingright.io/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -d '{ "model": "gpt-4o", "messages": [ {"role": "user", "content": "Hello, how are you?"} ] }' ``` ```typescript const response = await fetch("https://api.passingright.io/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.LLM_GATEWAY_API_KEY}`, }, body: JSON.stringify({ model: "gpt-4o", messages: [{ role: "user", content: "Hello, how are you?" }], }), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); console.log(data.choices[0].message.content); ``` ```tsx import { useState } from "react"; function ChatComponent() { const [response, setResponse] = useState(""); const [loading, setLoading] = useState(false); const sendMessage = async () => { setLoading(true); try { const res = await fetch("https://api.passingright.io/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.REACT_APP_LLM_GATEWAY_API_KEY}`, }, body: JSON.stringify({ model: "gpt-4o", messages: [{ role: "user", content: "Hello, how are you?" }], }), }); if (!res.ok) { throw new Error(`HTTP error! status: ${res.status}`); } const data = await res.json(); setResponse(data.choices[0].message.content); } catch (error) { console.error("Error:", error); } finally { setLoading(false); } }; return (
{response &&

{response}

}
); } export default ChatComponent; ```
```typescript // app/api/chat/route.ts import { NextRequest, NextResponse } from "next/server"; export async function POST(request: NextRequest) { const { message } = await request.json(); const response = await fetch( "https://api.passingright.io/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.LLM_GATEWAY_API_KEY}`, }, body: JSON.stringify({ model: "gpt-4o", messages: [{ role: "user", content: message }], }), }, ); if (!response.ok) { return NextResponse.json( { error: "Failed to get response" }, { status: response.status }, ); } const data = await response.json(); return NextResponse.json({ message: data.choices[0].message.content, }); } // Usage in component: // const response = await fetch('/api/chat', { // method: 'POST', // headers: { 'Content-Type': 'application/json' }, // body: JSON.stringify({ message: 'Hello, how are you?' }) // }); ``` ```python import requests import os response = requests.post( 'https://api.passingright.io/v1/chat/completions', headers={ 'Content-Type': 'application/json', 'Authorization': f'Bearer {os.getenv("LLM_GATEWAY_API_KEY")}' }, json={ 'model': 'gpt-4o', 'messages': [ {'role': 'user', 'content': 'Hello, how are you?'} ] } ) response.raise_for_status() print(response.json()['choices'][0]['message']['content']) ``` ```java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; String apiKey = System.getenv("LLM_GATEWAY_API_KEY"); String requestBody = """ { "model": "gpt-4o", "messages": [ {"role": "user", "content": "Hello, how are you?"} ] } """; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.passingright.io/v1/chat/completions")) .header("Content-Type", "application/json") .header("Authorization", "Bearer " + apiKey) .POST(HttpRequest.BodyPublishers.ofString(requestBody)) .build(); HttpResponse response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ```rust use reqwest::Client; use serde_json::json; use std::env; #[tokio::main] async fn main() -> Result<(), Box> { let client = Client::new(); let api_key = env::var("LLM_GATEWAY_API_KEY")?; let response = client .post("https://api.passingright.io/v1/chat/completions") .header("Content-Type", "application/json") .header("Authorization", format!("Bearer {}", api_key)) .json(&json!({ "model": "gpt-4o", "messages": [ {"role": "user", "content": "Hello, how are you?"} ] })) .send() .await?; let result: serde_json::Value = response.json().await?; println!("{}", result["choices"][0]["message"]["content"]); Ok(()) } ``` ```go package main import ( "bytes" "encoding/json" "fmt" "net/http" "os" ) type ChatRequest struct { Model string `json:"model"` Messages []Message `json:"messages"` } type Message struct { Role string `json:"role"` Content string `json:"content"` } func main() { apiKey := os.Getenv("LLM_GATEWAY_API_KEY") requestBody := ChatRequest{ Model: "gpt-4o", Messages: []Message{{Role: "user", Content: "Hello, how are you?"}}, } jsonData, _ := json.Marshal(requestBody) req, _ := http.NewRequest("POST", "https://api.passingright.io/v1/chat/completions", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+apiKey) client := &http.Client{} resp, _ := client.Do(req) defer resp.Body.Close() fmt.Println("Response received") } ``` ```php 'gpt-4o', 'messages' => [ ['role' => 'user', 'content' => 'Hello, how are you?'] ] ]; $options = [ 'http' => [ 'header' => [ 'Content-Type: application/json', 'Authorization: Bearer ' . $apiKey ], 'method' => 'POST', 'content' => json_encode($data) ] ]; $context = stream_context_create($options); $response = file_get_contents( 'https://api.passingright.io/v1/chat/completions', false, $context ); if ($response === FALSE) { throw new Exception('Request failed'); } $result = json_decode($response, true); echo $result['choices'][0]['message']['content']; ?> ``` ```ruby require 'net/http' require 'json' require 'uri' uri = URI('https://api.passingright.io/v1/chat/completions') http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Post.new(uri) request['Content-Type'] = 'application/json' request['Authorization'] = "Bearer #{ENV['LLM_GATEWAY_API_KEY']}" request.body = { model: 'gpt-4o', messages: [ { role: 'user', content: 'Hello, how are you?' } ] }.to_json response = http.request(request) if response.code != '200' raise "HTTP Error: #{response.code}" end result = JSON.parse(response.body) puts result['choices'][0]['message']['content'] ```
*** ## 3 · SDK integrations [#3--sdk-integrations] ```ts title="ai-sdk.ts" import { llmgateway } from "@llmgateway/ai-sdk-provider"; import { generateText } from "ai"; const { text } = await generateText({ model: llmgateway("gpt-4o"), prompt: "Write a vegetarian lasagna recipe for 4 people.", }); ``` ```ts title="vercel-ai-sdk.ts" import { createOpenAI } from "@ai-sdk/openai"; const llmgateway = createOpenAI({ baseURL: "https://api.passingright.io/v1", apiKey: process.env.LLM_GATEWAY_API_KEY!, }); const completion = await llmgateway.chat({ model: "gpt-4o", messages: [{ role: "user", content: "Hello, how are you?" }], }); console.log(completion.choices[0].message.content); ``` ```ts title="openai-sdk.ts" import OpenAI from "openai"; const openai = new OpenAI({ baseURL: "https://api.passingright.io/v1", apiKey: process.env.LLM_GATEWAY_API_KEY, }); const completion = await openai.chat.completions.create({ model: "gpt-4o", messages: [{ role: "user", content: "Hello, how are you?" }], }); console.log(completion.choices[0].message.content); ``` *** ## 4 · Going further [#4--going-further] * **Streaming**: pass `stream: true` to any request—the gateway normalizes every provider's stream into OpenAI-format SSE chunks, with a final chunk carrying `usage` and routing metadata before `data: [DONE]`. * **Monitoring**: Every call appears in the dashboard with latency, cost & provider breakdown. *** ## 5 · FAQ [#5--faq] See the [Models page](https://passingright.io/models).

Unlike OpenRouter, we offer:

Our pricing structure is designed to be flexible and cost-effective: See the [Pricing section](https://passingright.io#pricing).
*** ## 6 · Next steps [#6--next-steps] * Read [Self host docs](https://docs.passingright-staging.sandbloc.com/self-host) guide. * Drop into our [GitHub](https://github.com/theopenco/llmgateway) for help or feature requests. Happy building! ✨ # Health check URL: https://docs.passingright-staging.sandbloc.com/health {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create speech URL: https://docs.passingright-staging.sandbloc.com/v1_audio_speech {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create transcription URL: https://docs.passingright-staging.sandbloc.com/v1_audio_transcriptions {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Chat Completions URL: https://docs.passingright-staging.sandbloc.com/v1_chat_completions {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Embeddings URL: https://docs.passingright-staging.sandbloc.com/v1_embeddings {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Edit image URL: https://docs.passingright-staging.sandbloc.com/v1_images_edits {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create image URL: https://docs.passingright-staging.sandbloc.com/v1_images_generations {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Retrieve key status URL: https://docs.passingright-staging.sandbloc.com/v1_key_retrieve {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Anthropic Messages URL: https://docs.passingright-staging.sandbloc.com/v1_messages {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Models URL: https://docs.passingright-staging.sandbloc.com/v1_models {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Moderations URL: https://docs.passingright-staging.sandbloc.com/v1_moderations {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # OCR URL: https://docs.passingright-staging.sandbloc.com/v1_ocr {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create a realtime client secret URL: https://docs.passingright-staging.sandbloc.com/v1_realtime_client_secrets {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Rerank URL: https://docs.passingright-staging.sandbloc.com/v1_rerank {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # System One URL: https://docs.passingright-staging.sandbloc.com/v1_systemone {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Video content URL: https://docs.passingright-staging.sandbloc.com/v1_videos_content {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create video URL: https://docs.passingright-staging.sandbloc.com/v1_videos_create {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Video log content URL: https://docs.passingright-staging.sandbloc.com/v1_videos_log_content {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Retrieve video URL: https://docs.passingright-staging.sandbloc.com/v1_videos_retrieve {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # AI SDK Gateway protocol URL: https://docs.passingright-staging.sandbloc.com/developers/ai-sdk-gateway-protocol When you pass a bare model string to the AI SDK — `streamText({ model: "anthropic/claude-sonnet-5" })` — the SDK resolves it through its **default provider**, `@ai-sdk/gateway`. That provider does not speak the OpenAI Chat Completions format: it has its own wire protocol, `LanguageModelV*CallOptions` in and `LanguageModelV*` parts out. PassingRight implements that protocol, so an app written against the Vercel AI Gateway runs here with **no code change** — only its base URL and API key are repointed. ## Repoint the default provider [#repoint-the-default-provider] ```ts import { createGateway } from "@ai-sdk/gateway"; globalThis.AI_SDK_DEFAULT_PROVIDER = createGateway({ baseURL: "https://api.passingright.io/v4/ai", apiKey: process.env.LLM_GATEWAY_API_KEY, }); ``` Put this wherever your app runs before its first model call — a Next.js [`instrumentation.ts`](https://nextjs.org/docs/app/guides/instrumentation), a server entrypoint, or a platform-injected preamble. Every bare model string in the app then resolves through PassingRight. `@ai-sdk/gateway` is already a transitive dependency of `ai`, so there is nothing extra to install. `@ai-sdk/gateway` reads its API key from `AI_GATEWAY_API_KEY` but has **no** environment variable for the base URL — it is a constructor option only. That is why repointing takes this one line rather than an env var. Or construct the provider explicitly and pass it per call: ```ts import { createGateway } from "@ai-sdk/gateway"; import { streamText } from "ai"; const gateway = createGateway({ baseURL: "https://api.passingright.io/v4/ai", apiKey: process.env.LLM_GATEWAY_API_KEY, }); const result = streamText({ model: gateway("anthropic/claude-sonnet-5"), prompt: "Hello!", }); ``` ## Base URL per AI SDK version [#base-url-per-ai-sdk-version] The protocol carries the language-model specification version in a request header, and every prefix below serves the same surface — pick the one matching the `@ai-sdk/gateway` your app has, so the default path stays intact: | AI SDK | Spec version | Base URL | | ------ | ------------ | ----------------------------------- | | 5 | 2 | `https://api.passingright.io/v1/ai` | | 6 | 3 | `https://api.passingright.io/v3/ai` | | 7 | 4 | `https://api.passingright.io/v4/ai` | ## Model IDs [#model-ids] Model IDs use the provider-pinned `provider/model` form (`anthropic/claude-sonnet-5`, `openai/gpt-4o`) — the same convention AI Gateway IDs use, so existing model strings resolve unchanged. PassingRight's smart-routing IDs work here too: pass a bare model ID (`gpt-4o`) to let the gateway pick a provider, or `auto` to let it pick the model. Those are not returned by `getAvailableModels()` because they do not name one provider, but they are accepted. ## Listing models [#listing-models] ```ts const { models } = await gateway.getAvailableModels(); ``` Returns one entry per active provider mapping, with pricing and an AI SDK `specification` block. This is what backs a model picker built on `GatewayModel[]`. ## Credits [#credits] ```ts const { balance, totalUsed } = await gateway.getCredits(); ``` `balance` is the organization's remaining credit balance and `totalUsed` its lifetime credits spend. ## Gateway-only options [#gateway-only-options] Features that have no field in the AI SDK call options — reasoning effort, service tier, routing strategy, prompt cache keys, plugins — are set through the `llmgateway` provider options namespace, which is passed onto the underlying request: ```ts const result = streamText({ model: gateway("openai/gpt-5.6-terra"), prompt: "Hello!", providerOptions: { llmgateway: { reasoning_effort: "high", routing: "price", }, }, }); ``` Any field the [chat completions API](https://docs.passingright-staging.sandbloc.com/api-reference) accepts works here, except `model`, `messages` and `stream`, which this surface owns. ## Web search [#web-search] The provider-native web search tools serialize to provider-defined tools, and the gateway maps them onto its [native web search](https://docs.passingright-staging.sandbloc.com/features/web-search): ```ts import { openai } from "@ai-sdk/openai"; const result = streamText({ model: gateway("openai/gpt-4o"), prompt: "What happened in the news today?", tools: { web_search: openai.tools.webSearch() }, }); ``` Recognised tools: `openai.web_search`, `openai.web_search_preview`, `anthropic.web_search_20250305`, `anthropic.web_search_20260209`, `google.google_search`. Search results come back as `source-url` message parts plus a provider-executed tool call under the name you bound the tool to, so the AI SDK's sources UI works unchanged. A provider-defined tool the gateway cannot map is reported as an `unsupported-tool` warning on the result rather than failing the request. ## What is not covered [#what-is-not-covered] This surface implements language models. Embeddings, images, video, speech, transcription, reranking and realtime are served by the [OpenAI-compatible endpoints](https://docs.passingright-staging.sandbloc.com/api-reference) — use [`@llmgateway/ai-sdk-provider`](https://docs.passingright-staging.sandbloc.com/developers/ai-sdk) for those. These call options have no chat completions equivalent and are reported as `unsupported` warnings: `stopSequences`, `seed`, `topK`. # Image Generation with the AI SDK URL: https://docs.passingright-staging.sandbloc.com/developers/ai-sdk-images The `@llmgateway/ai-sdk-provider` package supports image generation both through the AI SDK's dedicated `generateImage` function and through chat-based image models that stream images as part of a conversation. ## generateImage [#generateimage] Use `llmgateway.image()` to get an image model: ```typescript import { createLLMGateway } from "@llmgateway/ai-sdk-provider"; import { generateImage } from "ai"; import { writeFileSync } from "fs"; const llmgateway = createLLMGateway({ apiKey: process.env.LLM_GATEWAY_API_KEY, }); const result = await generateImage({ model: llmgateway.image("gemini-3-pro-image"), prompt: "A cozy cabin in a snowy mountain landscape at night with aurora borealis", size: "1024x1024", n: 1, // aspectRatio and quality are model-specific — only some providers honor them. // aspectRatio works on Gemini image models; OpenAI gpt-image-2 ignores it // (use a literal WxH `size` instead). aspectRatio: "16:9", // quality works on OpenAI gpt-image-2 ("low" | "medium" | "high" | "auto") // and moderation ("auto" | "low") on GPT Image models. // The AI SDK only forwards these through providerOptions. providerOptions: { llmgateway: { quality: "high", moderation: "low" }, }, }); result.images.forEach((image, i) => { const buf = Buffer.from(image.base64, "base64"); writeFileSync(`image-${i}.png`, buf); }); ``` Which sizes, aspect ratios, and quality settings a model accepts depends on the model — see [Image Generation](https://docs.passingright-staging.sandbloc.com/features/image-generation) for the full parameter reference and per-model behavior. ## Chat-based image models [#chat-based-image-models] Multimodal models like `gemini-3-pro-image` can return images inside a chat conversation. Use `llmgateway.chat()` with `streamText` in a route handler: ```typescript // app/api/chat/route.ts import { createLLMGateway } from "@llmgateway/ai-sdk-provider"; import { convertToModelMessages, streamText } from "ai"; const llmgateway = createLLMGateway({ apiKey: process.env.LLM_GATEWAY_API_KEY, }); export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: llmgateway.chat("gemini-3-pro-image"), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse(); } ``` On the client, image parts arrive as message file parts that you can render with the AI Elements `Image` component or a plain `` tag with a data URL. See [Image Generation](https://docs.passingright-staging.sandbloc.com/features/image-generation) for the complete `useChat` frontend example. ## Video and audio [#video-and-audio] The AI SDK does not yet cover the gateway's video and speech endpoints — call them over REST instead: * [Video Generation](https://docs.passingright-staging.sandbloc.com/features/video-generation) — `POST /v1/videos` (async jobs with optional signed callbacks) * [Speech Generation](https://docs.passingright-staging.sandbloc.com/features/speech-generation) — `POST /v1/audio/speech` * [Transcription](https://docs.passingright-staging.sandbloc.com/features/transcription) — `POST /v1/audio/transcriptions` # Using the AI SDK URL: https://docs.passingright-staging.sandbloc.com/developers/ai-sdk PassingRight ships a first-party provider for the [Vercel AI SDK](https://ai-sdk.dev): [`@llmgateway/ai-sdk-provider`](https://github.com/theopenco/llmgateway-ai-sdk-provider). One provider instance and one API key reach every model in the catalog. ## Install [#install] ```bash pnpm add ai @llmgateway/ai-sdk-provider ``` Set your API key (create one from the [dashboard](https://passingright.io/dashboard)): ```bash export LLM_GATEWAY_API_KEY=llmgtwy_your_key_here ``` ## Generate text [#generate-text] ```typescript import { createLLMGateway } from "@llmgateway/ai-sdk-provider"; import { generateText } from "ai"; const llmgateway = createLLMGateway({ apiKey: process.env.LLM_GATEWAY_API_KEY, }); const { text } = await generateText({ model: llmgateway("openai/gpt-4o"), prompt: "Hello!", }); ``` Switching models is a one-line change — the same provider serves every model: ```typescript const { text } = await generateText({ model: llmgateway("anthropic/claude-3-5-sonnet-20241022"), prompt: "Hello!", }); ``` ## Model ID formats [#model-id-formats] PassingRight supports two model ID formats: * **Canonical model IDs** (`gpt-4o`) — smart routing picks the best provider based on uptime, throughput, price, and latency * **Provider-prefixed IDs** (`openai/gpt-4o`) — routes to a specific provider with automatic failover if uptime drops below 90% See the [routing documentation](https://docs.passingright-staging.sandbloc.com/features/routing) for details and the [models page](https://passingright.io/models) for the full catalog. ## Stream responses [#stream-responses] ```typescript import { createLLMGateway } from "@llmgateway/ai-sdk-provider"; import { streamText } from "ai"; const llmgateway = createLLMGateway({ apiKey: process.env.LLM_GATEWAY_API_KEY, }); const { textStream } = await streamText({ model: llmgateway("anthropic/claude-3-5-sonnet-20241022"), prompt: "Write a poem about coding", }); for await (const text of textStream) { process.stdout.write(text); } ``` ## Next.js route handler [#nextjs-route-handler] ```typescript // app/api/chat/route.ts import { createLLMGateway } from "@llmgateway/ai-sdk-provider"; import { streamText } from "ai"; const llmgateway = createLLMGateway({ apiKey: process.env.LLM_GATEWAY_API_KEY, }); export async function POST(req: Request) { const { messages } = await req.json(); const result = await streamText({ model: llmgateway("openai/gpt-4o"), messages, }); return result.toDataStreamResponse(); } ``` ## Tool calling [#tool-calling] ```typescript import { createLLMGateway } from "@llmgateway/ai-sdk-provider"; import { generateText, tool } from "ai"; import { z } from "zod"; const llmgateway = createLLMGateway({ apiKey: process.env.LLM_GATEWAY_API_KEY, }); const { text, toolResults } = await generateText({ model: llmgateway("openai/gpt-4o"), tools: { weather: tool({ description: "Get the weather for a location", parameters: z.object({ location: z.string(), }), execute: async ({ location }) => { return { temperature: 72, condition: "sunny" }; }, }), }, prompt: "What's the weather in San Francisco?", }); ``` ## Without the provider package [#without-the-provider-package] If you prefer not to add a dependency, point `@ai-sdk/openai` at the gateway with a custom base URL: ```typescript import { createOpenAI } from "@ai-sdk/openai"; import { generateText } from "ai"; const llmgateway = createOpenAI({ baseURL: "https://api.passingright.io/v1", apiKey: process.env.LLM_GATEWAY_API_KEY, }); const { text } = await generateText({ model: llmgateway("openai/gpt-4o"), prompt: "Hello!", }); ``` Every request made through the AI SDK shows up in your [Activity](https://docs.passingright-staging.sandbloc.com/learn/activity) and [Usage & Metrics](https://docs.passingright-staging.sandbloc.com/learn/usage-metrics) dashboards like any other gateway request — with per-request cost, tokens, and latency. ## Generate video [#generate-video] Version 4 of the provider (AI SDK 7, Node.js 22 or later, ESM) adds `llmgateway.video()` for the SDK's experimental video API. The provider submits a gateway video job, the SDK polls it, and the finished file is downloaded from the authenticated content endpoint. ```typescript import { writeFile } from "node:fs/promises"; import { llmgateway } from "@llmgateway/ai-sdk-provider"; import { experimental_generateVideo as generateVideo } from "ai"; const { video } = await generateVideo({ model: llmgateway.video("seedance-2-0"), prompt: "A cinematic aerial view of ocean waves at sunrise", duration: 8, resolution: "1280x720", poll: { intervalMs: 5_000, timeoutMs: 600_000 }, }); await writeFile("video.mp4", video.uint8Array); ``` `duration` and a text prompt are required. `resolution` maps to the gateway's `size`, `duration` to `seconds`, and `generateAudio` to `audio`. Pass a first frame as `prompt.image`, first and last frames with `frameImages`, and reference inputs with `inputReferences`; any other gateway field, such as `callback_url`, goes through `providerOptions.llmgateway`. `experimental_startVideo` and `experimental_getVideoStatus` let another process pick up a running job. Supported sizes, durations, and inputs depend on the model — see [video generation](https://docs.passingright-staging.sandbloc.com/features/video-generation) for the REST reference. Projects on AI SDK 6 should stay on `@llmgateway/ai-sdk-provider@3`, which has no video support. ## Next steps [#next-steps] * [Image generation with the AI SDK](https://docs.passingright-staging.sandbloc.com/developers/ai-sdk-images) * [Migrate from Vercel AI Gateway](https://docs.passingright-staging.sandbloc.com/migrations/vercel-ai-gateway) * [Reasoning support](https://docs.passingright-staging.sandbloc.com/features/reasoning) and [caching](https://docs.passingright-staging.sandbloc.com/features/caching) # PassingRight CLI URL: https://docs.passingright-staging.sandbloc.com/developers/cli The **PassingRight CLI** (`@llmgateway/cli`) is a command-line utility for launching coding agents pre-configured with PassingRight, scaffolding projects, discovering models, and managing your PassingRight account — API keys, spending budgets, and usage analytics — straight from the terminal. **Using DevPass?** This integration also works with a [DevPass](https://devpass.passingright.io) plan key. Use canonical model IDs without a provider prefix (`claude-sonnet-4-5`, not `anthropic/claude-sonnet-4-5`) — provider-pinned routing is not available on coding plans; the gateway picks the provider for you. ## Installation [#installation] Run commands directly without installation: ```bash npx @llmgateway/cli init ``` Install globally for faster access: ```bash npm install -g @llmgateway/cli ``` Then run commands directly (`lg` works as a shorthand alias): ```bash llmgateway init lg init ``` ## Quick Start [#quick-start] ### Initialize a Project [#initialize-a-project] Create a new project from a template: ```bash npx @llmgateway/cli init ``` Or specify the template and name directly: ```bash npx @llmgateway/cli init --template image-generation --name my-ai-app ``` ### Sign In [#sign-in] Sign in with your PassingRight account to unlock key management, budgets, and usage analytics: ```bash npx @llmgateway/cli auth login --email you@example.com ``` Or store a gateway API key only (enough for making gateway requests): ```bash npx @llmgateway/cli auth login --key ``` Credentials are stored in `~/.llmgateway/config.json`. The `LLMGATEWAY_API_KEY` environment variable takes precedence over a stored key. ### Start Development [#start-development] Navigate to your project and start the development server: ```bash cd my-ai-app npx @llmgateway/cli dev ``` Or specify a custom port: ```bash npx @llmgateway/cli dev --port 3000 ``` ## Launch Coding Agents [#launch-coding-agents] ### `launch` [#launch] Start any supported coding agent pre-wired to PassingRight: one API key, 200+ models, and every request tracked in your [dashboard](https://passingright.io/dashboard). ```bash # Interactive agent picker npx @llmgateway/cli launch # Launch a specific agent (shortcuts work too: `llmgateway claude`) npx @llmgateway/cli launch claude npx @llmgateway/cli launch opencode npx @llmgateway/cli launch codex # Pick a model — launcher flags go before the agent name npx @llmgateway/cli launch -m gpt-5.5 claude # Everything after the agent name is passed to the agent itself npx @llmgateway/cli launch claude --continue # List all supported agents and see which are installed npx @llmgateway/cli launch --list # Inspect what would run without launching npx @llmgateway/cli launch --dry-run codex ``` Every supported agent also works as a direct shortcut, e.g. `npx @llmgateway/cli claude` or `npx @llmgateway/cli devpass-code`. The launcher configures each agent automatically — environment variables, config files, or the agent's own key-registration command, whichever that agent needs — without overwriting your existing setup. For OpenCode and Claude Code, launching also applies the same model-catalog setup as [`configure`](#configure) on every launch, so their model pickers stay fresh as new models ship. The API key is resolved from `--key`, the `LLMGATEWAY_API_KEY` environment variable, or the key stored by `llmgateway auth login --key` — in that order. Before launching, the key is verified against the gateway; a stale key (e.g. one you rolled or deleted) is reported with its exact source and the launcher falls back to the next valid one, prompting you for a fresh key if none works. If an agent isn't installed, the launcher prints its official install command and exits. See the [integration guides](https://passingright.io/guides) for per-agent setup details, and run `npx @llmgateway/cli launch --list` for the up-to-date list of supported agents. ### `configure` [#configure] Put PassingRight's coding-model catalog directly into an agent's own config, so its model picker lists gateway models without launching through the CLI: ```bash # opencode: adds every coding model, pinned per upstream provider, to the picker npx @llmgateway/cli configure opencode # Claude Code: routes it through PassingRight and fills /model from the gateway catalog npx @llmgateway/cli configure claude # ...for the current repo only (.claude/settings.local.json) npx @llmgateway/cli configure claude --project # Preview without writing npx @llmgateway/cli configure opencode --dry-run ``` * **OpenCode** — merges `provider/model` entries (e.g. `anthropic/claude-sonnet-5`, `aws-bedrock/claude-sonnet-5`) into `provider.llmgateway.models` in `~/.config/opencode/opencode.json`, with display names, context limits, and per-provider pricing. They show up in the picker as `llmgateway//` and pin that upstream provider via the gateway's [provider-routing syntax](https://docs.passingright-staging.sandbloc.com/features/routing#provider-specific-routing). OpenCode now ships both catalogs natively too — canonical IDs under **DevPass (PassingRight)** (`llmgateway`) and pinned `provider/model` IDs under **PassingRight** (`llmgateway-providers`) — so `configure` mainly covers models the built-in catalog has not picked up yet. Existing custom entries and the rest of the file are preserved, and a hand-written `opencode.jsonc` keeps working alongside it. * **Claude Code** — sets `ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN`, and `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1` in `~/.claude/settings.json` (requires Claude Code v2.1.129+). Claude Code then loads the gateway's `/v1/models` catalog into its `/model` picker (shown as "From gateway"). Claude Code only lists IDs starting with `claude`/`anthropic`; any other gateway model still works via `claude --model `. `llmgateway launch opencode` and `llmgateway launch claude` apply the same setup automatically on every launch, keeping the catalog fresh as new models ship. ## Project Commands [#project-commands] ### `init` [#init] Initialize a new project from a template. ```bash npx @llmgateway/cli init [directory] [options] ``` **Options:** * `-t, --template ` — Template to use (default: `image-generation`) * `-n, --name ` — Project name **Examples:** ```bash # Interactive mode npx @llmgateway/cli init # With options npx @llmgateway/cli init --template image-generation --name my-app ``` ### `list` [#list] Display available project templates, grouped by category. Alias: `ls`. ```bash npx @llmgateway/cli list ``` **Options:** * `--json` — Output in JSON format ### `models` [#models] Browse and filter available AI models. ```bash npx @llmgateway/cli models [options] ``` **Options:** * `-c, --capability ` — Filter by capability (e.g., `image`, `text`) * `-p, --provider ` — Filter by provider (e.g., `openai`, `anthropic`) * `-s, --search ` — Search models by name * `--json` — Output in JSON format **Examples:** ```bash # List all models npx @llmgateway/cli models # Filter by provider npx @llmgateway/cli models --provider openai # Search models npx @llmgateway/cli models --search gpt ``` ### `add` [#add] Add tools or API routes to an existing project. ```bash npx @llmgateway/cli add [type] [name] ``` Runs interactively when `type` (`tool` or `route`) and `name` are omitted. **Tools available:** * `weather` — Weather lookup functionality * `search` — Web search capability * `calculator` — Mathematical operations **API routes available:** * `generate` — Text generation endpoint * `chat` — Chat completion endpoint with streaming ### `dev` [#dev] Start the local development server using your project's package manager. ```bash npx @llmgateway/cli dev [options] ``` **Options:** * `-p, --port ` — Port to run on ### `upgrade` [#upgrade] Update PassingRight dependencies (`@llmgateway/ai-sdk-provider`, `@llmgateway/models`, `@llmgateway/cli`) in your project. ```bash npx @llmgateway/cli upgrade [options] ``` **Options:** * `--check` — Check for updates without installing ### `docs` [#docs] Open the documentation in your browser. ```bash npx @llmgateway/cli docs [topic] ``` **Topics:** `models`, `api`, `sdk`, `quickstart` — omit to open the docs home and see all topics. ## Account Commands [#account-commands] The commands below require a dashboard session — sign in first with `llmgateway auth login --email`. A gateway API key alone is not enough for account management. ### `auth` [#auth] Manage authentication (dashboard session and gateway API key). For browser-based device sign-in, the approval page is titled **Authorize your device**. Check that its code matches the one on your device before choosing **Authorize device**. Approval creates a separate account session; signing the device out leaves the browser signed in. Choose **Deny** for a request you did not start. ```bash # Sign in with email & password (full access), or paste an API key npx @llmgateway/cli auth login npx @llmgateway/cli auth login --email you@example.com npx @llmgateway/cli auth login --key # Check authentication status (session + API key) npx @llmgateway/cli auth status # Show the signed-in user npx @llmgateway/cli auth whoami # Remove stored session and API key npx @llmgateway/cli auth logout ``` ### `keys` [#keys] Create and manage gateway API keys. ```bash npx @llmgateway/cli keys ``` #### `keys create` [#keys-create] Create a new API key, optionally with spending limits and an expiry. ```bash npx @llmgateway/cli keys create --description "CI key" --limit 100 --expires 30d ``` **Options:** * `-p, --project ` — Project the key belongs to * `-d, --description ` — Key description * `-l, --limit ` — Total spending limit in USD (e.g. `100` or `49.99`) * `--period-limit ` — Spending limit per rolling period in USD * `--period ` — Rolling period for `--period-limit` (`12h`, `1d`, `2w`, `1mo`; default `1mo`) * `-e, --expires ` — TTL as a duration (`30d`, `12h`) or an ISO date * `--json` — Output in JSON format The token is only displayed once at creation time — save it immediately. #### `keys list` [#keys-list] List API keys with spend, budget, and expiry. Alias: `keys ls`. **Options:** * `-p, --project ` — Filter by project * `--all` — Show all keys in the org (admin/owner only) * `--json` — Output in JSON format #### `keys update ` [#keys-update-id] Activate or deactivate an API key. **Options:** * `--activate` — Set the key to active * `--deactivate` — Set the key to inactive * `-e, --expires ` — New expiry as a duration (`30d`) or ISO date (needed to reactivate expired keys) #### `keys limit ` [#keys-limit-id] Set spending limits on an API key (same as `budget set`). **Options:** * `-l, --limit ` — Total spending limit in USD * `--period-limit ` — Spending limit per rolling period in USD * `--period ` — Rolling period (`12h`, `1d`, `2w`, `1mo`; default `1mo`) * `--clear` — Remove all spending limits #### `keys roll ` [#keys-roll-id] Regenerate the token for an API key. The old token becomes invalid immediately. **Options:** * `-y, --yes` — Skip confirmation #### `keys delete ` [#keys-delete-id] Delete an API key. Alias: `keys rm`. **Options:** * `-y, --yes` — Skip confirmation ### `budget` [#budget] Manage API key spending limits. ```bash # Set a total and/or rolling-period budget npx @llmgateway/cli budget set --limit 100 --period-limit 25 --period 1w # Remove all spending limits npx @llmgateway/cli budget set --clear # Show budget and current spend npx @llmgateway/cli budget get ``` **`budget set` options:** `-l, --limit `, `--period-limit `, `--period `, `--clear` **`budget get` options:** `-p, --project `, `--json` ### `usage` [#usage] View usage and cost analytics. ```bash npx @llmgateway/cli usage [options] ``` **Options:** * `-o, --org ` — Aggregate usage across an organization * `-p, --project ` — Filter by project * `-k, --api-key ` — Filter by API key * `--by ` — Break down by `model` or `key` * `-r, --range ` — Time range: `1h`, `4h`, `24h`, `7d`, `30d`, `365d` (default `7d`) * `--days ` — Look back N days instead of `--range` * `--from ` / `--to ` — Custom date range (`YYYY-MM-DD`) * `--json` — Output in JSON format **Examples:** ```bash # Last 7 days for the default project npx @llmgateway/cli usage # Cost per model over the last 30 days npx @llmgateway/cli usage --by model --range 30d # Whole-org aggregate npx @llmgateway/cli usage --org ``` #### `usage sources` [#usage-sources] Break down usage by session/agent source to see which agents or sessions are spending. ```bash npx @llmgateway/cli usage sources [options] ``` **Options:** `-p, --project `, `-r, --range ` (`7d`, `30d`), `--from `, `--to `, `--json` ### `orgs` [#orgs] List your organizations with plan and credit balance. Alias: `orgs ls`. ```bash npx @llmgateway/cli orgs list [--json] ``` ### `projects` [#projects] Manage projects and the CLI's default project. ```bash # List projects (optionally filtered by org) npx @llmgateway/cli projects list [--org ] [--json] # Set the default project used by keys/budget/usage commands npx @llmgateway/cli projects use ``` ### `credits` [#credits] Show organization credit balances. ```bash npx @llmgateway/cli credits [--org ] [--json] ``` ## Available Templates [#available-templates] ### Web Applications [#web-applications] * **`embeddable-credits`** — Monetize your AI app in 5 minutes. End-user wallets and in-app credit purchases ("Stripe for AI") with the embeddable SDK. * **`image-generation`** — Full-stack AI image generation app (Next.js 16, React 19). Multi-provider support with a unified API. * **`ai-chatbot`** — AI chatbot with streaming responses. * **`og-image-generator`** — AI-powered OG image generator. * **`feedback-dashboard`** — Customer feedback sentiment dashboard. * **`writing-assistant`** — AI writing assistant with text actions. * **`qa-agent`** — AI-powered QA testing agent with browser automation, real-time action timeline, and live browser preview. * **`showcase`** — Public, filterable gallery of apps built with PassingRight templates. Static and deployable, with a "Submit your app" flow. ### Bots [#bots] * **`slack-qa-bot`** — Slack bot that streams AI answers and keeps thread context. ### CLI Agents [#cli-agents] * **`weather-agent`** — Answers weather queries using tool calling. * **`lead-agent`** — Researches people and posts results to Discord. * **`changelog-generator-agent`** — Generates changelogs from git history. * **`email-drafter-agent`** — Drafts polished emails from rough notes. * **`sentiment-analyzer-agent`** — Analyzes text sentiment. * **`data-extractor-agent`** — Extracts structured entities from text. ```bash npx @llmgateway/cli init --template qa-agent ``` ## Configuration [#configuration] The CLI stores configuration in `~/.llmgateway/config.json`: ```json { "apiKey": "llmgtwy_...", "defaultTemplate": "image-generation", "sessionEmail": "you@example.com", "defaultOrgId": "org_...", "defaultProjectId": "proj_..." } ``` Signing in with `auth login --email` also stores a dashboard session used by the account commands (`keys`, `budget`, `usage`, `orgs`, `projects`, `credits`). ### Environment Variables [#environment-variables] * `LLMGATEWAY_API_KEY` — Gateway API key; takes precedence over the config file: ```bash export LLMGATEWAY_API_KEY="llmgtwy_..." ``` * `LLMGATEWAY_API_URL` — Override the management API base URL (defaults to `https://internal.passingright.io`), useful for self-hosted deployments. ## More Resources [#more-resources] * [Agents](https://passingright.io/agents) — Pre-built AI agents * [Templates](https://passingright.io/templates) — Production-ready starter projects * [GitHub Repository](https://github.com/theopenco/llmgateway-templates) — Source code and issues Need help or want to request a feature? Open an issue on [GitHub](https://github.com/theopenco/llmgateway-templates/issues). # DevPass Usage API URL: https://docs.passingright-staging.sandbloc.com/developers/devpass-usage DevPass subscription prices stay the same, but included usage decreases. Daily and premium weekly caps and Reset Pass benefits change on October 15; existing monthly allowances change at the first renewal on or after that date. New subscriptions from that date start with the new allowance. Review the [dated plan-change notice](https://devpass.passingright.io/legal/terms#october-2026-plan-changes) before subscribing. Use `GET /v1/key` to show DevPass allowance meters in a coding tool or other trusted application. The endpoint uses the application's existing gateway API key, so it does not require a dashboard session. A regular gateway API key is a secret. Do not embed it in public browser code or distribute it with an application. Read it from the user's secure local configuration or call the endpoint from a trusted backend. ## Request [#request] ```bash curl https://api.passingright.io/v1/key \ -H "Authorization: Bearer llmgtwy_your_api_key_here" ``` Only regular gateway API keys can use this endpoint. Platform publishable keys and end-user sessions receive `403 Forbidden` because they cannot read organization-level plan state. ## Response [#response] ```json { "data": { "label": "My coding tool", "usage": "31.42", "limit": null, "devPlan": "pro", "devPlanCreditsUsed": "25", "devPlanCreditsLimit": "237", "devPlanCreditsRemaining": "212.00", "devPlanPremiumWeeklyLimit": "35.55", "devPlanPremiumCreditsUsed": "5.00", "devPlanPremiumWeekResetsAt": "2026-08-28T12:00:00.000Z" } } ``` | Field | Meaning | | ---------------------------- | -------------------------------------------------------------- | | `label` | Description of the key | | `usage` | All-time usage attributed to the key, in USD | | `limit` | All-time key usage limit in USD, or `null` | | `devPlan` | `lite`, `pro`, `max`, or `none` | | `devPlanCreditsUsed` | Plan credits used in the current billing cycle | | `devPlanCreditsLimit` | Plan credit allowance for the current billing cycle | | `devPlanCreditsRemaining` | Remaining plan credits, clamped to zero | | `devPlanPremiumWeeklyLimit` | Weekly premium-model allowance | | `devPlanPremiumCreditsUsed` | Premium-model credits used in the current weekly window | | `devPlanPremiumWeekResetsAt` | ISO 8601 reset time, or `null` when no weekly window is active | All USD values are decimal strings. Parse them before calculating percentages or remaining premium allowance. ## Weekly window behavior [#weekly-window-behavior] The premium window starts with the first premium-model request and lasts seven days. When it expires, the endpoint returns `"0.00"` for `devPlanPremiumCreditsUsed` and `null` for `devPlanPremiumWeekResetsAt`; the full weekly allowance is already available, and the next premium request starts a new window. Pay-as-you-go keys return `"none"` for `devPlan` and zero for every DevPass field. This lets one integration support both key types without a separate account lookup. The endpoint remains readable when the key has exceeded its own usage limit, so an integration can explain why requests stopped. Invalid or inactive keys receive `401 Unauthorized`. See [Retrieve key status](https://docs.passingright-staging.sandbloc.com/v1_key_retrieve) for the generated API reference and complete response schema. # PassingRight Developer Resources URL: https://docs.passingright-staging.sandbloc.com/developers This section is for developers building applications on top of PassingRight — with our command-line tool, the MCP server, the [Vercel AI SDK](https://ai-sdk.dev) via our first-party provider package [`@llmgateway/ai-sdk-provider`](https://github.com/theopenco/llmgateway-ai-sdk-provider), and [TanStack AI](https://tanstack.com/ai) via the first-party [`@tanstack/ai-llmgateway`](https://www.npmjs.com/package/@tanstack/ai-llmgateway) adapter. ## API entry points [#api-entry-points] * [OpenAPI specification](https://passingright.io/openapi.json) — typed request, response, and error schemas * [Authentication](https://docs.passingright-staging.sandbloc.com/features/api-keys) — API keys and access control * [Developer dashboard](https://passingright.io/dashboard) — projects, keys, usage, and budgets * [API versioning and deprecation policy](https://docs.passingright-staging.sandbloc.com/resources/api-versioning) — compatibility and retirement notices ## Guides [#guides] * [**DevPass Usage API**](https://docs.passingright-staging.sandbloc.com/developers/devpass-usage) — Show monthly and weekly DevPass allowance meters in your application * [**PassingRight CLI**](https://docs.passingright-staging.sandbloc.com/developers/cli) — Launch coding agents, scaffold projects from templates, generate agent configs, and manage keys, budgets, and usage from the terminal * [**Model Context Protocol (MCP)**](https://docs.passingright-staging.sandbloc.com/developers/mcp) — Use PassingRight as an MCP server from Claude Code, Cursor, and other MCP clients * [**Using the AI SDK**](https://docs.passingright-staging.sandbloc.com/developers/ai-sdk) — Install the provider, generate and stream text, call tools, and wire up Next.js routes * [**Image Generation with the AI SDK**](https://docs.passingright-staging.sandbloc.com/developers/ai-sdk-images) — Generate images with `generateImage` and stream image output through chat * [**Using TanStack AI**](https://docs.passingright-staging.sandbloc.com/developers/tanstack-ai) — Stream chat with `useChat`, call tools, and surface reasoning through the first-party `@tanstack/ai-llmgateway` adapter ## Why the AI SDK [#why-the-ai-sdk] The AI SDK gives you one TypeScript interface for text generation, streaming, tool calling, and image generation. Combined with PassingRight, a single provider instance and one API key reach every model in the catalog — see the [models page](https://passingright.io/models) for what's available. ## Other ways to integrate [#other-ways-to-integrate] If you're not using the AI SDK: * Use any OpenAI-compatible SDK against `https://api.passingright.io/v1` — see the [Quickstart](https://docs.passingright-staging.sandbloc.com/quick-start) * Use the Anthropic SDK against the [Anthropic-compatible endpoint](https://docs.passingright-staging.sandbloc.com/features/anthropic-endpoint) * Call the REST API directly — see the API reference in the sidebar ## Brand assets [#brand-assets] For integration listings, presentations, and partner pages, download the official [PassingRight brand assets](https://passingright.io/brand) in SVG or transparent PNG. The brand guide covers the horizontal logo, standalone symbol, clear space, minimum sizes, background colors, and typography. # PassingRight MCP Server URL: https://passingright.io/guides/mcp Connect your AI assistant to PassingRight to inspect your usage and costs, discover your most-used models, providers and coding apps, and generate text or images. The same API key connects all of these tools. **Using DevPass?** This integration also works with a [DevPass](https://devpass.passingright.io) plan key. Use canonical model IDs without a provider prefix (`model-id` instead of `provider/model-id`) — provider-pinned routing is not available on coding plans; the gateway picks the provider for you. ## Connection and discovery [#connection-and-discovery] Connect with Streamable HTTP at `https://passingright.io/mcp` or `https://api.passingright.io/mcp`. Send an API key in `Authorization: Bearer `. A GET without an SSE Accept header returns public server information; browser navigation on the main domain opens the setup page. For protocol requests, POST a single JSON-RPC message with `Content-Type: application/json` and `Accept: application/json, text/event-stream`. Initialize first, then send the negotiated `MCP-Protocol-Version` header on subsequent requests. The transport is stateless: requests return JSON, accepted notifications return an empty 202, and standalone SSE subscriptions and session deletion return 405. Clients using the original HTTP+SSE bridge remain supported. [Protected resource metadata](https://api.passingright.io/.well-known/oauth-protected-resource/mcp) publishes authentication discovery. Unauthorized protocol requests include a `WWW-Authenticate` challenge pointing to it. ## What is MCP? [#what-is-mcp] The Model Context Protocol (MCP) is an open standard that allows AI assistants to connect with external tools and data sources. PassingRight's MCP server exposes tools for: * **Account and usage analytics** - Check spending limits, request/token totals, costs, trends, and provider/model/app rankings * **Chat completions** - Send messages to any supported LLM * **Image generation** - Generate images using models like Qwen Image * **Nano Banana image generation** - Generate images with Gemini 3 Pro Image and optionally save to disk * **Model discovery** - List available models with capabilities and pricing ## Available Tools [#available-tools] ### `get-account` [#get-account] Inspect the connected user, organization, project, role, analytics scope, and API key spending limits. Owners and admins also receive the organization's current credit balance. The balance is not a DevPass plan allowance. No parameters are required, and credentials are never returned. ### `get-usage` [#get-usage] Get request/token totals, errors, cache hits, costs, a time series, and your most-used provider, model, and coding agent/app **by request count**. | Parameter | Description | | ------------- | ---------------------------------------------------------------------------------- | | `from` | Optional first UTC date, `YYYY-MM-DD`, inclusive. Defaults to 29 days before `to`. | | `to` | Optional last UTC date, inclusive. Defaults to today. Maximum range: 366 days. | | `granularity` | `day` (default) or `hour`. Hourly reports allow at most 31 days. | ```json { "from": "2026-08-01", "to": "2026-08-31", "granularity": "day" } ``` The response includes `scope`, the resolved dates, `updatedAt`, `totals`, `series`, `mostUsedProvider`, `mostUsedModel`, `mostUsedApp`, and `appUsageCoverage`. Only time buckets with activity appear in `series`. Empty periods return zero totals, an empty series, and null rankings. ### `get-usage-breakdown` [#get-usage-breakdown] Rank providers, models, coding apps, or API keys by requests, inference cost, or tokens. | Parameter | Description | | ------------ | ----------------------------------------------------------------------- | | `group_by` | Required: `provider`, `model`, `app`, or `api_key`. | | `sort_by` | `requests` (default), `cost`, or `tokens`, descending. Ties use the ID. | | `from`, `to` | Same inclusive UTC dates as `get-usage`. | | `limit` | Results per page: 1–100, default 10. | | `offset` | Results to skip: 0–10000, default 0. | ```json { "group_by": "app", "sort_by": "cost", "limit": 10 } ``` The response includes each row's ID, display name, requests, tokens and costs, plus `pagination.hasMore` and `coverage`. Increase `offset` by `limit` to get the next page. Known app aliases are combined before ranking. `unknown` identifies requests with no recorded source; custom app names remain as recorded. ### Analytics scope and cost fields [#analytics-scope-and-cost-fields] * Owners and admins see the **connected project's** usage across its keys. Developers see only the keys they created in that project, including inactive-key history. * A project API key does not grant access to another project or organization. Tools do not accept scope overrides. Use a key for the project you want to inspect. * Use an active user API key. Customer credentials and expired or revoked keys cannot read account analytics. Project access is checked on every analytics request. * Analytics tools are read-only, incur no model charges, and remain available when a key or member reaches a spending limit. Generation tools still enforce those limits. * `costUsd` is inference usage cost. `creditsCostUsd` and `byokCostUsd` separate gateway credits from provider costs paid with your own provider keys. `dataStorageCostUsd` is separate. These are usage statistics, not invoice totals or exact changes in credit balance. * Statistics come from hourly aggregates, survive request-retention cleanup, and may lag recent requests. `updatedAt` reports the last summary aggregation in the requested period. * App attribution uses the request's recorded source, including recognized coding clients and `x-source` values. MCP generation calls preserve client attribution headers. Configure `x-source` on your MCP connection if your client does not identify itself. Attribution does not identify which person used a shared key. * Historical per-key app statistics start when per-key source aggregation is enabled. `appUsageCoverage` / `coverage` compare recorded breakdown requests with total requests; `complete: false` means rankings cover only part of the period. This differs from an `unknown` source, which is a recorded request without app attribution. All three tools return JSON in both `structuredContent` and a text content block for older clients. An unavailable backend produces a tool error, never a fabricated zero-usage report. ### `chat` [#chat] Send a message to any LLM and get a response. **Parameters:** * `model` (string) - A model ID from `list-models` or the [live catalog](https://passingright.io/models) * `messages` (array) - Array of messages with `role` and `content` * `temperature` (number, optional) - Sampling temperature (0-2) * `max_tokens` (number, optional) - Maximum tokens to generate **Example:** ```json { "model": "MODEL_ID", "messages": [{ "role": "user", "content": "Explain quantum computing" }], "temperature": 0.7 } ``` ### `generate-image` [#generate-image] Generate images from text prompts using AI image models. **Parameters:** * `prompt` (string) - Text description of the image to generate * `model` (string, optional) - Image model (default: `"qwen-image-3.0"`) * `size` (string, optional) - Image size (default: `"1024x1024"`) * `n` (number, optional) - Number of images (1-4, default: 1) **Example:** ```json { "prompt": "A serene mountain landscape at sunset", "model": "qwen-image-3.0-pro", "size": "1024x1024" } ``` ### `generate-nano-banana` [#generate-nano-banana] Generate an image using Gemini 3 Pro Image ("Nano Banana Pro"). Returns an inline image preview, and optionally saves the image to disk when the server is configured with an upload directory. **Parameters:** * `prompt` (string) - Text description of the image to generate * `filename` (string, optional) - Filename for the saved image, no path separators allowed (default: `nano-banana-{timestamp}.png`) * `aspect_ratio` (string, optional) - Aspect ratio: `"1:1"`, `"16:9"`, `"4:3"`, or `"5:4"` **Example:** ```json { "prompt": "A pixel-art cat sitting on a rainbow", "filename": "hero-image.png", "aspect_ratio": "16:9" } ``` **Saving images to disk** requires the `UPLOAD_DIR` environment variable to be set on the MCP server. When set, images are saved to that directory. Without it, images are returned inline only — no files are written to disk. See [Enabling local image saving](#enabling-local-image-saving) for setup instructions. ### `list-models` [#list-models] List available LLM models with capabilities and pricing. **Parameters:** * `include_deactivated` (boolean, optional) - Include deactivated models * `exclude_deprecated` (boolean, optional) - Exclude deprecated models * `limit` (number, optional) - Maximum models to return (default: 20) * `family` (string, optional) - Filter by model family ### `list-image-models` [#list-image-models] List all available image generation models. Use the tool for current model IDs, capabilities, and pricing, or browse the [live models page](https://passingright.io/models). ## Setup [#setup] ### Get Your API Key [#get-your-api-key] 1. Log in to your [PassingRight dashboard](https://passingright.io/dashboard) 2. Navigate to **API Keys** section 3. Create a new API key and copy it ### Configure Claude Code [#configure-claude-code] Run the following command in your terminal: ```bash claude mcp add --transport http --scope user llmgateway https://api.passingright.io/mcp \ --header "Authorization: Bearer your-api-key-here" ``` **Alternative: Manual configuration** You can also add the MCP server manually by editing `~/.claude.json` (user scope) or `.mcp.json` in your project root (project scope): ```json { "mcpServers": { "llmgateway": { "url": "https://api.passingright.io/mcp", "headers": { "Authorization": "Bearer your-api-key-here" } } } } ``` Restart Claude Code after manual configuration changes. ### Test the Integration [#test-the-integration] Try using the tools in Claude Code: * "Show my usage and costs for the last 30 days" * "Generate an image of a futuristic city using the generate-image tool" * "Use generate-nano-banana to create a hero image for my landing page" * "Which model, provider, and coding app do I use most?" ### Get Your API Key [#get-your-api-key-1] 1. Log in to your [PassingRight dashboard](https://passingright.io/dashboard) 2. Navigate to **API Keys** section 3. Create a new API key and copy it 4. Set it as an environment variable: `export LLM_GATEWAY_API_KEY="your-api-key-here"` ### Configure Codex [#configure-codex] Run the following command in your terminal: ```bash codex mcp add llmgateway --url https://api.passingright.io/mcp \ --bearer-token-env-var LLM_GATEWAY_API_KEY ``` **Alternative: Manual configuration** You can also add the MCP server manually by editing `~/.codex/config.toml`: ```toml [mcp_servers.llmgateway] url = "https://api.passingright.io/mcp" bearer_token_env_var = "LLM_GATEWAY_API_KEY" ``` ### Test the Integration [#test-the-integration-1] Run `/mcp` in the Codex TUI to confirm the `llmgateway` server is connected. Try: * "Show my usage and costs for the last 30 days" * "Generate an image of a futuristic city using the generate-image tool" * "Use generate-nano-banana to create a hero image for my landing page" * "Which model, provider, and coding app do I use most?" ### Get Your API Key [#get-your-api-key-2] 1. Log in to your [PassingRight dashboard](https://passingright.io/dashboard) 2. Navigate to **API Keys** section 3. Create a new API key and copy it ### Configure Cursor [#configure-cursor] Add the following to your Cursor MCP configuration file (`~/.cursor/mcp.json`): ```json { "mcpServers": { "llmgateway": { "url": "https://api.passingright.io/mcp", "headers": { "Authorization": "Bearer your-api-key-here" } } } } ``` Or open the Command Palette (`Cmd/Ctrl + Shift + P`), search for **"Cursor Settings"**, then go to **Tools & Integrations** > **Add Custom MCP** and paste the configuration above. Cursor v0.48.0+ is required for Streamable HTTP MCP support. ### Test the Integration [#test-the-integration-2] Open a chat in **Agent Mode**, click the **Select Tools** icon, and verify the PassingRight tools appear. Try: * "Show my usage and costs for the last 30 days" * "Generate an image of a futuristic city using the generate-image tool" * "Use generate-nano-banana to create a hero image for my landing page" * "Which model, provider, and coding app do I use most?" PassingRight's MCP server supports the standard HTTP Streamable transport. Configure your client with: * **Endpoint:** `https://api.passingright.io/mcp` * **Authentication:** Bearer token via `Authorization` header or `x-api-key` header * **Protocol Version:** 2024-11-05 **Direct HTTP Example:** ```bash curl -X POST https://api.passingright.io/mcp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer your-api-key" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }' ``` **Server-Sent Events (SSE):** For real-time updates, connect with `Accept: text/event-stream`: ```bash curl -N https://api.passingright.io/mcp \ -H "Accept: text/event-stream" \ -H "Authorization: Bearer your-api-key" ``` ## Use Cases [#use-cases] ### Usage and Spending [#usage-and-spending] ```text What did I spend this month, and which coding app accounts for the most cost? Show my most-used model and provider, then compare daily usage with last month. ``` Use `get-account` to confirm the scope, `get-usage` for each period, and `get-usage-breakdown` with `group_by: "app"` and `sort_by: "cost"` for the app ranking. ### Multi-Model Access in Claude Code [#multi-model-access-in-claude-code] Use Claude Code to interact with models it doesn't natively support: ``` List available models, then use the chat tool with a suitable model to review this code. ``` ### Image Generation [#image-generation] Generate images directly from your AI assistant: ``` Use generate-image to create a logo for my new startup. It should be minimalist, blue and white, representing AI and cloud computing. ``` ### Nano Banana (Gemini Image Generation) [#nano-banana-gemini-image-generation] Generate images with Gemini 3 Pro for use in your project: ``` Use generate-nano-banana to create a hero image for my landing page with a 16:9 aspect ratio. ``` ### Cost-Effective Model Selection [#cost-effective-model-selection] Query available models to find the best option for your task: ``` List models and their pricing, then choose a suitable low-cost model for this task. ``` ## Authentication [#authentication] The MCP server supports two authentication methods: 1. **Bearer Token** - `Authorization: Bearer your-api-key` 2. **API Key Header** - `x-api-key: your-api-key` Use the same project API key you use for inference. Analytics access follows the [scope rules above](#analytics-scope-and-cost-fields). ## OAuth Support [#oauth-support] For applications that prefer OAuth authentication, PassingRight's MCP server implements OAuth 2.0: * **Authorization Endpoint:** `/oauth/authorize` * **Token Endpoint:** `/oauth/token` * **Registration Endpoint:** `/oauth/register` * **Supported Flows:** Authorization Code, Client Credentials ## Enabling Local Image Saving [#enabling-local-image-saving] By default, `generate-nano-banana` returns images inline without writing to disk. To enable saving generated images to the server filesystem, the `UPLOAD_DIR` environment variable must be set on the **gateway host** at startup. This is a server-side setting — it cannot be configured from the client. This is only possible for **self-hosted** MCP deployments. Configure `UPLOAD_DIR` using your deployment method: * **Docker:** Pass `-e UPLOAD_DIR=/data/images` or add it to your `docker-compose.yml` environment section. * **systemd:** Add `Environment=UPLOAD_DIR=/data/images` to your service unit file. * **.env file:** Add `UPLOAD_DIR=/data/images` to the `.env` file loaded by your gateway process. The shared hosted endpoint (`api.passingright.io`) does not support configuring `UPLOAD_DIR`. On the hosted service, images are always returned inline — no files are written to disk. To enable server-side image saving, you must self-host the MCP server and set `UPLOAD_DIR` at startup. ## Troubleshooting [#troubleshooting] ### Connection Errors [#connection-errors] If you're having trouble connecting: 1. Verify your API key is valid 2. Check the endpoint URL is correct: `https://api.passingright.io/mcp` 3. Ensure your firewall allows outbound HTTPS connections ### Tool Not Found [#tool-not-found] If tools aren't appearing: 1. Restart your MCP client 2. Check the configuration syntax 3. Verify the MCP server is responding: `GET https://api.passingright.io/mcp` ### Rate Limiting [#rate-limiting] The MCP server respects your account's rate limits. If you're hitting limits: 1. Check your usage in the dashboard 2. Consider upgrading your plan 3. Implement request queuing in your application Need help? Join our [Discord community](https://passingright.io/discord) for support. ## Benefits [#benefits] * **Unified Access** - Discover the [live model and provider catalog](https://passingright.io/models) through one interface * **Cost Tracking** - Ask your assistant about usage, spending, and your most-used models, providers, and apps * **Caching** - Automatic response caching reduces costs and latency * **Fallback** - Automatic provider failover ensures reliability * **Image Generation** - Generate images directly from your AI assistant # Using TanStack AI URL: https://docs.passingright-staging.sandbloc.com/developers/tanstack-ai [TanStack AI](https://tanstack.com/ai) ships a first-party PassingRight adapter: [`@tanstack/ai-llmgateway`](https://www.npmjs.com/package/@tanstack/ai-llmgateway), maintained in the TanStack AI repository alongside the OpenAI and Anthropic adapters. One adapter and one API key reach every model in the catalog. ## Install [#install] ```bash pnpm add @tanstack/ai @tanstack/ai-react @tanstack/ai-llmgateway ``` `@tanstack/ai-react` is the React client; TanStack AI also ships Vue, Svelte, Angular, and Preact packages that work with the same adapter. Set your API key (create one from the [dashboard](https://passingright.io/dashboard)): ```bash export LLM_GATEWAY_API_KEY=llmgtwy_your_key_here ``` ## Stream chat from a server route [#stream-chat-from-a-server-route] `llmGatewayText(model)` creates the adapter and reads the key from `LLM_GATEWAY_API_KEY`: ```typescript // app/api/chat/route.ts import { chat, toServerSentEventsResponse } from "@tanstack/ai"; import { llmGatewayText } from "@tanstack/ai-llmgateway"; export async function POST(request: Request) { const { messages } = await request.json(); const stream = chat({ adapter: llmGatewayText("gpt-5.6-terra"), messages, }); return toServerSentEventsResponse(stream); } ``` Switching models is a one-line change — the same adapter serves every model: ```typescript adapter: llmGatewayText("claude-sonnet-5"), ``` ## Model ID formats [#model-id-formats] PassingRight supports two model ID formats: * **Canonical model IDs** (`gpt-5.6-terra`) — smart routing picks the best provider based on uptime, throughput, price, and latency * **Provider-prefixed IDs** (`moonshot/kimi-k3`) — routes to a specific provider with automatic failover if uptime drops below 90% A curated set of flagship models carries typed metadata (`LLMGATEWAY_CHAT_MODELS`) with editor autocomplete for input modalities and options; any other ID from the [models page](https://passingright.io/models) still works. See the [routing documentation](https://docs.passingright-staging.sandbloc.com/features/routing) for details. ## Connect the React client [#connect-the-react-client] `useChat` consumes the AG-UI event stream from the route above — no client-side API key, no per-provider wiring: ```tsx // components/chat.tsx "use client"; import { fetchServerSentEvents, useChat } from "@tanstack/ai-react"; import { useState } from "react"; export function Chat() { const [input, setInput] = useState(""); const { messages, sendMessage, isLoading } = useChat({ connection: fetchServerSentEvents("/api/chat"), }); return (
{messages.map((message) => (
{message.role === "assistant" ? "Assistant" : "You"} {message.parts.map((part, index) => part.type === "text" ?

{part.content}

: null, )}
))}
{ event.preventDefault(); if (!input.trim() || isLoading) { return; } sendMessage(input); setInput(""); }} > setInput(event.target.value)} placeholder="Say something..." />
); } ``` ## Tool calling [#tool-calling] Define tools with `toolDefinition` and attach a server handler — TanStack AI runs the tool loop for you: ```typescript import { chat, toServerSentEventsResponse, toolDefinition } from "@tanstack/ai"; import { llmGatewayText } from "@tanstack/ai-llmgateway"; import { z } from "zod"; const getWeather = toolDefinition({ name: "get_weather", description: "Get the current weather for a location", inputSchema: z.object({ location: z.string(), }), }).server(async ({ location }) => { return { temperature: 72, condition: "sunny" }; }); export async function POST(request: Request) { const { messages } = await request.json(); const stream = chat({ adapter: llmGatewayText("gpt-5.6-terra"), messages, tools: [getWeather], }); return toServerSentEventsResponse(stream); } ``` ## Reasoning models [#reasoning-models] Reasoning models stream their thinking as `reasoning_content` deltas, which the adapter surfaces as AG-UI `REASONING_*` events — they arrive in `useChat` as `thinking` parts. Control the depth with `reasoning_effort` in `modelOptions`: ```typescript const stream = chat({ adapter: llmGatewayText("kimi-k3"), messages, modelOptions: { temperature: 0.7, reasoning_effort: "high", }, }); ``` `reasoning_effort` accepts the extended scale `none` / `minimal` / `low` / `medium` / `high` / `xhigh` / `max`; which tiers a model honors depends on the model and the provider it routes to. Parameters a routed provider doesn't support are stripped server-side, so `modelOptions` stay portable across models. See [reasoning support](https://docs.passingright-staging.sandbloc.com/features/reasoning). ## Summarization [#summarization] The adapter also covers TanStack AI's `summarize` surface: ```typescript import { summarize } from "@tanstack/ai"; import { llmGatewaySummarize } from "@tanstack/ai-llmgateway"; const result = await summarize({ adapter: llmGatewaySummarize("gpt-5.4-mini"), text: "Long article text...", stream: false, }); ``` ## Self-hosted deployments [#self-hosted-deployments] `createLLMGatewayText` takes the key explicitly plus a `baseURL` for self-hosted gateways: ```typescript import { createLLMGatewayText } from "@tanstack/ai-llmgateway"; const adapter = createLLMGatewayText( "gpt-5.6-terra", process.env.LLM_GATEWAY_API_KEY!, { baseURL: "https://gateway.internal.example.com/v1", }, ); ``` Every request made through TanStack AI shows up in your [Activity](https://docs.passingright-staging.sandbloc.com/learn/activity) and [Usage & Metrics](https://docs.passingright-staging.sandbloc.com/learn/usage-metrics) dashboards like any other gateway request — with per-request cost, tokens, and latency. ## Next steps [#next-steps] * [TanStack AI adapter reference](https://tanstack.com/ai/latest/docs/adapters/llmgateway) * [Routing and fallback](https://docs.passingright-staging.sandbloc.com/features/routing) * [Reasoning support](https://docs.passingright-staging.sandbloc.com/features/reasoning) and [caching](https://docs.passingright-staging.sandbloc.com/features/caching) # Airside for Providers URL: https://docs.passingright-staging.sandbloc.com/features/airside [Airside](https://providers.passingright.io) is the self-serve console for the companies behind the models — the [providers](https://passingright.io/providers) PassingRight routes to. It runs on an airport metaphor: PassingRight is the airport, developers are passengers, and providers are **carriers**. As a carrier you claim your provider, register your fleet of models, file your fares, and watch traffic arrive. ## Claiming your carrier [#claiming-your-carrier] Sign up at [providers.passingright.io](https://providers.passingright.io) with your **company email** and verify it. A catalogue provider is claimable when the registrable domain of your verified email matches the registrable domain of the provider's API endpoint (or its website) — if your API is served from `api.example.ai`, an `@example.ai` address can claim it. Subdomains collapse to the registrable domain, so `ops@mail.example.ai` works too. Every claim is reviewed by the PassingRight team before the carrier goes live; a rejected claim shows the review note so you can follow up. One company can operate several carriers — regional deployments or separate brands all live under one console. Listing on passingright.io carries a one-time **$2,500 listing fee** per provider company, paid during onboarding. Providers we already work with receive an **invite code** that waives it — entered in the same onboarding step instead of paying. See the [pricing summary](https://providers.passingright.io/pricing.md) for the full economics. ## Crew [#crew] A listing covers your whole team. Under **Crew**, company owners invite teammates by email — up to **10 members** per company, pending invites included. Invites are limited to your team's domains (the ones that prove your claims); a teammate with an existing account joins instantly, anyone else joins the first time they sign in with the invited address. ## Registering a new carrier [#registering-a-new-carrier] Not in the [provider catalogue](https://passingright.io/providers) yet? Register your provider as a **new carrier** from the onboarding page: pick a carrier id, display name, and the base URL of your **OpenAI-compatible API** (the gateway calls `/v1/chat/completions`, unless a listing picks a different [upstream API](#upstream-api-and-preflight)). The endpoint must live on your verified email's domain — the same anti-squatting rule as claiming — and registrations go through the same review. Once approved, your carrier appears on the public providers and models pages and its listings route like any other provider's. ## Listing your fleet [#listing-your-fleet] Once your claim is active, register models under **Fleet**: model name, display name, description, context size, max output, capability flags (streaming, vision, audio input, tools, JSON output, reasoning), and the supported `reasoning_effort` tiers. A new listing is created as a draft together with an **initial price filing**; when the PassingRight team approves that filing, the model goes active. Everything except pricing stays freely editable afterward. Carriers claiming a catalogue provider can also **import their catalogue models** in one click: each active catalogue model becomes a managed listing with its current price as an approved filing. Routing keeps using the catalogue entry until the PassingRight team retires it — from then on the Airside listing (and its filed fares) takes over. This is how a provider's models migrate from the static catalogue into carrier self-management. ### Upstream API and preflight [#upstream-api-and-preflight] Each listing also picks the **upstream API** the gateway speaks to it: the carrier default, OpenAI Chat Completions, OpenAI Responses, or Google Vertex `generateContent`. The choice is independent of the capabilities you declare — tool calls, JSON output, vision, reasoning, and web search are all probed and served through whichever API you picked, so pick the one your endpoint actually implements. It is fixed once the model is listed. Before a listing can be filed, Airside runs a **preflight** against your endpoint through that API: one check per declared capability. You supply a provider API key that can call the model — it is used only by that run and erased when it finishes. Editing the model invalidates the run, so a listing never ships with checks made against different settings. Re-verifying a live listing has teeth: every capability a check disproves is switched off on the listing (and on the mapping that serves it), so routing stops sending it traffic it just failed. A failed check on reasoning also clears the reasoning budget and effort tiers. The listing keeps serving on what still works, and Fleet marks it **In service · unverified** until a run passes again. ## Price filings [#price-filings] Listed prices are what developers are billed, so pricing never changes silently: it only changes through a reviewed **price filing**. * The **initial** filing sets the launch pricing and activates the model on approval. * Every later price change is an **update** filing — drafted by you, approved by the PassingRight team, and only then in effect. A filing covers input, output, optional cached-input, and optional per-request prices (USD per token, decimal or exponent notation), plus a note for the reviewer. It can also carry **regional fares** — per-region price overrides developers reach with `/:`, while all other traffic pays the default fares. Each approved filing's regional set fully replaces the previous one, and while no filing is pending a region can be dropped from the Fleet page — removing an offering changes no price, so it applies immediately without review. The latest approved filing is the model's effective price; pending and rejected filings are tracked under **Filings**. ## Routing and billing [#routing-and-billing] Approved listings are genuinely routable. A request for `/` that does not match the static catalogue resolves against active Airside listings with an approved price filing, and is billed at the filed prices — no extra configuration on the developer side. ## Traffic [#traffic] The **Traffic** page shows usage of your claimed providers: requests, errors, input/output/total tokens, and billed traffic in USD, as a daily series and broken down per model. Chart axes and compact counts use `k`, `M`, `B`, or `T` for thousands through trillions, so large traffic volumes fit within the chart. ## Incidents [#incidents] The **Incidents** page lists every mapping of your claimed providers that failed requests in the selected window (1 hour to 3 days), with its error rate, error count, upstream/gateway split, and total requests. Client errors — requests the caller got wrong — are excluded. Expand a row to see the top error shapes behind it: HTTP status, status text, a response excerpt, the cause, the gateway's classification, and whether the request was streamed. Counts always include retried attempts; the **Retried errors in details** switch drops retried attempts from the error shapes only. To focus on one mapping, pick it from the mapping filter or open `/dashboard/incidents?mapping=/[:]`. The **Errors** column on Traffic and the **Incidents** link on each active Fleet listing open the page pre-filtered. ## Fares and routing [#fares-and-routing] Under **Fares** each claimed provider has two knobs: * **Traffic discount** (0–50%) — a discount you offer on routed traffic. * **Landing fee** (5–50%) — the gateway margin you accept; the baseline is **20%**. Accepting more than the baseline margin or offering a discount makes your traffic effectively cheaper, which boosts you in the [smart routing election](https://docs.passingright-staging.sandbloc.com/features/routing); accepting less prices you up. Like price changes, fare changes are filed for review and only reach routing once the PassingRight team approves them. The election scores every candidate provider on weighted factors — expected token cost after your discount and margin (`0.6`), availability/uptime (`0.5`), throughput (`0.05`), and latency (`0.025`) — and the lowest score wins. Cache-read savings count toward token cost; cache support alone receives no additional preference by default. Routing reacts to live metrics: reliability and speed still matter. A cheap but flaky deployment loses the election to a slightly pricier, stable one. ## Carrier Slack channel [#carrier-slack-channel] Every new carrier is invited to a shared cross-team Slack Connect channel with the PassingRight team — the fastest way to reach us about claims, filings, or routing. # Anthropic API Compatibility URL: https://docs.passingright-staging.sandbloc.com/features/anthropic-endpoint PassingRight provides a native Anthropic-compatible endpoint at `/v1/messages` that allows you to use any model in our catalog while maintaining the familiar Anthropic API format This is especially useful for applications designed for Claude that you want to extend to use other models. Enjoy a 50% discount on our Anthropic models for a limited time. ## Overview [#overview] The Anthropic endpoint transforms requests from Anthropic's message format to the OpenAI-compatible format used by PassingRight, then transforms the responses back to Anthropic's format. This means you can: * Use **any model** available in PassingRight with Anthropic's API format * Maintain existing code that uses Anthropic's SDK or API format * Access models from OpenAI, Google, Cohere, and other providers through the Anthropic interface * Leverage PassingRight's routing, caching, and cost optimization features ## Basic Usage [#basic-usage] ## Configuration for Claude Code [#configuration-for-claude-code] This endpoint is perfect for configuring Claude Code to use any model available in PassingRight: ```bash export ANTHROPIC_BASE_URL=https://api.passingright.io export ANTHROPIC_AUTH_TOKEN=llmgtwy_your_api_key_here # optional: specify a model, otherwise it uses the default Claude model export ANTHROPIC_MODEL=gpt-5 # or any model from our catalog # now run claude! claude ``` Environment variables are read once at startup. The `/model` picker lists Claude models only, so non-Claude models are selected with `ANTHROPIC_MODEL` or `--model`. See the [Claude Code guide](https://docs.passingright-staging.sandbloc.com/guides/claude-code) for the settings-file options and gateway model discovery. ### Choosing Models [#choosing-models] You can use any model from the [models page](https://passingright.io/models). Popular options for Claude Code include: ```bash # Use OpenAI's latest model export ANTHROPIC_MODEL=gpt-5 # Use a cost-effective alternative export ANTHROPIC_MODEL=gpt-5-mini # Use Google's Gemini export ANTHROPIC_MODEL=gemini-3.1-pro-preview # Use Anthropic's actual Claude models export ANTHROPIC_MODEL=claude-3-5-sonnet-20241022 ``` ## Environment Variables [#environment-variables] When configuring Claude Code or other Anthropic-compatible applications, you can use these environment variables: ### ANTHROPIC\_MODEL [#anthropic_model] Specifies the main model to use for primary requests. * **Default**: `claude-sonnet-4-20250514` * **Example**: `export ANTHROPIC_MODEL=gpt-5` ### ANTHROPIC\_SMALL\_FAST\_MODEL [#anthropic_small_fast_model] Specifies a smaller, faster model used for background functionality and internal operations. * **Default**: `claude-3-5-haiku-20241022` * **Example**: `export ANTHROPIC_SMALL_FAST_MODEL=gpt-5-nano` ```bash # Example configuration export ANTHROPIC_BASE_URL=https://api.passingright.io export ANTHROPIC_AUTH_TOKEN=llmgtwy_your_api_key_here export ANTHROPIC_MODEL=gpt-5 export ANTHROPIC_SMALL_FAST_MODEL=gpt-5-nano ``` ## Advanced Features [#advanced-features] ### Making a manual request [#making-a-manual-request] ```bash curl -X POST "https://api.passingright.io/v1/messages" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-5", "messages": [ {"role": "user", "content": "Hello, how are you?"} ], "max_tokens": 100 }' ``` ### Response Format [#response-format] The endpoint returns responses in Anthropic's message format: ```json { "id": "msg_abc123", "type": "message", "role": "assistant", "model": "gpt-5", "content": [ { "type": "text", "text": "Hello! I'm doing well, thank you for asking. How can I help you today?" } ], "stop_reason": "end_turn", "stop_sequence": null, "usage": { "input_tokens": 13, "output_tokens": 20 } } ``` ### Request Format [#request-format] `/v1/messages` expects Anthropic Messages requests and always answers in Anthropic's format. Because the two formats share `model` and `messages`, an OpenAI Chat Completions body can reach this endpoint by accident — and since unknown parameters are ignored rather than rejected, the request succeeds and returns an Anthropic response body that OpenAI SDKs cannot read. If your client reports an empty completion here, check that it is pointed at `/v1/chat/completions`. Unknown parameters are deliberately ignored rather than rejected, so a valid Anthropic request is never denied for carrying an extra field. As a consequence, OpenAI-only parameters (`response_format`, `stream_options`, `max_completion_tokens`, `n`, `stop`, `seed`, `frequency_penalty`, and similar) have no effect here — the model will not honour them. Use `/v1/chat/completions` if you need them. A body that is *structurally* OpenAI is rejected by the schema, as it always has been — OpenAI-shaped `tools` (`{"type": "function", "function": {…}}`), OpenAI content parts such as `image_url`, or assistant turns with `content: null`. Those rejections now name the mismatch and point at the right endpoint instead of returning an opaque validation error: ```json { "type": "error", "error": { "type": "invalid_request_error", "message": "This endpoint implements Anthropic's Messages API, and the request body uses OpenAI Chat Completions structures (tools[0].function) that Anthropic's format has no equivalent for. Send OpenAI-format requests to /v1/chat/completions instead, or convert the body to Anthropic's Messages format." } } ``` Rejected requests are recorded in your logs with a `client_error` finish reason and zero cost, so a malformed client is visible in the activity feed rather than failing silently. ### Prompt Caching [#prompt-caching] For Claude models, `cache_control` markers on `system` and message content blocks are forwarded to the provider unchanged, including the optional `ttl` (`5m` or `1h`): ```json { "model": "claude-sonnet-4-6", "max_tokens": 100, "system": [ { "type": "text", "text": "", "cache_control": { "type": "ephemeral" } } ], "messages": [{ "role": "user", "content": "Hello!" }] } ``` Cache usage comes back in Anthropic's native fields: `usage.cache_creation_input_tokens` (tokens written to the cache this request, billed at the write premium), `usage.cache_read_input_tokens` (tokens served from cache at the discounted rate), and `usage.cache_creation` (the per-TTL write breakdown). Each Claude model has a minimum cacheable prompt length (it varies by model and is exposed as `min_cacheable_tokens` on `/v1/models`). A `cache_control` marker on a shorter prompt is accepted but silently not cached — both cache usage fields stay `0`. See [Provider Cache Control](https://docs.passingright-staging.sandbloc.com/features/caching/provider-cache-control) for details. ### Web Search [#web-search] Anthropic's server-side web search tool works on this endpoint. Pass it as usual and the response carries `server_tool_use` and `web_search_tool_result` blocks before the text that cites them, so Anthropic SDK clients surface sources: ```json { "model": "claude-haiku-4-5", "max_tokens": 400, "messages": [{ "role": "user", "content": "What shipped in Node 24?" }], "tools": [{ "type": "web_search_20250305", "name": "web_search" }] } ``` Replaying the assistant turn verbatim on the next request is supported: the `server_tool_use` and `web_search_tool_result` blocks are accepted and dropped, since the provider re-runs the search rather than reusing the previous results. ### Tool Search [#tool-search] Anthropic's server-side [tool search](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool) works on this endpoint. Pass a `tool_search_tool_*` tool alongside your catalog and mark the tools that should load on demand with `defer_loading: true`: ```json { "model": "claude-sonnet-4-6", "max_tokens": 1024, "messages": [{ "role": "user", "content": "What is the weather in Paris?" }], "tools": [ { "type": "tool_search_tool_regex_20251119", "name": "tool_search_tool_regex" }, { "name": "get_weather", "description": "Get the weather at a specific location", "input_schema": { "type": "object" }, "defer_loading": true } ] } ``` Deferred tools stay out of the rendered tools section, so adding one does not invalidate an existing prompt cache. The response carries the `server_tool_use` and `tool_search_tool_result` blocks; replay them verbatim on the next request and Anthropic keeps expanding the `tool_reference` entries they carry, so Claude reuses a discovered tool instead of searching again. `tool_reference` blocks returned from your own client-side search inside a `tool_result` are forwarded unchanged too. Send every tool definition on every request, including the deferred ones — Anthropic needs them server-side to run the search. At least one tool must stay non-deferred (normally the tool search tool itself), and a tool cannot carry both `defer_loading: true` and `cache_control`. **Where it works.** Tool search reaches the provider on the Anthropic API and on Anthropic models served through Google Cloud, and it needs a Claude 4.5-generation model or newer — older Claude models reject it upstream. On every other provider, including Anthropic models on AWS Bedrock, the tool search tool and `defer_loading` are dropped and all tools are sent eagerly. The request still succeeds, it just loses the cache and token savings, so pin the provider (`anthropic/claude-sonnet-4-6`) when those savings matter. Bedrock is a transport limitation rather than a missing capability: Anthropic exposes server-side tool search there only through the InvokeModel API, and the gateway routes Bedrock through the Converse API. ### Gateway Response Cache [#gateway-response-cache] If [gateway caching](https://docs.passingright-staging.sandbloc.com/features/caching/gateway-caching) is enabled on the project, a repeated request with the same [cache-key fields](https://docs.passingright-staging.sandbloc.com/features/caching/gateway-caching#cache-key-generation) — resolved provider and model, messages, and the other keyed parameters; fields outside the cache key and insignificant JSON whitespace don't affect matching, but the key order inside message and tool objects does, and a request routed to a different provider is a cache miss — is replayed from cache instead of being sent upstream. Because this endpoint exposes no metadata envelope or cost fields, the replayed body is indistinguishable from the original (same `id`, content, and token counts), so the `x-llmgateway-cache: HIT` response header is the marker to check. Send `x-no-cache: true` to bypass the cache for a single request. # PassingRight Authentication, API Keys & IAM Rules URL: https://docs.passingright-staging.sandbloc.com/features/api-keys API keys are the primary method for authenticating with the PassingRight. This guide covers creating API keys, managing them, and configuring IAM rules for fine-grained access control. ## Overview [#overview] PassingRight provides comprehensive API key management with the following features: * **Basic API Key Management**: Create, list, rename, update, and delete API keys * **Usage Limits**: Set lifetime and recurring spending limits on individual API keys * **Expiration (TTL)**: Give a key a time-to-live so it disables itself automatically * **Rotation (Rolling)**: Replace a key's secret in place without losing its settings or history * **IAM Rules**: Fine-grained access control for models, providers, and pricing * **Usage Tracking**: Monitor API key usage and costs * **Status Management**: Enable/disable keys without deletion ### Related key types [#related-key-types] This page covers gateway API keys (`llmgtwy_…`), the keys you send to the gateway as a bearer token. PassingRight also issues three other kinds of keys, each with its own page: | Key | What it is for | | ----------------------------------------------------- | -------------------------------------------------------------------------------------- | | [Master keys](https://docs.passingright-staging.sandbloc.com/features/master-keys) | Manage projects, gateway API keys, IAM rules, and custom models programmatically | | [Provider keys](https://docs.passingright-staging.sandbloc.com/learn/provider-keys) | Bring your own upstream provider credentials so requests bill to your provider account | | [Platform secret keys](https://docs.passingright-staging.sandbloc.com/features/embeddable-payments) | Mint end-user sessions from your backend when using the Payments SDK (`sk_…`) | ## Creating API Keys [#creating-api-keys] ### Via Dashboard [#via-dashboard] 1. Navigate to your project in the PassingRight dashboard 2. Go to the **API Keys** section 3. Click **Create API Key** 4. Provide a description for your key 5. Optionally set an all-time usage limit 6. Optionally set a recurring usage limit such as `$10 / day` or `$500 / month` 7. Optionally set an expiration (TTL) such as `30 minutes`, `12 hours`, or `7 days` 8. Click **Create** API keys are shown in full only once during creation. Make sure to copy and store them securely. New and rolled secrets are stored only as keyed HMAC-SHA-256 fingerprints, and authentication compares the fingerprint of the presented secret. ### Programmatically [#programmatically] Gateway API keys can also be created, listed, updated, and deleted without the dashboard using a [master key](https://docs.passingright-staging.sandbloc.com/features/master-keys) — useful when you provision a key per customer or per environment from your own backend: ```bash curl -X POST https://internal.passingright.io/v1/master/keys \ -H "Authorization: Bearer $MASTER_KEY" \ -H "Content-Type: application/json" \ -d '{ "projectId": "proj_...", "description": "Customer ACME — production key", "periodUsageLimit": "10.00", "periodUsageDurationValue": 1, "periodUsageDurationUnit": "day" }' ``` Usage limits and IAM rules can be configured through the master key API as well. Expiration (TTL) and rotation are currently dashboard-only. See the [master key API reference](https://docs.passingright-staging.sandbloc.com/features/master-keys#create-a-gateway-api-key) for the full endpoint list. ## Using API Keys [#using-api-keys] Once you have an API key, use it in the `Authorization` header of your requests: ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer llmgtwy_your_api_key_here" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o", "messages": [{"role": "user", "content": "Hello!"}] }' ``` To show DevPass allowance meters in an integration, see the [DevPass Usage API](https://docs.passingright-staging.sandbloc.com/developers/devpass-usage). ## Renaming API Keys [#renaming-api-keys] A key's name is only a label, so you can change it at any time — from the dashboard, or with the `description` field on [`PATCH /v1/master/keys/{id}`](https://docs.passingright-staging.sandbloc.com/features/master-keys#update-a-gateway-api-key) — without affecting the secret, its usage history, limits, or IAM rules. ## Budget alerts [#budget-alerts] Enable **API key budgets** in the dashboard's notification settings to receive in-app or email warnings as a key approaches its lifetime or recurring limit. Choose a threshold from 50% to 100%; the default is 80%. See [Notifications](https://docs.passingright-staging.sandbloc.com/features/notifications) for delivery and access rules. ## Disabling/Enabling API Keys [#disablingenabling-api-keys] You can disable an API key to stop it from being used, but the key is not deleted and can be re-enabled later. ## Rotating (Rolling) API Keys [#rotating-rolling-api-keys] Rolling a key generates a **new secret for the same key** and invalidates the old one immediately. Everything else about the key is preserved: its name, usage history and statistics, all-time and recurring limits (including the active period window), IAM rules, and expiration. Use this when a secret may have been exposed — in a commit, a log, a CI artifact, or a shared environment — and you want to cut off the leaked value without losing the key's spend tracking or access rules. 1. Open the **API Keys** page and pick the key's actions menu 2. Choose **Roll Key** and confirm 3. Copy the new secret and update every client that used the old one The old secret stops working the moment the key is rolled, and the new secret is shown only once. Roll during a window where you can update your clients promptly. Requests made with the old secret are rejected with a `401 Unauthorized`. Rolling is limited to regular gateway API keys — the auto-generated playground key cannot be rolled, and Payments SDK [platform secret keys](https://docs.passingright-staging.sandbloc.com/features/embeddable-payments) have their own lifecycle. ## Expiration (TTL) [#expiration-ttl] You can give an API key a **time-to-live (TTL)** when you create it. Set how long the key should live — in **minutes**, **hours**, or **days** — and it will be disabled automatically once that time passes. This is ideal for short-lived integrations, demos, CI jobs, and temporary access. * A key works normally until its expiration time * Once expired, the gateway rejects requests with that key with a `401 Unauthorized` * A background job marks expired keys as **inactive**, so the dashboard reflects the disabled state * Keys created without a TTL never expire (the default) ### Reactivating an Expired Key [#reactivating-an-expired-key] An expired key is paused, not deleted. To bring it back online you must reactivate it **with a new future expiration** — an expired key cannot be re-enabled while its TTL is still in the past. Keys that have no TTL, or whose TTL is still in the future, can be enabled and disabled freely without setting a new expiration. Expiration is independent of usage limits. A key can hit its TTL before, or instead of, reaching a spend cap. ## Usage Limits [#usage-limits] Usage is tracked per API key on the API Keys page. Usage includes both costs from PassingRight credits and usage from your own provider keys when applicable, giving you complete visibility into total spending per key. You can set two independent limits for each key: * **All-time usage limit**: A lifetime spend cap * **Recurring usage limit**: A spend cap that resets every configured hour, day, week, or month When a key reaches either limit, requests using that key return `401 Unauthorized` until the key is updated or, for recurring limits, the next usage window starts. This is separate from IAM rule violations, which return `403 Forbidden`. Recurring windows support: * Minimum duration: **1 hour** * Maximum duration: **12 months** * Units: **hour**, **day**, **week**, **month** For the dashboard walkthrough and field-by-field details, see [API Keys in Learn](https://docs.passingright-staging.sandbloc.com/learn/api-keys). ## IAM Rules [#iam-rules] IAM (Identity Access Management) rules provide fine-grained access control over what models, providers, and pricing tiers an API key can access. ### Rule Types [#rule-types] #### Model Access Rules [#model-access-rules] Control access to specific models: * **Allow Models**: Only allow access to specific models * **Deny Models**: Block access to specific models #### Provider Access Rules [#provider-access-rules] Control access to specific providers: * **Allow Providers**: Only allow access to specific providers * **Deny Providers**: Block access to specific providers Provider rules can also target your organization's own [custom providers](https://docs.passingright-staging.sandbloc.com/features/custom-providers). The generic `custom` entry matches every custom provider, while a `custom:` entry (offered in the selector for each of your custom providers) matches only the custom provider with that name. Model rules can likewise reference a custom-catalog model as `/`. #### Pricing Rules [#pricing-rules] Control access based on model pricing: * **Allow Pricing**: Set constraints on what pricing tiers are allowed * **Deny Pricing**: Block specific pricing tiers * **Free vs Paid**: Allow or deny access to free vs paid models #### IP Address Rules [#ip-address-rules] IP address rules are available on the **Enterprise** plan only. Contact us at [support@passingright.io](mailto:support@passingright.io) to enable them for your organization. Restrict where the API key can be used from by source IP, using CIDR ranges: * **Allow IP Ranges (CIDR)**: Only permit requests from the listed IPv4/IPv6 CIDRs * **Deny IP Ranges (CIDR)**: Block requests from the listed IPv4/IPv6 CIDRs Both IPv4 (e.g. `192.0.2.0/24`) and IPv6 (e.g. `2001:db8::/32`) ranges are supported, and you can mix both in a single rule. To restrict to a single address, use a `/32` (IPv4) or `/128` (IPv6) prefix. The gateway reads the client IP from the first entry in the `X-Forwarded-For` header (set by the GCP load balancer). When an `allow_ip_cidrs` rule is configured and the gateway cannot determine the client IP, the request is denied. Invalid CIDR syntax is rejected at rule-creation time with a `400` error. ### Combining Multiple Rules [#combining-multiple-rules] * **Allow rules of the same type are unioned**: a request passes if it matches *any* of them. For example, one `allow_models` rule with `["claude-opus-4-6"]` and another with `["claude-fable-5"]` allow both models — exactly as if you had a single rule listing both. * **Allow rules of different types are combined with AND**: the request must satisfy every configured allow rule type (e.g. the model must be in the allowed models *and* served by an allowed provider). * **Deny rules always apply**: a request matching any deny rule is rejected, regardless of allow rules. * **Moderation is special-cased**: `/v1/moderations` runs a fixed moderation model that cannot appear in model allowlists, so only provider and IP rules apply to it — model and pricing rules are skipped. ## Member-Level IAM Rules [#member-level-iam-rules] The same rule types can also be configured **per organization member** by owners and admins on the [Team page](https://docs.passingright-staging.sandbloc.com/learn/team) (admins cannot modify an owner's rules). Member-level rules act as an organization-wide ceiling for that member: * A request must pass **both** the member's rules and the API key's rules. Within each level, rules combine exactly as described above. * Key rules can only **narrow** access further — they can never grant anything the member's rules deny. For example, if an admin restricts a member to a single approved [provider](https://passingright.io/providers) with an `allow_providers` rule, the member can create a key rule allowing only a specific model from that provider, but a key rule allowing any other provider has no effect. * A key with **no rules of its own** is still fully constrained by its owner's member-level rules. * Member-level rules apply to all regular API keys created by that member, across every project in the organization. When a request is denied by a member-level rule, the `403` error message states that the restriction is an organization member IAM rule set by the org admin (rather than the key's own IAM configuration), so key holders know who to contact. Member-level rules can also be managed programmatically via the [master key API](https://docs.passingright-staging.sandbloc.com/features/master-keys#member-iam-rules), addressing members by membership id or email. ## Team-Level IAM Rules [#team-level-iam-rules] The same rule types can additionally be configured on an **organization team**. Team rules apply to members with the `developer` role who are assigned to that team, and they are evaluated **before** member and key rules: a request must pass the team's rules, then the member's rules, then the key's rules, and each layer can only narrow what the previous one allowed. When a request is denied by a team-level rule, the `403` error message states that the restriction is inherited from an organization team IAM rule set by the org admin. ## Error Handling [#error-handling] When API keys encounter IAM rule violations, the API returns a `403` with the standard OpenAI error envelope: ```json { "error": { "message": "Access denied: Model gpt-4 is not in the allowed models list", "type": "invalid_request_error", "param": null, "code": "permission_denied" } } ``` Common error scenarios: * Model not allowed by IAM rules * Provider blocked by IAM rules * Pricing limits exceeded * API key disabled or deleted * API key expired (TTL passed) * API key rolled, so the old secret is no longer valid * Usage limit reached ## Migration from Legacy Keys [#migration-from-legacy-keys] If you have existing API keys without IAM rules: 1. **Backward Compatibility**: Existing keys continue to work without restrictions 2. **Gradual Migration**: Add IAM rules incrementally 3. **Testing**: Test IAM rules in development before applying to production 4. **Monitoring**: Monitor for access denied errors after implementing rules API keys without IAM rules have unrestricted access to all models and providers. # Audit Logs URL: https://docs.passingright-staging.sandbloc.com/features/audit-logs Audit logs provide complete visibility into all actions within your organization. Track who did what, when, and to which resource. Audit logs are available on the [**Enterprise plan**](https://passingright.io/enterprise) for organization owners and admins. ## What's Tracked [#whats-tracked] Every significant action is logged with detailed metadata: | Field | Description | | ----------------- | -------------------------------------------------------- | | **Timestamp** | When the action occurred | | **User** | Who performed the action (name and email) | | **Action** | What was done (e.g., `api_key.create`, `project.update`) | | **Resource Type** | Category of the affected resource | | **Resource ID** | Unique identifier of the affected resource | | **Details** | Additional context like resource names or changed fields | ## Tracked Actions [#tracked-actions] The categories below are illustrative, not exhaustive — many more actions are recorded (master keys, custom models, organization teams and team IAM rules, SSO and SCIM events, invites, plan changes, refunds, and others). The action filter dropdown in the dashboard lists the full set. ### Organization Management [#organization-management] * `organization.update` — Organization settings changed * `organization.delete` — Organization deleted ### Project Management [#project-management] * `project.create` — New project created * `project.update` — Project settings changed * `project.delete` — Project deleted ### Team Management [#team-management] * `team_member.add` — New member invited * `team_member.update` — Member role changed * `team_member.remove` — Member removed ### API Key Management [#api-key-management] * `api_key.create` — New API key created * `api_key.update_status` — API key enabled/disabled * `api_key.update_limit` — Usage limit changed * `api_key.delete` — API key deleted * `api_key.iam_rule.create` — IAM rule added * `api_key.iam_rule.update` — IAM rule modified * `api_key.iam_rule.delete` — IAM rule removed ### Provider Key Management [#provider-key-management] * `provider_key.create` — Provider key added * `provider_key.update` — Provider key changed (status, name, limits, and other settings; base URL and token rotation when done through the master key API). Custom providers are recorded as `provider_key.*` events with `provider: "custom"` * `provider_key.delete` — Provider key removed ### Billing Events [#billing-events] * `subscription.create` — Subscription started * `subscription.cancel` — Subscription cancelled * `subscription.resume` — Subscription resumed * `payment.credit_topup` — Credits purchased ## Filtering and Search [#filtering-and-search] Filter logs by: * **Action** — Specific action type * **Resource Type** — Category of resource * **User** — Who performed the action * **Date Range** — Time period ## Data Retention [#data-retention] Audit logs are retained for **at least 90 days** on the Enterprise plan — there is currently no automatic expiry. ## Access Control [#access-control] Only organization **owners** and **admins** can view audit logs. This ensures sensitive activity data is only visible to authorized personnel. ## Get Started [#get-started] Audit logs are an Enterprise feature. [Contact us](https://passingright.io/enterprise) to enable Enterprise for your organization. # Compliance URL: https://docs.passingright-staging.sandbloc.com/features/compliance Provider compliance policies let you guarantee that requests are only ever routed to providers that meet your organization's regulatory requirements. When a request would be routed to a provider that doesn't meet the policy, the gateway blocks it **before any data leaves the gateway**. The complete provider compliance policy is available on [**Enterprise**](https://passingright.io/enterprise). DevPass offers only **No AI training**. ## Requirements [#requirements] Enable a policy under **Settings → Compliance** in the dashboard and toggle the requirements you need: In DevPass, enable **No AI training** directly under **Settings**. It uses the same fail-closed provider policy described below, but the other requirements are not available on DevPass. | Requirement | A provider is allowed when… | | ----------------------------- | ---------------------------------------------------------------- | | **Zero data retention (ZDR)** | it does not log prompts and declares a zero-day retention period | | **No training on prompts** | it does not train on API prompts | | **No stealth providers** | it is not a stealth provider | | **GDPR compliant** | it is GDPR compliant | | **SOC 2 (Type 1 or 2)** | it holds a SOC 2 report of any type | | **SOC 2 Type 2** | it holds a SOC 2 Type 2 report specifically | | **ISO 27001** | it holds an ISO 27001 certification | | **SOC 2 Type 2 or ISO 27001** | it holds either a SOC 2 Type 2 report or ISO 27001 | Every requirement is **fail-closed**: a provider passes only if its published data policy explicitly satisfies the requirement. If an attribute is unknown for a provider, that provider is treated as non-compliant. Before enabling **Zero data retention (ZDR)**, set the organization's data retention to **Metadata Only** and disable response caching in every project. While ZDR is active, **Retain All Data** and project response caching are unavailable, gateway response caching is bypassed, provider prompt-cache markers are stripped, Responses API requests must set `store: false`, and asynchronous video generation is unavailable because video jobs require temporary output storage. Disable ZDR before turning payload retention or caching back on. Existing policies that use the deprecated **No prompt logging** rule continue to enforce their original provider-routing behavior. The legacy rule is no longer available for new configuration and does not enable the gateway-level ZDR controls described above. Stealth providers are undisclosed platforms that PassingRight routes to without naming the operator, so their data policy and headquarters are unknown. That already makes them fail every certification and data-policy requirement above, but **No stealth providers** excludes them explicitly — useful when you want them gone without turning on any other requirement. The settings page shows a live **Provider Impact** preview of which providers are allowed (green) and which are blocked (red) under the current policy, so you can see the impact before saving. Hovering a blocked provider lists exactly which requirements it does not meet — a missing certification, its data policy, its headquarters country, or a provider-list restriction. ## Provider headquarters [#provider-headquarters] Beyond certifications, you can restrict routing by the country a provider is headquartered in. The **Provider Headquarters** card presents a country selector, and requests are only routed to providers based in a selected country. The selector only offers countries that are referenced by a provider in the catalogue — browse the [providers directory](https://passingright.io/providers) to see the current set and each provider's headquarters. Selecting no country applies no location restriction. The country filter is also fail-closed: when at least one country is selected, a provider whose headquarters is unknown is treated as non-compliant. The country filter composes with the certification and data-policy requirements above — a provider must satisfy all active requirements to be allowed. ## Provider & model restrictions [#provider--model-restrictions] Beyond attribute-based requirements, the **Provider & Model Restrictions** card lets you block or allow individual providers and models: * **Blocked providers / blocked models** — deny lists. A listed provider or model is always blocked, even when it satisfies every requirement above. * **Allowed providers / allowed models** — fine-grained allow lists. When non-empty, only the listed providers (or models) may be used; everything else is blocked. An empty list applies no restriction. Deny lists always win over allow lists, and both compose with the requirements above — an allow-listed provider must still satisfy every active certification, data-policy, and country requirement. A non-empty allowed-providers list blocks **every** provider not on it, regardless of certifications or data policy. If you allow-list only a custom provider, all catalogue providers show as blocked with the reason "Not on the allowed-providers list" — add any additional provider you want to use to the allow list. The selectors include your organization's own [custom providers](https://docs.passingright-staging.sandbloc.com/features/custom-providers) (stored as `custom:` refs) and their custom-catalog models (stored as `/` refs), so a specific custom deployment can be blocked or allow-listed individually just like a catalogue provider. ### Choosing a compliant provider [#choosing-a-compliant-provider] The provider dropdowns are policy-aware, so you can tell **before** adding a provider to a list whether it satisfies your policy: * A **green shield** marks a provider that meets every active certification, data-policy, and headquarters requirement. * A **red shield** marks a provider that does not; the requirements it misses (for example "May log prompts" or "Headquartered in China, which is not an allowed country") are listed under its name. * The **"Only providers that meet policy requirements"** toggle at the top of the dropdown hides incompatible providers entirely. These indicators evaluate the requirements only — deliberately ignoring the allowed/blocked lists themselves — so an active allow list never paints every other provider red while you're deciding what to add to it. Your own custom providers are evaluated against their [self-attested posture](#custom-providers); one without an attestation on file shows "No compliance attestation on file". Elsewhere in the dashboard (for example the API-key provider filter), the colored dot next to a provider is simply the provider's **brand color** and carries no compliance meaning. These restrictions are **organization-wide** and take precedence over member-level and API-key-level [IAM rules](https://docs.passingright-staging.sandbloc.com/features/api-keys): they are enforced after IAM evaluation, so no user-, team-, or key-level allow rule can grant access to a provider or model the compliance policy excludes. ## Enforcement [#enforcement] When no available provider for a model meets the policy — or a pinned provider is non-compliant — the gateway returns a `403`: ```json { "error": { "message": "This request was blocked by your organization's provider compliance policy. No available provider for deepseek-v3.2 meets the required certifications or provider/model restrictions. Contact your PassingRight admin to adjust the policy." } } ``` This applies to both routing modes: * **Automatic routing** — non-compliant providers are removed from the candidate set, and the request is blocked if none remain. * **Pinned providers** — a request such as `deepseek/deepseek-v3.2` is blocked when that specific provider does not meet the policy. Each block is recorded as a **security event** so administrators can review what was rejected and why. ## Custom providers [#custom-providers] [Custom providers](https://docs.passingright-staging.sandbloc.com/features/custom-providers) point at infrastructure your organization operates itself (for example, models hosted in your own cloud account), so they have no entry in the provider catalogue and no published data policy. Under any enabled compliance policy they are therefore **blocked by default** — the same fail-closed rule that applies to catalogue providers with unknown attributes. To route through a custom provider while a policy is active, an organization owner or admin can record a **self-attestation** of that provider key's compliance posture under **Models → Compliance attestation** in the dashboard. The attestation covers the same attributes as a catalogue data policy — SOC 2 report type, ISO 27001, GDPR, training on API prompts, prompt logging, retention period, and the country the deployment is operated from — and the gateway evaluates it against your policy with **identical fail-closed rules**: an attribute left "unknown" never satisfies a requirement, and only an explicit "No" satisfies requirements like "No training on prompts". Clearing the attestation restores the blocked state. PassingRight does not verify attestations — they are your organization's own assertion about infrastructure you operate. Every attestation change is recorded in the audit log with the attesting user and timestamp. Attestations only apply to custom provider keys. They can never be used to clear a catalogue provider that fails your policy. **Known limitation:** the country selector in the compliance policy only offers countries referenced by catalogue providers (see the [providers directory](https://passingright.io/providers)). A custom deployment attested to a country outside that set cannot be allowed while a country restriction is active. ## Safety identifiers [#safety-identifiers] Providers that offer an abuse-attribution identifier receive an opaque, random value that PassingRight generates once per organization. It contains no personal data — no email address, user id or organization name — and it is the only thing that ties a report of abusive traffic back to an account. Whether PassingRight forwards this identifier depends on the upstream API. Check the [providers directory](https://passingright.io/providers) and open a provider to see its current behavior. Any `safety_identifier` you set on a request yourself is ignored — the value is always the one derived from your organization. ## Access Control [#access-control] Only organization **owners** and **admins** can view and change the compliance policy. Project-scoped **developer** members cannot see the policy itself, but they can browse the org's available providers and models — including custom providers and their model catalogs — read-only on the **Models** page. This is the recommended way for developers to discover what they can call without being granted additional permissions. ## Related [#related] * [Org Models Directory](https://docs.passingright-staging.sandbloc.com/features/models-directory) — see per-model eligibility under the active policy in the dashboard. * [Data Retention](https://docs.passingright-staging.sandbloc.com/features/data-retention) — control whether request and response payloads are stored. * [Guardrails](https://docs.passingright-staging.sandbloc.com/features/guardrails) — detect and block harmful or sensitive content. ## Get Started [#get-started] DevPass subscribers can configure **No AI training** under **Settings**. [Contact us](https://passingright.io/enterprise) for the complete provider compliance policy. # Cost Breakdown URL: https://docs.passingright-staging.sandbloc.com/features/cost-breakdown PassingRight provides real-time cost information for each API request directly in the response's `usage` object. This allows you to track costs programmatically without needing to query the dashboard. Cost breakdown is available for all users on both hosted and self-hosted deployments. ## Response Format [#response-format] API responses include cost fields in the `usage` object: ```json { "id": "chatcmpl-123", "object": "chat.completion", "created": 1234567890, "model": "openai/gpt-4o", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "Hello! How can I help you today?" }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 10, "completion_tokens": 15, "total_tokens": 25, "cost": 0.000125, "cost_details": { "upstream_inference_cost": 0.000125, "upstream_inference_prompt_cost": 0.000025, "upstream_inference_completions_cost": 0.0001, "total_cost": 0.000125, "input_cost": 0.000025, "output_cost": 0.0001, "cached_input_cost": 0, "request_cost": 0, "web_search_cost": 0, "image_input_cost": null, "image_output_cost": null, "data_storage_cost": 0.00000025 }, "prompt_tokens_details": { "cached_tokens": 0, "cache_write_tokens": 0, "audio_tokens": 0, "video_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "image_tokens": 0, "audio_tokens": 0 } } } ``` ## Cost Fields [#cost-fields] | Field | Description | | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `cost` | Total inference cost for the request in USD | | `cost_details.upstream_inference_cost` | Combined upstream inference cost in USD (prompt + completions) | | `cost_details.upstream_inference_prompt_cost` | Upstream cost for prompt tokens in USD (includes cached prompt discount) | | `cost_details.upstream_inference_completions_cost` | Upstream cost for completion tokens in USD | | `cost_details.total_cost` | Total request cost in USD (PassingRight extended field) | | `cost_details.input_cost` | Cost for non-cached prompt tokens in USD | | `cost_details.output_cost` | Cost for completion tokens in USD | | `cost_details.cached_input_cost` | Cost for cached prompt tokens in USD | | `cost_details.cache_write_input_cost` | Cost for prompt tokens written to the provider cache in USD, billed at the provider's cache-write premium (e.g. 1.25x for 5m / 2x for 1h on Anthropic) | | `cost_details.request_cost` | Per-request flat fee in USD (when the model applies one) | | `cost_details.web_search_cost` | Cost for web search tool calls in USD | | `cost_details.image_input_cost` | Cost for image inputs in USD | | `cost_details.image_output_cost` | Cost for image outputs in USD | | `cost_details.data_storage_cost` | Storage cost for retained request/response payloads in USD | ## Token Detail Fields [#token-detail-fields] The `usage` object also includes detailed token counters that mirror OpenAI's extended format: | Field | Description | | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `prompt_tokens_details.cached_tokens` | Number of prompt tokens served from the provider's prompt cache | | `prompt_tokens_details.cache_write_tokens` | Number of prompt tokens written into the provider's prompt cache | | `prompt_tokens_details.cache_creation_tokens` | Alias of `cache_write_tokens`, matching Anthropic's naming | | `prompt_tokens_details.cache_creation` | Per-TTL breakdown of cache writes (`ephemeral_5m_input_tokens` / `ephemeral_1h_input_tokens`), present when a cache write occurred | | `prompt_tokens_details.audio_tokens` | Number of audio prompt tokens | | `prompt_tokens_details.video_tokens` | Number of video prompt tokens | | `completion_tokens_details.reasoning_tokens` | Number of reasoning tokens produced by reasoning models | | `completion_tokens_details.image_tokens` | Number of image tokens produced | | `completion_tokens_details.audio_tokens` | Number of audio tokens produced | ## Streaming Responses [#streaming-responses] Cost information is also available in streaming responses. The cost fields are included in the final usage chunk sent before the `[DONE]` message: ``` data: {"id":"chatcmpl-123","object":"chat.completion.chunk","choices":[...],"usage":{"prompt_tokens":10,"completion_tokens":15,"total_tokens":25,"cost":0.000125,"cost_details":{"upstream_inference_cost":0.000125,"upstream_inference_prompt_cost":0.000025,"upstream_inference_completions_cost":0.0001,"total_cost":0.000125,"input_cost":0.000025,"output_cost":0.0001,"cached_input_cost":0,"request_cost":0,"web_search_cost":0,"image_input_cost":null,"image_output_cost":null,"data_storage_cost":0.00000025}}} data: [DONE] ``` ## Example: Tracking Costs in Code [#example-tracking-costs-in-code] Here's an example of how to track costs programmatically using the cost breakdown feature: ```typescript import OpenAI from "openai"; const client = new OpenAI({ apiKey: process.env.LLM_GATEWAY_API_KEY, baseURL: "https://api.passingright.io/v1", }); async function trackCosts() { const response = await client.chat.completions.create({ model: "gpt-4o", messages: [{ role: "user", content: "Hello!" }], }); const usage = response.usage as any; if (usage.cost !== undefined) { console.log(`Request cost: $${usage.cost.toFixed(6)}`); console.log( ` Prompt: $${usage.cost_details.upstream_inference_prompt_cost.toFixed(6)}`, ); console.log( ` Completions: $${usage.cost_details.upstream_inference_completions_cost.toFixed(6)}`, ); const cachedTokens = usage.prompt_tokens_details?.cached_tokens ?? 0; if (cachedTokens > 0) { console.log(` Cached prompt tokens: ${cachedTokens}`); } } return response; } ``` ## Use Cases [#use-cases] ### Budget Monitoring [#budget-monitoring] Track costs in real-time and implement budget limits in your application: ```typescript let totalSpent = 0; const BUDGET_LIMIT = 10.0; // $10 budget async function makeRequest(messages: Message[]) { const response = await client.chat.completions.create({ model: "gpt-4o", messages, }); const cost = (response.usage as any).cost || 0; totalSpent += cost; if (totalSpent > BUDGET_LIMIT) { throw new Error(`Budget exceeded: $${totalSpent.toFixed(2)}`); } return response; } ``` ### Per-User Cost Allocation [#per-user-cost-allocation] Track costs per user for billing or analytics: ```typescript const userCosts: Map = new Map(); async function makeRequestForUser(userId: string, messages: Message[]) { const response = await client.chat.completions.create({ model: "gpt-4o", messages, }); const cost = (response.usage as any).cost || 0; const currentCost = userCosts.get(userId) || 0; userCosts.set(userId, currentCost + cost); return response; } ``` ### Cost Analytics [#cost-analytics] Aggregate costs by model, time period, or any other dimension: ```typescript interface CostEntry { timestamp: Date; model: string; promptCost: number; completionsCost: number; totalCost: number; } const costLog: CostEntry[] = []; async function loggedRequest(model: string, messages: Message[]) { const response = await client.chat.completions.create({ model, messages, }); const usage = response.usage as any; costLog.push({ timestamp: new Date(), model: response.model, promptCost: usage.cost_details?.upstream_inference_prompt_cost || 0, completionsCost: usage.cost_details?.upstream_inference_completions_cost || 0, totalCost: usage.cost || 0, }); return response; } ``` ## Self-Hosted Deployments [#self-hosted-deployments] If you're running a self-hosted PassingRight deployment, cost breakdown is always included in API responses regardless of plan. This allows you to track internal costs and allocate them across teams or projects. # Custom Providers URL: https://docs.passingright-staging.sandbloc.com/features/custom-providers PassingRight supports integrating custom OpenAI-compatible providers, allowing you to use any API that follows the OpenAI chat completions format. This feature is perfect for: * Private or self-hosted LLM deployments * Specialized AI providers not natively supported * Internal AI services within your organization * Testing against different model endpoints Custom providers must be OpenAI-compatible, supporting the `/v1/chat/completions` endpoint format. ## Quick Setup [#quick-setup] ### 1. Add a Custom Provider Key [#1-add-a-custom-provider-key] Navigate to your organization's provider settings and add a custom provider via the UI. Provide a lowercase name, OpenAI-compatible base URL, and API token for the custom provider. ### 2. Make Requests [#2-make-requests] Once configured, make requests using the format `{customName}/{modelName}`: ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "mycompany/custom-gpt-4", "messages": [ { "role": "user", "content": "Hello from my custom provider!" } ] }' ``` ## Configuration Requirements [#configuration-requirements] ### Custom Provider Name [#custom-provider-name] * **Format**: Lowercase letters (`a-z`) with optional single hyphens between them * **Examples**: `mycompany`, `internal`, `my-company`, `eu-west` * **Invalid**: `MyCompany`, `my_company`, `123test`, `-mycompany`, `my-`, `my--company` * **Reserved**: `dynamic` is rejected (it is the [dynamic routes](https://docs.passingright-staging.sandbloc.com/features/dynamic-routes) model prefix), and the name must be unique within your organization The custom provider name must match the regex pattern `/^[a-z]+(-[a-z]+)*$/` exactly. ### Base URL [#base-url] * Must be a valid URL pointing to your provider's base endpoint * On the hosted service the URL must be `https://` and must not resolve to a private, reserved, or internal address. `http://` URLs and private-network hosts (for example a service on your own network) work only on self-hosted deployments that set `ALLOW_INSECURE_PROVIDER_URLS=true` * PassingRight will append `/v1/chat/completions` automatically — do not include `/v1` in the base URL (it is not stripped, so the path would be doubled) * **Example**: `https://api.example.com` → `https://api.example.com/v1/chat/completions` ### API Token [#api-token] * Provider-specific authentication token * Used in the `Authorization: Bearer {token}` header Unlike built-in providers, custom provider models are not validated, giving you complete flexibility. ## Supported Features [#supported-features] Custom providers serve **chat completions** — `/v1/chat/completions` and the endpoints that proxy through it (`/v1/responses`, `/v1/messages`, and the AI SDK gateway endpoints under `/v*/ai`). Gateway features that apply to that path (caching, routing, IAM, logging, cost tracking via the custom model catalog) work as usual. Embeddings, images, audio, video, rerank, OCR, and moderations do not route to custom providers. ## Custom Model Catalog [#custom-model-catalog] The custom model catalog is an **Enterprise** feature. Managing entries requires an enterprise plan, but the gateway always honors catalog entries that already exist. By default, requests through a custom provider are **not billed** and have **no enforced limits** — PassingRight has no catalog entry for the model, so it cannot know its pricing, context window, or capabilities. The **custom model catalog** lets you define that information per custom provider key, so PassingRight can attribute cost, enforce limits, and report usage just like a built-in model. Custom catalog models are **text-output** models. Multi-modal *input* (images, audio) is still supported and can be priced via the input fields below; output generation (images, video, audio) is intentionally out of scope because it is too provider-specific to bill generically. ### Defining a model [#defining-a-model] Open **Organization → Models**, pick the custom provider key, and add a catalog entry. Every field except the model id is optional: | Field | Purpose | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------- | | **Model id** | The id used after the provider prefix (e.g. `gpt-5.5` in `mycompany/gpt-5.5`). | | **Display name** | Optional human-readable label. | | **Context size** | Maximum input window in tokens. Enforced per request. | | **Max output** | Maximum completion tokens. Enforced against `max_tokens`. | | **Token prices** | Input, output, cached input, cache read, cache write (5m and 1h), per-request, web search, image input, and audio input prices. | | **Capabilities** | `streaming`, `vision`, `tools`, `reasoning`, `jsonOutput`, `audio`. | | **Supported parameters** | Advisory list of accepted request parameters. | Prices are in **USD per token** and accept either decimal (`0.000003`) or exponent (`3.0e-6`) notation. ### Cost attribution [#cost-attribution] When a request matches a catalog entry, PassingRight bills it using the entry's token prices and records the cost on the activity log. Without a matching entry the request stays unbilled (zero/null cost). A known model id (for example `gpt-5.5`) routed through a custom provider is **not** billed at that model's public pricing — only the prices you define in the catalog apply. Define the model in the catalog to attribute cost. ### Limit and capability enforcement [#limit-and-capability-enforcement] When an entry defines limits or capabilities, PassingRight enforces them before forwarding the request: * **Context size / max output** — requests that would exceed the configured window or output budget are rejected with `400`. * **Capabilities** — when a flag is explicitly set to disabled, requests that use that feature are rejected (e.g. images against a non-`vision` model, tools against a non-`tools` model, streaming against a non-streaming model). Flags left unset stay permissive and the upstream provider enforces them. ### Restricting to the catalog [#restricting-to-the-catalog] Each custom provider key has an **Only allow catalog models** switch. When enabled, requests through that provider must reference a defined catalog model — undefined models are rejected with `400`. This guarantees that every request has known cost attribution and enforced limits. When disabled, undefined models still work but remain unbilled, as before. ## Visibility [#visibility] Every active organization member — including project-scoped **developer** members — can browse the org's custom providers and their model catalogs read-only on the **Models** page, so developers can always discover which providers and models are available to them. Creating, editing, and deleting providers, models, and attestations stays restricted to organization owners and admins. ## Compliance attestation [#compliance-attestation] If your organization enforces a [provider compliance policy](https://docs.passingright-staging.sandbloc.com/features/compliance), custom providers are blocked by default — they have no catalogue data policy, so the fail-closed rules treat them as non-compliant. Organization owners and admins can record a per-key **self-attestation** of the deployment's compliance posture under **Models → Compliance attestation**, which the gateway then evaluates against the policy exactly like a catalogue provider's data policy. See [Compliance → Custom providers](https://docs.passingright-staging.sandbloc.com/features/compliance#custom-providers) for details and caveats. # Data Retention URL: https://docs.passingright-staging.sandbloc.com/features/data-retention PassingRight offers configurable data retention policies that allow you to store full request and response payloads. This enables powerful debugging capabilities, detailed analytics, and compliance with data governance requirements. ## Retention Levels [#retention-levels] PassingRight supports two retention levels that can be configured per organization: | Level | Description | Storage Cost | | ------------------- | ---------------------------------------------------------------------------------------------- | --------------- | | **Metadata Only** | Stores request metadata (timestamps, model, tokens, costs) without full payloads. Default. | Free | | **Retain All Data** | Stores complete request and response payloads including messages, tool calls, and attachments. | $0.01/1M tokens | Metadata-only retention is enabled by default and provides usage analytics without additional storage costs. Retention levels are configurable on standard (pay-as-you-go) organizations only. DevPass and chat subscriptions are always metadata only — their request and response payloads are not retained, and there is no setting to turn payload storage on. The Responses API exception below still applies to them. ## Storage Pricing [#storage-pricing] When full data retention is enabled, storage is billed at **$0.01 per 1 million tokens**. This rate applies to: * Input tokens (prompt) * Cached input tokens * Output tokens (completion) * Reasoning tokens Storage costs are calculated per request and billed separately from inference. When "Retain All Data" is enabled, each response's `usage.cost_details` object includes a `data_storage_cost` field with the per-request storage cost in USD. See [Cost Breakdown](https://docs.passingright-staging.sandbloc.com/features/cost-breakdown) for the full list of cost fields. ### Example Cost Calculation [#example-cost-calculation] For a request with: * 1,000 input tokens * 500 output tokens * 1,500 total tokens Storage cost = 1,500 / 1,000,000 × $0.01 = **$0.000015** ## Configuring Retention [#configuring-retention] Data retention is configured at the organization level in your dashboard settings. The setting is only available on standard pay-as-you-go organizations: 1. Navigate to **Organization Settings** → **Policies** 2. Select your preferred **Data Retention Level** 3. Save changes Only organization **owners** can change the retention level — admins and project admins receive a `403`. Changing retention settings applies to new requests only. Existing stored data follows the retention period active when it was created. ## Retention Periods [#retention-periods] Data is retained for 30 days for all users, regardless of plan. After the retention period expires, the stored payloads (prompts, completions, tool payloads, raw request and response bodies) are automatically cleared; request metadata used for analytics is kept. The Responses API does not require data retention. Stored responses (used for `previous_response_id` chaining and `GET /v1/responses/:id`) are kept in dedicated storage for 30 days — matching OpenAI's own retention — regardless of your organization's data retention policy, and are not billed as data storage. Because this is independent of the retention level, it applies to metadata-only organizations, including DevPass and chat, whose Responses API input and output items are therefore held for up to 30 days. Send `store: false` with the request to opt out. When an Enterprise organization's [zero-data-retention policy](https://docs.passingright-staging.sandbloc.com/features/compliance) is active, the gateway rejects Responses API requests unless `store: false` is set, does not retain compaction state, bypasses the gateway response cache, strips provider prompt-cache markers, and prevents payload retention or project response caching from being enabled until ZDR is disabled. ZDR cannot be enabled until both settings are off. ## Accessing Stored Data [#accessing-stored-data] When data retention is enabled, you can access your stored requests through the dashboard: * View request history with full payload inspection * Filter by model and date range * Inspect complete request and response payloads ## Use Cases [#use-cases] ### Debugging [#debugging] Full data retention enables you to: * Inspect exact prompts sent to models * Review complete responses including tool calls * Trace conversation histories * Identify issues in production ### Analytics [#analytics] With stored payloads, you can: * Analyze prompt patterns and effectiveness * Track response quality over time * Build custom dashboards and reports * Measure model performance across use cases ### Compliance [#compliance] Data retention helps meet compliance requirements by: * Maintaining audit trails of AI interactions * Enabling data governance policies * Supporting incident investigation * Providing records for regulatory requirements ## Billing Considerations [#billing-considerations] ### Credit Usage [#credit-usage] In **API keys mode** (using your own provider keys): * Only storage costs are deducted from PassingRight credits * Inference costs are billed directly to your provider In **credits mode**: * Both inference and storage costs are deducted from credits ### Monitoring Storage Costs [#monitoring-storage-costs] Storage costs appear in: * Usage dashboard under "Storage" category * Billing invoices as a separate line item Enable [auto top-up](https://passingright.io/dashboard) in billing settings to ensure uninterrupted service when storage costs accumulate. ## Self-Hosted Deployments [#self-hosted-deployments] Self-hosted deployments have full control over data retention: * Enable or disable the 30-day cleanup job with `ENABLE_DATA_RETENTION_CLEANUP=true` (disabled by default; the period itself is not configurable) * Data is stored in your own PostgreSQL database * No additional storage costs (you manage your own infrastructure) ## Privacy and Security [#privacy-and-security] * All stored data is encrypted at rest * Access is restricted to organization members with appropriate permissions * Stored payloads are automatically cleared after the retention period (on self-hosted deployments, only when the cleanup job is enabled via `ENABLE_DATA_RETENTION_CLEANUP=true`) * You can request immediate deletion of specific records through support # Document Reading URL: https://docs.passingright-staging.sandbloc.com/features/documents PassingRight supports sending documents (PDFs and other file types) to document-capable models using OpenAI's `file` content block format. The gateway forwards the document to the underlying provider so the model can read and reason over its contents. ## Document-Capable Models [#document-capable-models] Document input is currently supported on Google Gemini models via Google AI Studio. You can find document-capable models on the [models page with the document filter](https://passingright.io/models?filters=1\&document=true). ## Sending a Document [#sending-a-document] Add a `file` content block to a user message. The `file_data` field must be a base64-encoded data URL that includes the document's MIME type. ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gemini-3.6-flash", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Summarize this document." }, { "type": "file", "file": { "filename": "report.pdf", "file_data": "data:application/pdf;base64,JVBERi0xLjQKJ..." } } ] } ] }' ``` ### Content Block Fields [#content-block-fields] * **`type`**: must be `"file"`. * **`file.filename`** *(optional)*: original filename, shown in Lounge and forwarded for context. * **`file.file_data`**: base64-encoded data URL of the form `data:;base64,`. The `file.file_id` field (for referencing files uploaded via a provider's Files API) is accepted by the schema but not currently supported by the Google transform. Use `file_data` with an inline base64 data URL. ## Supported File Types [#supported-file-types] The accepted MIME types depend on the target model. Gemini models commonly support: * `application/pdf` * `text/plain` * `text/html` * `text/css` * `text/javascript` * `text/csv` * `text/markdown` * `text/xml` If the upstream provider rejects the MIME type, the gateway surfaces a `400` error including the unsupported MIME type and the provider it was sent to. To use a different file type, encode the file with the matching MIME type in the data URL prefix. ## Encoding a File as a Data URL [#encoding-a-file-as-a-data-url] Any tool that can produce base64 output works. For example, in a shell: ```bash DATA=$(base64 -i report.pdf | tr -d '\n') echo "data:application/pdf;base64,$DATA" ``` Or in JavaScript: ```javascript import { readFileSync } from "node:fs"; const buffer = readFileSync("report.pdf"); const fileData = `data:application/pdf;base64,${buffer.toString("base64")}`; ``` Then pass `fileData` as the `file.file_data` value in your request. ## Multiple Documents [#multiple-documents] You can include multiple `file` blocks in a single message, optionally mixed with text and image content: ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gemini-3.1-pro-preview", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Compare these two reports." }, { "type": "file", "file": { "filename": "q1.pdf", "file_data": "data:application/pdf;base64,JVBERi0x..." } }, { "type": "file", "file": { "filename": "q2.pdf", "file_data": "data:application/pdf;base64,JVBERi0x..." } } ] } ] }' ``` ## Error Handling [#error-handling] The gateway returns `400` for the following document-related errors: * The selected model does not support document input. * The `file` block is missing both `file_data` and `file_id`. * `file_data` is not a valid base64 data URL. * The upstream provider rejects the document's MIME type for the selected model. # Dynamic Routes URL: https://docs.passingright-staging.sandbloc.com/features/dynamic-routes Dynamic routes let you move routing logic out of your application code and into the gateway. Instead of hardcoding a model, you define a named decision graph — conditions on the request, percentage-based traffic splits, and model targets — and invoke it by putting `dynamic/` in the `model` field of a chat request. Dynamic routes are resolved on `/v1/chat/completions` and the endpoints that proxy through it (`/v1/responses`, `/v1/messages`, and the AI SDK gateway endpoints under `/v*/ai`) — not on embeddings, images, audio, video, rerank, OCR, or moderations: ```bash curl https://api.passingright.io/v1/chat/completions \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "x-user-tier: paid" \ -d '{ "model": "dynamic/support", "messages": [{"role": "user", "content": "Hello!"}] }' ``` The graph is evaluated on every request. Once it resolves to a model, the request flows through the same [smart routing](https://docs.passingright-staging.sandbloc.com/features/routing) as any other request: weighted provider scoring, sticky sessions, and automatic cross-provider fallback all apply to the resolved target. Dynamic routes are available on the Enterprise plan. Manage them under{" "} Project Settings → Dynamic Routes in the dashboard, either in a visual drag-and-drop editor or as raw JSON. ## The route graph [#the-route-graph] A route is a JSON document with an `entry` node id and a list of nodes. Evaluation starts at `entry` and follows branches until it reaches a `model` node (route the request) or an `end` node (reject the request with a `400`). ```json { "entry": "tier", "nodes": [ { "id": "tier", "type": "conditional", "conditions": [ { "field": { "source": "header", "path": "x-user-tier" }, "op": "eq", "value": "paid", "next": "premium" } ], "else": "split" }, { "id": "premium", "type": "model", "model": "claude-sonnet-4-6" }, { "id": "split", "type": "percentage", "splits": [ { "weight": 80, "next": "stable" }, { "weight": 20, "next": "experiment" } ] }, { "id": "stable", "type": "model", "model": "gpt-5-nano", "providers": ["openai"] }, { "id": "experiment", "type": "model", "model": "gemini-2.5-flash" } ] } ``` Node ids may contain letters, digits, hyphens, and underscores. ## Node types [#node-types] ### `conditional` [#conditional] Evaluates its conditions top to bottom; the first match wins and the request follows that condition's `next`. When nothing matches, the request follows `else`. Each condition reads one field from the request: | `field.source` | What `field.path` means | | -------------- | --------------------------------------------------------------------------------- | | `header` | A request header name (case-insensitive), e.g. `x-user-tier` | | `body` | A dot-path into the JSON request body, e.g. `metadata.segment` or `max_tokens` | | `metadata` | A gateway-provided request attribute: `orgId`, `projectId`, `apiKeyId`, or `plan` | Supported operators: | Operator | Matches when | Value type | | ---------- | ------------------------------------------------------------------------- | ------------------------- | | `eq` | The field equals the value (compared as strings) | string / number / boolean | | `neq` | The field differs from the value — also matches when the field is missing | string / number / boolean | | `in` | The field equals any entry in the value array | array of strings | | `contains` | The field's string form contains the value | string | | `gt`, `lt` | The field is numerically greater / less than the value | number | | `exists` | The field is present (no value) | — | ### `percentage` [#percentage] Splits traffic across branches by relative weight. The draw is **deterministic per session**: the split key is the request's session id (`x-session-id` and the other [sticky session routing](https://docs.passingright-staging.sandbloc.com/features/routing#sticky-session-routing) signals), so a conversation keeps its assignment across requests instead of flip-flopping between experiment arms. Requests without a session id get an independent draw per request. Weights are relative — `{ 80, 20 }` and `{ 4, 1 }` produce the same split. ### `model` [#model] Terminates evaluation and routes the request to a catalog model (see the [models page](https://passingright.io/models) for available ids). Optional `providers` restricts routing to those providers and doubles as the ordered fallback preference; when omitted, every provider serving the model is a candidate and weighted smart routing picks the best one. A `model` node can also target one of your organization's [custom catalog models](https://docs.passingright-staging.sandbloc.com/features/custom-providers#custom-model-catalog) as `/`. Such a node must not set `providers` (the custom model already fixes its provider), and the referenced custom model must be active in your organization when the route is saved or published. ### `end` [#end] Terminates evaluation and rejects the request with a `400`. Useful as an explicit deny branch — for example, refusing traffic that doesn't carry a required header. ## Validation [#validation] Graphs are validated when you save a draft and again when you publish: * every referenced node must exist and be reachable from `entry` * cycles are rejected — evaluation is deterministic per request, so a revisited node would loop forever * model ids and provider ids must exist in the catalog, and each listed provider must actually serve the model * operator/value mismatches (e.g. `gt` with a non-numeric value) are rejected An invalid graph can never be saved or published, and publishing re-validates against the live model catalog so a stale draft can't resurrect a removed model. ## Versions and rollback [#versions-and-rollback] Edits always go to the route's **draft**. Publishing snapshots the draft as an immutable, numbered version and points the route at it; the published version is what serves traffic. Rolling back re-points the route at any previous version instantly — no data migration, no re-deploy. A route only serves requests when it is **enabled** and has a **published version**; otherwise requests using it are rejected with a `404`. ## Observability [#observability] Each request served through a dynamic route records the route name, published version, and the node path the evaluation took in its routing metadata, alongside the usual provider scoring details — visible in the activity log detail view. ## Reliability [#reliability] Published routes are cached in the gateway and carry a stale fallback: when the database is temporarily unreachable, recently used routes keep resolving from cache, the same way API keys and project settings do. # Embeddable Payments URL: https://docs.passingright-staging.sandbloc.com/features/embeddable-payments The **Payments SDK** lets you drop **end-user payments + in-app credit purchases** into your product the same way Stripe Elements lets you drop in payments. Your end-users get their **own wallet**, buy credits **inside your app**, and pay per request for any model the gateway supports. PassingRight is the merchant of record; you set a markup and keep the margin. **Preview — opt-in only.** Embeddable Payments is in preview and enabled on an opt-in basis per project. The **Settings → Payments SDK** dashboard page shows a read-only preview until the feature is turned on for your project. [Contact us](mailto:support@passingright.io) to request access before integrating. ## When to use this [#when-to-use-this] Reach for the Payments SDK when **you embed PassingRight's billing on your own site** and want **your end-users to pay for the AI they use**, each with their own wallet and credit balance. It handles end-user **payments and sessions**: per-user wallets, in-app credit top-ups (Stripe), short-lived browser sessions scoped to a single wallet, and developer-margin payouts. **This is a payments product, not an AI client SDK.** Do not confuse it with a normal AI SDK such as the OpenAI SDK or the Vercel AI SDK. If you just want to call models from your own backend with a single API key, use the [OpenAI-compatible API](https://docs.passingright-staging.sandbloc.com/quick-start) instead — you do not need the Payments SDK. Use the Payments SDK only when you need to **charge your own end-users** and give each of them a wallet and checkout inside your app. It ships as three packages: | Package | Runs in | Use it for | | ---------------------- | ------------------------- | ------------------------------------------------------------------------------------ | | `@llmgateway/server` | Your backend (secret key) | Mint end-user sessions, manage wallets/customers, verify webhooks, trigger payouts | | `@llmgateway/client` | Browser (headless) | Framework-agnostic chat/image/embeddings + balance/top-up, with auto session refresh | | `@llmgateway/elements` | React | Drop-in ``, ``, `` + hooks | A complete, runnable Next.js example lives in the templates repo: [**Embeddable Payments template**](https://github.com/theopenco/llmgateway-templates/tree/main/templates/embeddable-credits). ## How it works [#how-it-works] ``` Your backend ──(secret key sk_)──▶ POST /v1/sessions ──▶ ephemeral session token (es_, ~15 min) │ │ └────────── returns es_ to your frontend ◀────────────────┘ │ Browser (es_) ──▶ chat / images / embeddings ──▶ debits the end-user wallet └──▶ buy credits (Stripe Elements) ─▶ credits land in the wallet ``` * Your **secret key** (`sk_…`) never leaves your backend. It mints short-lived **ephemeral session tokens** (`es_…`) scoped to one end-user wallet. * The **browser** only ever holds the `es_…` token (and a publishable Stripe key). It calls the gateway directly; usage is billed to that user's wallet. * **Markup is applied at top-up time**: if you set a 20% markup and a user buys $10, their wallet is credited the net spend power and your **margin accrues to your organization** for later payout. * **Top-up bonus (optional)**: set a bonus percent to credit end-users *more* than they pay — e.g. a 50% bonus turns a $10 top-up into $15 of spend power. The extra credits are funded from **your organization's credit balance** at top-up time (capped at your available credits), so it's a promotional lever you can switch on or off anytime. Markup and bonus are independent: the bonus is applied on top of the net credited amount. ## Set up in the dashboard [#set-up-in-the-dashboard] Before you write any code, configure the project you want to embed: 1. Open the PassingRight dashboard and select your project. 2. Go to **Settings → Payments SDK** and turn on **End-user sessions** (this requires the preview to be enabled for your project). 3. *(Optional)* Set a **markup percent** — the margin you earn on every top-up — and/or a **top-up bonus percent** to gift end-users extra credit (funded from your organization's credit balance). 4. Add the browser origins allowed to call the gateway, one per line (e.g. `https://app.example.com`), then click **Save Settings**. 5. Under **Platform Secret Keys**, click **Create Live Key** (or **Create Test Key**) and copy the `sk_…` value immediately. 6. Store it as a server-side environment variable, for example `LLMGATEWAY_SECRET_KEY`. The platform secret key (`sk_…`) is different from a regular gateway API key (`llmgtwy_…`): it mints end-user sessions and must only ever be used from your backend. **Test mode.** A `sk_test_…` key is a sandbox key: end-user wallet top-ups go through Stripe's sandbox (use Stripe [test cards](https://docs.stripe.com/testing), no real charges), and its wallets are fully segregated from live ones — the same end-user gets independent test and live wallets. To keep sandbox money from buying real inference, **test-mode wallets can only call free models**: use the `auto` route (it picks a free model automatically) or a free model id; paid models return a `403`. Pair a test secret key on your backend with `mode="test"` on `` (see below) — the two must match. Test-mode top-ups never receive the developer-funded top-up bonus, and a `sk_test_…` key cannot manage webhook endpoints or use Stripe Connect onboarding/payouts (both return `403` — use a live secret key). The platform secret key is shown only once. Do not put it in frontend code, browser bundles, mobile apps, or public repos. Only a keyed HMAC-SHA-256 fingerprint and masked preview are stored, so create a replacement if you lose the secret. ## 1. Install [#1-install] ```bash # backend npm install @llmgateway/server # frontend (pick one) npm install @llmgateway/elements # React drop-in components npm install @llmgateway/client # headless / non-React ``` ## 2. Mint a session on your backend [#2-mint-a-session-on-your-backend] Identify your signed-in user and mint a session bound to their wallet. Scope which models they may call. ```ts // app/api/llmgateway/session/route.ts (Next.js Route Handler) import { PassingRight } from "@llmgateway/server"; const lg = new PassingRight({ secretKey: process.env.LLMGATEWAY_SECRET_KEY! }); export async function POST() { const session = await lg.sessions.create({ customer: { externalId: "user_123" }, // your stable user id scope: { models: ["openai/gpt-4o-mini"] }, // lock down what they can call ttlSeconds: 900, // optional, default 15 min }); return Response.json(session); // { sessionToken, walletId, endCustomerId, expiresAt, publishableKey } // publishableKey is null unless the project has an active publishable // browser key — the browser authenticates with the sessionToken (es_…). } ``` Always mint sessions server-side. Never ship your `sk_…` secret key to the browser. ## 3a. Drop in the React components [#3a-drop-in-the-react-components] Wrap your UI in `` and use the components. `fetchSession` is how the client refreshes the short-lived token before it expires. ```tsx "use client"; import { LLMGatewayProvider, Chat, CreditBalance, BuyCredits, } from "@llmgateway/elements"; const fetchSession = () => fetch("/api/llmgateway/session", { method: "POST" }).then((r) => r.json()); export default function Assistant({ session }) { return ( ); } ``` Need full control over rendering? Use the hooks instead of the components: * `useBalance()` → `{ balance, currency, recentLedger, loading, error, refetch, refetchUntilChange }` * `useChat({ model })` → `{ turns, send, streaming, ... }` `useBalance().refetchUntilChange()` polls until the balance actually changes — use it after a purchase, since the wallet is credited asynchronously once the Stripe webhook lands. ## 3b. Or go headless (any framework) [#3b-or-go-headless-any-framework] ```ts import { LLMGatewayClient } from "@llmgateway/client"; const client = new LLMGatewayClient({ session: { token: session.sessionToken, expiresAt: session.expiresAt }, refresh: fetchSession, // auto-refreshes ~60s before expiry }); // stream a completion (billed to the user's wallet) for await (const delta of client.stream({ model: "openai/gpt-4o-mini", messages: [{ role: "user", content: "Hello!" }], })) { process.stdout.write(delta); } const { balance } = await client.getBalance(); ``` The headless client also exposes `chat()`, `image()`, `embeddings()`, `getBalance()`, `createTopUp(amount)`, and `getConfig()`. ### Spend limits [#spend-limits] `getBalance()` (and `useBalance()`) also returns a `limits` object describing the spend limits enforced on the session, the amount consumed so far, and — when a windowed limit is configured — when it resets. This lets you show the user how much of their allowance is left before a request is rejected. ```ts const { balance, limits } = await client.getBalance(); // limits: { // usageLimit, // lifetime spend cap (null = uncapped) // usage, // consumed over the session's lifetime // periodUsageLimit, // per-window cap (null = no windowed limit) // periodUsageDurationValue, // e.g. 1 // periodUsageDurationUnit, // "hour" | "day" | "week" | "month" // currentPeriodUsage, // consumed in the current window // currentPeriodStartedAt, // ISO timestamp, or null // currentPeriodResetAt, // ISO timestamp the window resets, or null // } ``` `null` limit fields mean that cap is not configured. `currentPeriodUsage` is `"0"` and `currentPeriodResetAt` is `null` until the first spend in a fresh window. ## Buying credits [#buying-credits] `` creates a Stripe PaymentIntent scoped to the user's wallet, renders Stripe's `PaymentElement`, and confirms the payment. Once PassingRight's webhook processes it, the wallet is credited the **net** amount (after your markup) and your margin accrues to your organization. If you've configured a **top-up bonus**, the additional developer-funded credit is applied on top of the net amount at the same time (debited from your organization's credit balance). The top-up API response includes `netCredited` and `bonusCredited` (the boosted total is their sum), and the `wallet.credited` webhook additionally carries `totalCredited`. `@llmgateway/elements` bundles PassingRight's browser-safe Stripe publishable keys. Pass `mode="test"` to `` while developing to use Stripe test mode; omit it or pass `mode="prod"` for live payments (`"prod"` is the default). You never need to provide PassingRight's Stripe publishable key yourself, and the end-user never sees your `sk_…` secret key. The frontend `mode` prop and the backend secret key must match. A `sk_test_…` key creates the top-up PaymentIntent in the Stripe sandbox, which only the `mode="test"` publishable key can confirm — mixing a test key with `mode="prod"` (or vice versa) makes `` fail to confirm. ## Managing wallets & customers (server-side) [#managing-wallets--customers-server-side] ```ts // grant credits directly (e.g. free trial) await lg.wallets.credit({ walletId, amount: 5, reason: "Signup bonus" }); const wallet = await lg.wallets.retrieve(walletId); // analytics: customers with balances + lifetime spend const { customers } = await lg.customers.list(); const detail = await lg.customers.retrieve(endCustomerId); ``` ## Webhooks [#webhooks] Register an endpoint to react to wallet events. Events are signed (`X-PassingRight-Signature`); verify them like Stripe. ```ts await lg.webhookEndpoints.create({ url: "https://yourapp.com/webhooks/llmgateway", enabledEvents: ["wallet.credited", "wallet.low_balance"], }); // in your handler const event = lg.webhooks.constructEvent( rawBody, signatureHeader, endpointSecret, ); ``` Webhook URLs must be **https** and public — requests to private/internal addresses are rejected (SSRF protection), both at registration and at delivery time. ## Margin payouts (Stripe Connect) [#margin-payouts-stripe-connect] Your accrued markup is held as a margin balance. Onboard a connected account and pay it out: ```ts const { url } = await lg.connect.createOnboardingLink({ refreshUrl: "https://yourapp.com/settings/payouts", returnUrl: "https://yourapp.com/settings/payouts?done=1", }); // redirect the developer to `url`, then later: const status = await lg.connect.status(); // { onboarded, payoutsEnabled, marginBalance } const payout = await lg.connect.payout(); // transfer the accrued margin out ``` ## Security model [#security-model] * **Ephemeral tokens** (`es_…`) are short-lived and revocable; mint them per-user from your backend. Platform secrets and ephemeral tokens are stored only as keyed fingerprints, never as retrievable plaintext. * **Model scopes** restrict each session to an allow-list of models. * **Origin allowlist** (configured on the project) blocks browser calls from unexpected origins. * **Per-session spend caps** (`scope.maxSpend`) bound how much a single session can spend. ## Full example [#full-example] The end-to-end Next.js app — backend session route, provider, chat, and buy-credits — is in the templates repo: ➡️ [**Embeddable Payments template**](https://github.com/theopenco/llmgateway-templates/tree/main/templates/embeddable-credits) # Embeddings URL: https://docs.passingright-staging.sandbloc.com/features/embeddings PassingRight exposes an OpenAI-compatible `/v1/embeddings` endpoint for generating vector representations of text — useful for semantic search, clustering, recommendations, and RAG. Browse available embedding models on the [models page](https://passingright.io/models?filters=1\&embedding=true). ## Supported providers [#supported-providers] * **OpenAI** — `text-embedding-3-small`, `text-embedding-3-large`, `text-embedding-ada-002` * **Google AI Studio** — `gemini-embedding-2` (recommended), `gemini-embedding-001` (legacy) * **Google Vertex AI** — `gemini-embedding-001`, `text-embedding-005` The gateway translates between provider-native request/response shapes (e.g. Google's `:embedContent` / `:batchEmbedContents`) and the OpenAI-compatible payload, so you can swap models without changing your client code. ## cURL [#curl] ```bash curl -X POST "https://api.passingright.io/v1/embeddings" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "text-embedding-3-small", "input": "The quick brown fox jumps over the lazy dog." }' ``` ## OpenAI JS SDK [#openai-js-sdk] ```ts import OpenAI from "openai"; const client = new OpenAI({ apiKey: process.env.LLM_GATEWAY_API_KEY, baseURL: "https://api.passingright.io/v1", }); const response = await client.embeddings.create({ model: "text-embedding-3-small", input: "The quick brown fox jumps over the lazy dog.", }); console.log(response.data[0].embedding); ``` Embedding models are billed only for input tokens. There are no output tokens since embeddings are fixed-size vectors. ## Improving retrieval quality [#improving-retrieval-quality] Vector similarity is fast but approximate. For RAG, pair embeddings with [rerank](https://docs.passingright-staging.sandbloc.com/features/rerank): use embeddings to pull a broad candidate set, then rerank those candidates to pick the few documents that actually go in the prompt. # Guardrails URL: https://docs.passingright-staging.sandbloc.com/features/guardrails Guardrails protect your organization by automatically detecting and blocking harmful content in LLM requests before they reach the model. Guardrails are available on the [**Enterprise plan**](https://passingright.io/enterprise). ## Overview [#overview] Guardrails run on every API request, scanning message content for: * Security threats (prompt injection, jailbreak attempts) * Sensitive data (PII, secrets, credentials) * Policy violations (blocked terms, restricted topics) When a violation is detected, you control what happens: block the request, redact the content, or log a warning. ## Organization and Project Scopes [#organization-and-project-scopes] Guardrails are configured for the whole organization by default, and every project uses that configuration. A project can opt out and define its own guardrails instead — useful when one application needs stricter or looser rules than the rest of the organization. Turn off **Use organization guardrails** in the project's Guardrails settings, and that project's configuration and custom rules fully replace the organization's for its traffic. Exactly one scope is ever in force for a request: | Project setting | What runs on that project's requests | | ----------------------------------- | --------------------------------------------------------- | | **Use organization guardrails** on | The organization configuration and its custom rules | | **Use organization guardrails** off | The project's own configuration and its custom rules only | Opting out never inherits partially: the organization's custom rules stop applying to that project too, and the project starts with a copy of the organization's current settings so nothing is silently dropped at the moment you opt out. Other projects are unaffected either way. Both pages state which scope wins. A project page names itself and, while it inherits, shows the organization settings read-only; the organization page lists any projects that override it, so it is clear those settings apply everywhere else. Only organization owners and admins can change guardrails, at either scope. ## System Rules [#system-rules] Built-in rules protect against common threats: ### Prompt Injection Detection [#prompt-injection-detection] Detects attempts to override or manipulate system instructions. Common patterns include: * "Ignore all previous instructions" * "You are now a different AI" * Hidden instructions in encoded text ### Jailbreak Detection [#jailbreak-detection] Identifies attempts to bypass safety measures: * DAN (Do Anything Now) prompts * Roleplay-based bypasses * Instruction override attempts ### PII Detection [#pii-detection] Identifies personal information: * Email addresses * Phone numbers * Social Security Numbers * Credit card numbers * IP addresses * Passport and driver's license numbers When the action is set to **redact**, PII is replaced with placeholders like `[EMAIL_REDACTED]`. Detection is validated rather than purely pattern-based, so identifiers that only look like PII are left alone: card numbers must pass the Luhn checksum, phone numbers need grouping separators or a nearby phone keyword, and passport and license numbers need a nearby keyword. A bare numeric id such as a product id, order number or unix timestamp is therefore never redacted as a phone number. ### Secrets Detection [#secrets-detection] Detects credentials and API keys: * AWS access keys and secrets * Generic API keys * Passwords in common formats * Private keys High-entropy checks and placeholder filtering keep ordinary content out of the way: git SHAs and hex digests are not treated as AWS secrets, and template values such as `YOUR_API_KEY`, `${OPENAI_API_KEY}` or `********` are not treated as credentials. ### File Type Restrictions [#file-type-restrictions] Control which file types can be uploaded: * Configure allowed MIME types * Set maximum file size limits * Block potentially dangerous file types ### Document Leakage Prevention [#document-leakage-prevention] Detects attempts to extract confidential documents or internal data. ## Configurable Actions [#configurable-actions] For each rule, choose how to respond: | Action | Behavior | | ---------- | --------------------------------------------------- | | **Block** | Reject the request with a content policy error | | **Redact** | Remove or mask the sensitive content, then continue | | **Warn** | Log the violation but allow the request to proceed | ## Custom Rules [#custom-rules] Create custom rules for your use case, on the organization or on a single project: ### Blocked Terms [#blocked-terms] Prevent specific words or phrases from being used: * Match type: exact, contains, or regex * Case-sensitive matching option * Multiple terms per rule ### Custom Regex [#custom-regex] Match patterns unique to your organization: * Internal project codenames * Customer identifiers * Domain-specific sensitive data ### Topic Restrictions [#topic-restrictions] Block content related to specific topics: * Define restricted topics * Keyword-based detection ## Security Events Dashboard [#security-events-dashboard] Monitor all guardrail violations with a dedicated dashboard: * **Total violations** — Overall count and trends * **By action** — Breakdown of blocked, redacted, and warned * **By category** — Which rules are being triggered * **Detailed logs** — Individual violations with timestamps and matched patterns ## How It Works [#how-it-works] ``` Request → Guardrails Check → Action Based on Rules → Forward to Model (if allowed) ↓ Log Violation ``` 1. **Request received** — API request comes in with messages 2. **Scope resolved** — The project's own guardrails if it overrides, otherwise the organization's 3. **Content scanned** — All text content is checked against enabled rules 4. **Violations detected** — Matches are identified and logged 5. **Action taken** — Based on rule configuration (block/redact/warn) 6. **Request proceeds** — If not blocked, the (potentially redacted) request continues ## Best Practices [#best-practices] 1. **Start with warnings** — Enable rules in warn mode first to understand your traffic patterns 2. **Review violations** — Check the Security Events dashboard regularly 3. **Tune custom rules** — Adjust blocked terms and regex patterns based on false positives 4. **Layer defenses** — Use multiple rule types together for comprehensive protection ## Get Started [#get-started] Guardrails are an Enterprise feature. [Contact us](https://passingright.io/enterprise) to enable Enterprise for your organization. # Image Generation URL: https://docs.passingright-staging.sandbloc.com/features/image-generation PassingRight supports image generation through two APIs: 1. **`/v1/images/generations`** — OpenAI-compatible images endpoint (recommended for simple image generation) 2. **`/v1/images/edits`** — OpenAI-compatible image editing endpoint 3. **`/v1/chat/completions`** — Chat completions with image generation models (for conversational image generation and editing) For asynchronous video generation, see [Video Generation](https://docs.passingright-staging.sandbloc.com/features/video-generation). ## Available Models [#available-models] You can find all available image generation models on our [models page](https://passingright.io/models?filters=1\&imageGeneration=true). ## OpenAI Images API [#openai-images-api] The `/v1/images/generations` endpoint provides a drop-in replacement for OpenAI's image generation API. It works with any OpenAI-compatible client library. ### Parameters [#parameters] | Parameter | Type | Default | Description | | ----------------- | ------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------- | | `prompt` | string | required | A text description of the desired image(s) | | `model` | string | `"auto"` | The model to use. `auto` resolves to `gemini-3-pro-image` | | `n` | integer | `1` | Number of images to generate (1-10) | | `size` | string | — | Image dimensions. Supported sizes depend on the model/provider — see [Image Configuration](#image-configuration) | | `quality` | string | — | Image quality. Supported values depend on the model/provider — see [Image Configuration](#image-configuration) | | `moderation` | string | `"auto"` | Content filtering strictness for models that support it: `auto` or `low` — see [Moderation](#moderation) | | `response_format` | string | `"b64_json"` | Only `b64_json` is supported | | `style` | string | — | Image style: `vivid` or `natural` | | `service_tier` | string | — | Processing tier for mappings that offer one: `flex`, `priority`, or `default` — see [Service Tiers](https://docs.passingright-staging.sandbloc.com/features/service-tiers) | ### curl [#curl] ```bash curl -X POST "https://api.passingright.io/v1/images/generations" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gemini-3-pro-image", "prompt": "A cute cat wearing a tiny top hat", "n": 1, "size": "1024x1024" }' ``` ### Usage and cost [#usage-and-cost] Both `/v1/images/generations` and `/v1/images/edits` return a `usage` object with the token counts and the billed cost of the request, in USD: ```json "usage": { "input_tokens": 10, "input_tokens_details": { "image_tokens": 0, "text_tokens": 10 }, "output_tokens": 229, "output_tokens_details": { "image_tokens": 229, "text_tokens": 0 }, "total_tokens": 239, "cost": 0.00692, "cost_details": { "input_cost": 0.00005, "image_output_cost": 0.00687 } } ``` `cost_details` has the same fields as `usage.cost_details` on [chat completions](#response-format). ### OpenAI SDK [#openai-sdk] Works with the standard OpenAI client library — just point the base URL to PassingRight. ```ts import OpenAI from "openai"; import { writeFileSync } from "fs"; const client = new OpenAI({ baseURL: "https://api.passingright.io/v1", apiKey: process.env.LLM_GATEWAY_API_KEY, }); const response = await client.images.generate({ model: "gemini-3-pro-image", prompt: "A futuristic city skyline at sunset with flying cars", n: 1, size: "1024x1024", }); response.data.forEach((image, i) => { if (image.b64_json) { const buf = Buffer.from(image.b64_json, "base64"); writeFileSync(`image-${i}.png`, buf); } }); ``` ### Vercel AI SDK [#vercel-ai-sdk] Use the `@llmgateway/ai-sdk-provider` with `generateImage`. ```ts import { createLLMGateway } from "@llmgateway/ai-sdk-provider"; import { generateImage } from "ai"; import { writeFileSync } from "fs"; const llmgateway = createLLMGateway({ apiKey: process.env.LLM_GATEWAY_API_KEY, }); const result = await generateImage({ model: llmgateway.image("gemini-3-pro-image"), prompt: "A cozy cabin in a snowy mountain landscape at night with aurora borealis", size: "1024x1024", n: 1, // aspectRatio and quality are model-specific — only some providers honor them. // aspectRatio works on Gemini image models; OpenAI gpt-image-2 ignores it // (use a literal WxH `size` instead). aspectRatio: "16:9", // quality works on OpenAI gpt-image-2 ("low" | "medium" | "high" | "auto"). // The AI SDK only forwards it through providerOptions. providerOptions: { llmgateway: { quality: "high" }, }, }); result.images.forEach((image, i) => { const buf = Buffer.from(image.base64, "base64"); writeFileSync(`image-${i}.png`, buf); }); ``` ## OpenAI Images Edit API [#openai-images-edit-api] The `/v1/images/edits` endpoint is OpenAI-compatible and supports a focused subset of `images.edit` parameters. ### Parameters [#parameters-1] | Parameter | Type | Required | Description | | -------------------- | ------------------------ | -------- | --------------------------------------------------------------------------------- | | `images` | array of `{ image_url }` | yes | Input images. `image_url` supports HTTPS URLs and base64 data URLs | | `prompt` | string | yes | A text description of the desired image edit | | `model` | string | no | Image editing model | | `background` | enum | no | `transparent`, `opaque`, or `auto` | | `input_fidelity` | enum | no | `high` or `low` | | `n` | integer | no | Number of edited images to generate | | `output_format` | enum | no | `png`, `jpeg`, or `webp` | | `output_compression` | integer | no | Compression level for `jpeg`/`webp` | | `quality` | enum | no | `low`, `medium`, `high`, or `auto`; GPT Image 2.5 also supports `xhigh` and `max` | | `moderation` | enum | no | `auto` or `low` — see [Moderation](#moderation) | | `size` | string | no | Output size. Examples: `1024x1024`, `1536x1024`, `1K`, `2K`, `4K` | | `aspect_ratio` | string | no | Aspect ratio override. Examples: `1:1`, `16:9`, `4:3`, `5:4` | | `service_tier` | enum | no | `flex`, `priority`, or `default` — see [Service Tiers](https://docs.passingright-staging.sandbloc.com/features/service-tiers) | `mask` is not supported yet on `/v1/images/edits`. ### curl (HTTPS image URL) [#curl-https-image-url] ```bash curl -X POST "https://api.passingright.io/v1/images/edits" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "images": [ { "image_url": "https://example.com/source-image.png" } ], "prompt": "Add a watercolor effect to this image", "model": "gemini-3-pro-image", "aspect_ratio": "16:9", "quality": "high", "size": "4K" }' ``` ### curl (base64 data URL) [#curl-base64-data-url] ```bash curl -X POST "https://api.passingright.io/v1/images/edits" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "images": [ { "image_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..." } ], "prompt": "Turn this into a pixel-art style image" }' ``` ## Chat Completions API [#chat-completions-api] Image generation also works through the `/v1/chat/completions` endpoint, which is useful for conversational image generation, image editing with vision, and multi-turn interactions. ### Making Requests [#making-requests] Simply use an image generation model and provide a text prompt describing the image you want to create. ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gemini-3-pro-image", "messages": [ { "role": "user", "content": "Generate an image of a cute golden retriever puppy playing in a sunny meadow" } ] }' ``` ### Response Format [#response-format] Image generation models return responses in the standard chat completions format, with generated images included in the `images` array within the assistant message: ```json { "id": "chatcmpl-1756234109285", "object": "chat.completion", "created": 1756234109, "model": "gemini-3-pro-image", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "Here's an image of a cute dog for you: ", "images": [ { "type": "image_url", "image_url": { "url": "data:image/png;base64," } } ] }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 8, "completion_tokens": 1303, "total_tokens": 1311 } } ``` A request stopped by the [gateway content filter](https://docs.passingright-staging.sandbloc.com/resources/error-handling#gateway-content-filter) returns `finish_reason: "content_filter"` on the Chat Completions API and an empty `data` array on the Images API. A provider's own safety rejection is returned the same way, and it is billed the way the provider bills us: a rejection the provider returns as an error carries no usage and costs nothing, apart from a rejection fee the provider publishes (currently only xAI's chat models). A block the provider serves as a normal response with usage, as Gemini image models do, is charged for the input tokens it reports, never for the blocked image. ### Vision support [#vision-support] You can edit or modify images by combining image generation with [vision models](https://docs.passingright-staging.sandbloc.com/features/vision) by including the image in the `messages` array. ### Response Structure [#response-structure] #### Images Array [#images-array] The `images` array contains one or more generated images with the following structure: * `type`: Always `"image_url"` for generated images * `image_url.url`: A data URL containing the base64-encoded image data (format: `data:image/png;base64,`) #### Content Field [#content-field] The `content` field may contain descriptive text about the generated image, depending on the model's behavior. ### AI SDK (Chat Completions) [#ai-sdk-chat-completions] You can use the AI SDK to generate images with your existing generateText or streamText calls using the PassingRight provider. #### Example [#example] ```ts title="/api/chat/route.ts" import { streamText, type UIMessage, convertToModelMessages } from "ai"; import { createLLMGateway } from "@llmgateway/ai-sdk-provider"; interface ChatRequestBody { messages: UIMessage[]; } export async function POST(req: Request) { const body = await req.json(); const { messages }: ChatRequestBody = body; const llmgateway = createLLMGateway({ apiKey: "llmgateway_api_key", baseUrl: "https://api.passingright.io/v1", }); try { const result = streamText({ model: llmgateway.chat("gemini-3-pro-image"), messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse(); } catch { return new Response( JSON.stringify({ error: "PassingRight request failed" }), { status: 500, }, ); } } ``` Then you can render the image in your frontend using the `Image` component from the [ai-elements](https://ai-sdk.dev/elements/components/image). Here is a full example of how to use the AI SDK to generate images in your frontend: ```tsx title="/app/page.tsx" "use client"; import { useState, useRef } from "react"; import { useChat } from "@ai-sdk/react"; import { parseImagePartToDataUrl } from "@/lib/image-utils"; import { PromptInput, PromptInputBody, PromptInputButton, PromptInputSubmit, PromptInputTextarea, PromptInputToolbar, } from "@/components/ai-elements/prompt-input"; import { Conversation, ConversationContent, } from "@/components/ai-elements/conversation"; import { Image } from "@/components/ai-elements/image"; import { Loader } from "@/components/ai-elements/loader"; import { Message, MessageContent } from "@/components/ai-elements/message"; import { Response } from "@/components/ai-elements/response"; export const ChatUI = () => { const textareaRef = useRef(null); const [text, setText] = useState(""); const { messages, status, stop, regenerate, sendMessage } = useChat(); return ( <>
{messages.length === 0 ? (

How can I help you?

) : ( messages.map((m, messageIndex) => { const isLastMessage = messageIndex === messages.length - 1; if (m.role === "assistant") { const textContent = m.parts .filter((p) => p.type === "text") .map((p) => p.text) .join(""); // Combine all image parts (both image_url and file types) const imageParts = m.parts.filter( (p) => p.type === "file" && p.mediaType?.startsWith("image/"), ); return (
{textContent ? {textContent} : null} {imageParts.length > 0 ? (
{imageParts.map((part, idx: number) => { const { base64Only, mediaType } = parseImagePartToDataUrl(part); if (!base64Only) { return null; } return ( {part.name ); })}
) : null} {isLastMessage && (status === "submitted" || status === "streaming") && ( )}
); } else { return ( {m.parts.map((p, i) => { if (p.type === "text") { return
{p.text}
; } return null; })}
{isLastMessage && (status === "submitted" || status === "streaming") && ( )}
); } }) )}
{ if (status === "streaming") { return; } try { const textContent = message.text ?? ""; if (!textContent.trim()) { return; } setText(""); // Clear input immediately const parts = [{ type: "text", text: textContent }]; // Call sendMessage which will handle adding the user message and API request sendMessage({ role: "user", parts, }); } catch (error) { // Throw error here } }} > setText(e.currentTarget.value)} placeholder="Message" />
{status === "streaming" ? ( stop()} variant="ghost"> Stop ) : null}
); }; ``` ```ts title="/lib/image-utils.ts" /** * Parses a file object containing image data and returns a properly formatted data URL * and normalized media type. * * Handles: * - Normalizing mediaType from various property names (mediaType, mime_type) * - Detecting existing data: URLs * - Detecting base64-looking content * - Stripping whitespace from base64 content * - Building proper data:...;base64,... URLs */ export function parseImageFile(file: { url?: string; mediaType?: string; mime_type?: string; }): { dataUrl: string; mediaType: string } { const mediaType = file.mediaType || file.mime_type || "image/png"; let url = String(file.url || ""); const isDataUrl = url.startsWith("data:"); const looksLikeBase64 = !isDataUrl && /^[A-Za-z0-9+/=\s]+$/.test(url.slice(0, 200)); if (looksLikeBase64) { url = url.replace(/\s+/g, ""); } const dataUrl = isDataUrl ? url : looksLikeBase64 ? `data:${mediaType};base64,${url}` : url; return { dataUrl, mediaType }; } /** * Extracts base64-only content from a data URL. * Returns empty string if the input is not a valid data URL. */ export function extractBase64FromDataUrl(dataUrl: string): string { if (!dataUrl.startsWith("data:")) { return ""; } const comma = dataUrl.indexOf(","); return comma >= 0 ? dataUrl.slice(comma + 1) : ""; } /** * Parses an image part (either image_url or file type) and returns * dataUrl, base64Only, and mediaType ready for rendering. * * Handles error cases gracefully by returning empty base64Only string * when parsing fails, allowing the renderer to skip invalid images. */ export function parseImagePartToDataUrl(part: any): { dataUrl: string; base64Only: string; mediaType: string; } { try { // Handle image_url parts if (part.type === "image_url" && part.image_url?.url) { const url = part.image_url.url; const mediaType = "image/png"; // Default for image_url parts if (url.startsWith("data:")) { // Extract media type from data URL if present const match = url.match(/data:([^;]+)/); const extractedMediaType = match?.[1] || mediaType; return { dataUrl: url, base64Only: extractBase64FromDataUrl(url), mediaType: extractedMediaType, }; } return { dataUrl: url, base64Only: "", mediaType, }; } // Handle file parts (AI SDK format) if (part.type === "file") { const { dataUrl, mediaType } = parseImageFile(part); return { dataUrl, base64Only: extractBase64FromDataUrl(dataUrl), mediaType, }; } return { dataUrl: "", base64Only: "", mediaType: "image/png", }; } catch { return { dataUrl: "", base64Only: "", mediaType: "image/png", }; } } ``` ## Image Configuration [#image-configuration] You can customize the generated image using the optional `image_config` parameter (for chat completions) or `size`/`quality`/`style` parameters (for the images API). The supported parameters vary by provider. ### Google Models [#google-models] Available Google models: | Model | Description | | ------------------------ | ----------------------------------------------------------------------------------- | | `gemini-3-pro-image` | Gemini 3 Pro with native image generation. Supports aspect ratios and 1K–4K sizes. | | `gemini-3.1-flash-image` | Gemini 3.1 Flash with native image generation. Supports 0.5K–4K sizes (default 1K). | #### gemini-3-pro-image [#gemini-3-pro-image] ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gemini-3-pro-image", "messages": [ { "role": "user", "content": "Generate an image of a mountain landscape at sunset" } ], "image_config": { "aspect_ratio": "16:9", "image_size": "4K" } }' ``` | Parameter | Type | Description | | -------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------- | | `aspect_ratio` | string | The aspect ratio of the generated image. Options: `"1:1"`, `"2:3"`, `"3:2"`, `"3:4"`, `"4:3"`, `"4:5"`, `"5:4"`, `"9:16"`, `"16:9"`, `"21:9"` | | `image_size` | string | The resolution of the generated image. Options: `"1K"` (1024x1024), `"2K"` (2048x2048), `"4K"` (4096x4096) | #### gemini-3.1-flash-image [#gemini-31-flash-image] ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gemini-3.1-flash-image", "messages": [ { "role": "user", "content": "Generate an image of a mountain landscape at sunset" } ], "image_config": { "image_size": "1K" } }' ``` | Parameter | Type | Description | | -------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `aspect_ratio` | string | The aspect ratio of the generated image. Options: `"1:1"`, `"1:4"`, `"1:8"`, `"2:3"`, `"3:2"`, `"3:4"`, `"4:1"`, `"4:3"`, `"4:5"`, `"5:4"`, `"8:1"`, `"9:16"`, `"16:9"`, `"21:9"` | | `image_size` | string | The resolution of the generated image. Options: `"0.5K"` (512x512), `"1K"` (1024x1024, default), `"2K"` (2048x2048), `"4K"` (4096x4096) | `gemini-3.1-flash-image` uniquely supports `"0.5K"` resolution, which is not available on other Google image models. ### Meta Models [#meta-models] Muse Image uses Meta's Responses API for generation and editing. It reasons before rendering and can use reference images across refinement turns. ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "meta/muse-image-1.0", "messages": [ { "role": "user", "content": "Create a product photo on a warm studio background" } ], "image_config": { "image_size": "1024x1536" } }' ``` | Parameter | Type | Description | | ------------ | ------ | -------------------------------------------------------------------------- | | `image_size` | string | One of `"1024x1024"`, `"1024x1536"`, or `"1536x1024"`. Defaults to square. | Muse Image does not expose a quality setting. Use `image_size` to choose square, portrait, or landscape output. ### Alibaba Models [#alibaba-models] ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "alibaba/qwen-image-3.0", "messages": [ { "role": "user", "content": "Generate an image of a mountain landscape at sunset" } ], "image_config": { "image_size": "1024x1536", "n": 1, "seed": 42 } }' ``` | Parameter | Type | Description | | ------------ | ------- | ------------------------------------------------------------------------------------------------ | | `image_size` | string | Image dimensions in `WIDTHxHEIGHT` format. Examples: `"1024x1024"`, `"1024x1536"`, `"1536x1024"` | | `n` | integer | Number of images to generate (1-4) | | `seed` | integer | Random seed for reproducible generation | Available Alibaba models (see the [models page](https://passingright.io/models?filters=1\&imageGeneration=true) for current pricing): | Model | Description | | ---------------------------- | ------------------------------------------------------------------------------------ | | `alibaba/qwen-image-3.0` | Third-generation image generation and editing | | `alibaba/qwen-image-3.0-pro` | Highest quality third-generation generation and editing. Priced per output size tier | Alibaba models use explicit pixel dimensions (e.g., `"1024x1536"`) instead of aspect ratios. For portrait orientation use `"1024x1536"`, for landscape use `"1536x1024"`. ### Z.AI Models [#zai-models] ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "zai/cogview-4", "messages": [ { "role": "user", "content": "Generate an image of a futuristic city skyline" } ], "image_config": { "image_size": "1024x1024" } }' ``` | Parameter | Type | Description | | ------------ | ------- | ------------------------------------------------------------------------------------------------ | | `image_size` | string | Image dimensions in `WIDTHxHEIGHT` format. Examples: `"1024x1024"`, `"2048x1024"`, `"1024x2048"` | | `n` | integer | Number of images to generate | Available Z.AI models (see the [models page](https://passingright.io/models?filters=1\&imageGeneration=true) for current pricing): | Model | Description | | --------------- | ------------------------------------------------------------------------------------------------------------------- | | `zai/cogview-4` | CogView-4 with bilingual support and excellent text rendering | | `zai/glm-image` | GLM-Image with hybrid auto-regressive architecture, excellent for text-rendering and knowledge-intensive generation | CogView-4 supports both Chinese and English prompts and excels at generating images with embedded text. ### OpenAI Models [#openai-models] ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-image-2", "messages": [ { "role": "user", "content": "Generate a photo-real cinematic landscape at golden hour" } ], "image_config": { "image_size": "3072x2160", "image_quality": "low" } }' ``` | Parameter | Type | Description | | --------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------- | | `image_size` | string | Image dimensions in `WIDTHxHEIGHT` format, or `"auto"` to let the model choose. | | `image_quality` | string | `"low"`, `"medium"`, `"high"`, or `"auto"`; GPT Image 2.5 also supports `"xhigh"` and `"max"`. Defaults to `"auto"` when omitted. | | `moderation` | string | `"auto"` or `"low"` — see [Moderation](#moderation). Defaults to `"auto"` when omitted. | OpenAI image models do **not** accept `aspect_ratio`. Always specify `image_size` as `WIDTHxHEIGHT` (e.g. `"1024x1024"`, `"3072x2160"`). OpenAI requires both width and height to be divisible by 16, the longest edge to be ≤ 3840, and the total pixel count to fit within the model's pixel budget; requests outside these bounds are rejected with HTTP 400. Available OpenAI image models: | Model | Description | | ------------------------------- | ------------------------------------------------------------------------------------------------------------ | | `openai/gpt-image-2` | OpenAI's next-generation image model with improved quality and prompt adherence, supporting text and vision. | | `openai/gpt-image-2.5-sunburst` | Image generation and precise editing with text and image inputs; adds `xhigh` and `max` quality. | | `openai/gpt-image-2.5-flare` | Fast everyday image generation and editing with text and image inputs; adds `xhigh` and `max` quality. | GPT Image 2.5 supports `1024x1024`, `1536x1024`, `1024x1536`, `auto`, and custom sizes within the limits above. Its aspect ratio must stay between 1:3 and 3:1, with 655,360–8,294,400 total pixels. Resolutions above `2560x1440` are experimental. Both variants use the same per-token rates as GPT Image 2. Actual image token usage varies by model, size, quality, and input; billing uses the provider's reported usage. See the [models page](https://passingright.io/models?filters=1\&imageGeneration=true) for current pricing. GPT Image 2 and both GPT Image 2.5 variants are also served through Azure at the same rates — swap the `openai/` prefix for `azure/` to pin that route, or send the bare model id and let the gateway pick. ### ByteDance Models [#bytedance-models] ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "bytedance/seedream-4-5", "messages": [ { "role": "user", "content": "Generate an image of a futuristic cyberpunk city at night" } ], "image_config": { "image_size": "2048x2048" } }' ``` | Parameter | Type | Description | | ------------ | ------ | ------------------------------------------------------------------------------------------------ | | `image_size` | string | Image dimensions in `WIDTHxHEIGHT` format. Examples: `"1024x1024"`, `"2048x2048"`, `"4096x4096"` | Available ByteDance models (see the [models page](https://passingright.io/models?filters=1\&imageGeneration=true) for current pricing): | Model | Description | | ---------------------------- | --------------------------------------------------------------- | | `bytedance/seedream-4-0` | High-quality text-to-image generation with 2K default output | | `bytedance/seedream-4-5` | Enhanced quality and consistency with improved prompt adherence | | `bytedance/seedream-5-0-pro` | Precise generation and reference-image editing at 1K or 2K | Seedream models support up to 2-10 reference images for multi-image fusion and generation. The default output resolution is 2048×2048 (2K), with support up to 4096×4096 (4K). ## Moderation [#moderation] GPT Image models expose a `moderation` parameter that controls how strict the provider's content filtering is: * `auto` (default) — standard filtering, which limits certain categories of potentially age-inappropriate content. * `low` — less restrictive filtering. OpenAI's content policy still applies. Send it as a top-level parameter on `/v1/images/generations` and `/v1/images/edits`, or inside `image_config` on `/v1/chat/completions`: ```bash curl -X POST "https://api.passingright.io/v1/images/generations" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-image-2.5-flare", "prompt": "A cute cat wearing a tiny top hat", "moderation": "low" }' ``` ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-image-2.5-flare", "messages": [{ "role": "user", "content": "A cute cat wearing a tiny top hat" }], "image_config": { "moderation": "low" } }' ``` Models without a moderation control ignore the parameter. ## Usage Notes [#usage-notes] Image generation models typically have higher token costs compared to text-only models due to the computational requirements of image synthesis. Generated images are returned as base64-encoded data URLs, which can be large. Consider the payload size when integrating image generation into your applications. # Lounge connectors URL: https://docs.passingright-staging.sandbloc.com/features/lounge-connectors Open **Connectors** in the Lounge to connect an account through OAuth. Connections are private to your account, and tool calls require your approval before they run. On iOS, open **Connectors** from the home screen. Sign-in opens in the system browser and returns to the app. You can search connections, pause them, reconnect, or disconnect from this screen. In an iOS conversation, review each proposed tool and its arguments, then choose **Approve** or **Decline**. The response continues once all pending requests are answered. Saved conversations retain pending approvals and completed results across restarts; temporary conversations keep them only for the current session. In an iOS model comparison, connected apps are available to **Model 1**. Approve or decline its pending requests before sending another message to all models. Other models keep separate conversations and can be retried without running connector actions. Pending requests and results survive restarts. If a tool's result cannot be confirmed, check the connected app before asking to run it again. **Continue response** lets the model respond to that uncertainty without repeating the tool call. A connector stays **Not configured** until the deployment has both its OAuth client ID and secret. Missing credentials also disable existing connections: you cannot enable, reconnect, or use their tools until configuration is restored. You can still disconnect an account. Pausing a configured connection retains its credentials and stops tool access. Disconnecting deletes its stored credentials and pending authorizations. Tool results used in a chat are sent to the selected model and saved with that conversation. Authorization must finish in the same session that started it and expires after ten minutes. If you sign out or the request expires during consent, start connecting again. Cancelling reconnection keeps the existing connection. ## Client integrations [#client-integrations] Authenticated clients can stream chat and signed tool proposals from `POST /lounge/chat`, using `x-llmgateway-key` to select the billing key. The endpoint accepts AI SDK UI messages and connector IDs. It does not execute tools or save conversations. After the user approves a proposal, execute it through `POST /connectors/{connectorId}/tools/{toolName}` and record its result before continuing. Send completed, failed, or denied tool parts with the next chat request. Pending approvals must be resolved first; never automatically retry a tool whose outcome is unknown. For deployment variables and callback setup, see the [connector setup guide](https://github.com/theopenco/llmgateway/blob/main/docs/lounge-connectors.md). # Master Keys URL: https://docs.passingright-staging.sandbloc.com/features/master-keys Master keys are org-scoped bearer tokens that let you create projects, gateway API keys, IAM rules (both per key and per organization member), and your organization's custom providers and custom models programmatically — without going through the dashboard. They are intended for server-to-server provisioning (e.g. multi-tenant onboarding from your own backend). They also expose [usage and cost reporting](#usage-and-cost-reporting), so you can pull per-member and per-model spend into your own dashboards, data warehouse, or chargeback process. Master keys are available on the **Enterprise** plan only. Contact us at [support@passingright.io](mailto:support@passingright.io) to enable them for your organization. ## Security [#security] * Master keys are stored as **HMAC-SHA256 hashes** in the database (using the `GATEWAY_API_KEY_HASH_SECRET` secret). The plain token is shown to you **only once** at creation time. * Each master key is scoped to a single organization and cannot access resources in other organizations. * Deleting or deactivating a master key revokes all programmatic access immediately. * All creates/deletes/status changes are recorded in your organization audit log. ## Limits [#limits] * Maximum **10 active master keys per organization**. * Programmatic project and API-key creation enforces the same per-org and per-project limits as the dashboard flow. ## Managing master keys [#managing-master-keys] In the dashboard, go to **Organization → Master Keys**. From there you can: * Create a new master key (the plain token is shown once — copy it immediately). * View the masked token, status, creator, and last-used timestamp for each existing key. * Activate / deactivate or delete keys. ## Authentication [#authentication] All programmatic endpoints live under `/v1/master/*` and require a master key in the `Authorization` header: ``` Authorization: Bearer llmgmk_... ``` A request with a missing, invalid, inactive, or non-enterprise master key receives a 401 / 403 response. ## Endpoints [#endpoints] ### List projects [#list-projects] `GET /v1/master/projects` Returns all non-deleted projects in the master key's organization. ```bash curl https://internal.passingright.io/v1/master/projects \ -H "Authorization: Bearer $MASTER_KEY" ``` Response (200): ```json { "projects": [ { "id": "proj_...", "name": "Customer ACME", "organizationId": "org_...", "cachingEnabled": false, "cacheDurationSeconds": 60, "mode": "hybrid", "status": "active", "createdAt": "...", "updatedAt": "..." } ] } ``` ### Create a project [#create-a-project] `POST /v1/master/projects` ```bash curl -X POST https://internal.passingright.io/v1/master/projects \ -H "Authorization: Bearer $MASTER_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Customer ACME", "cachingEnabled": false, "mode": "hybrid" }' ``` Body parameters: | Field | Type | Description | | ---------------------- | ------------------------------------------------ | -------------------------- | | `name` | string | Project name (1–255 chars) | | `cachingEnabled` | boolean (optional) | Default `false` | | `cacheDurationSeconds` | number (optional) | 10–31536000, default 60 | | `mode` | `"api-keys" \| "credits" \| "hybrid"` (optional) | Default `"hybrid"` | Response (201): the created project. ### Update a project [#update-a-project] `PATCH /v1/master/projects/{id}` Updates a project owned by the master key's organization. All body fields are optional; provide only the ones you want to change. ```bash curl -X PATCH https://internal.passingright.io/v1/master/projects/proj_... \ -H "Authorization: Bearer $MASTER_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Customer ACME (renamed)", "cachingEnabled": true, "status": "inactive" }' ``` Body parameters (all optional, at least one required): | Field | Type | Description | | -------------------------- | ------------------------------------- | ----------------------------------------------------------------- | | `name` | string | 1–255 chars | | `cachingEnabled` | boolean | | | `cacheDurationSeconds` | number | 10–31536000 | | `providerCacheControlMode` | `"auto" \| "passthrough" \| "off"` | [Provider cache writes](https://docs.passingright-staging.sandbloc.com/features/caching/provider-cache-control) | | `mode` | `"api-keys" \| "credits" \| "hybrid"` | | | `status` | `"active" \| "inactive"` | Toggle the project without deleting | Response (200): the updated project. It carries both `providerCacheControlMode` and the older `providerCacheControlEnabled` boolean, which reports `false` only for `"off"`. ### Delete a project [#delete-a-project] `DELETE /v1/master/projects/{id}` Soft-deletes a project (sets `status` to `"deleted"`). Cascades to its API keys. Mirroring the dashboard's owner-only project deletion, this endpoint returns a `403` unless the user who created the master key is currently an **owner** of the organization. ```bash curl -X DELETE https://internal.passingright.io/v1/master/projects/proj_... \ -H "Authorization: Bearer $MASTER_KEY" ``` Response (200): ```json { "message": "Project deleted successfully" } ``` ### List gateway API keys [#list-gateway-api-keys] `GET /v1/master/keys` Returns the developer-created gateway API keys in the master key's organization, each with its creator, project, configured limits, the usage consumed so far, and — when a windowed limit is set — the time the current period resets. `projectName` provides the human-readable project name alongside `projectId`. `createdBy` contains the creator's internal user ID, while `createdByEmail` contains their email address (`null` when unavailable). Pass an optional `projectId` query parameter to scope the list to a single project. ```bash curl "https://internal.passingright.io/v1/master/keys?projectId=proj_..." \ -H "Authorization: Bearer $MASTER_KEY" ``` Response (200): ```json { "apiKeys": [ { "id": "ak_...", "description": "Customer ACME — production key", "status": "active", "projectId": "proj_...", "projectName": "Customer ACME", "createdBy": "usr_...", "createdByEmail": "member@example.com", "maskedToken": "llmgtwy_...abcd", "usageLimit": "100.00", "usage": "42.13", "periodUsageLimit": "10.00", "periodUsageDurationValue": 1, "periodUsageDurationUnit": "day", "currentPeriodUsage": "3.50", "currentPeriodStartedAt": "2025-01-15T00:00:00.000Z", "currentPeriodResetAt": "2025-01-16T00:00:00.000Z", "createdAt": "...", "updatedAt": "..." } ] } ``` Limit and usage fields: | Field | Description | | -------------------------- | ---------------------------------------------------------------------------------- | | `usageLimit` | Lifetime spend cap (`null` when uncapped) | | `usage` | Total spend accrued against `usageLimit` over the key's lifetime | | `periodUsageLimit` | Recurring per-window spend cap (`null` when no windowed limit is configured) | | `periodUsageDurationValue` | Length of the window, paired with `periodUsageDurationUnit` | | `periodUsageDurationUnit` | `"hour" \| "day" \| "week" \| "month"` | | `currentPeriodUsage` | Spend accrued in the current window (`"0"` when unconfigured or the window lapsed) | | `currentPeriodStartedAt` | When the current window began (`null` when unconfigured or lapsed) | | `currentPeriodResetAt` | When the windowed limit resets (`null` when unconfigured or lapsed) | The plain token is never returned by this endpoint — only a masked form for identification. ### Get a gateway API key [#get-a-gateway-api-key] `GET /v1/master/keys/{id}` Returns a single gateway API key in the master key's organization, with the same limit, usage, and reset-time fields as the list endpoint. ```bash curl https://internal.passingright.io/v1/master/keys/ak_... \ -H "Authorization: Bearer $MASTER_KEY" ``` Response (200): ```json { "apiKey": { "id": "ak_...", "description": "Customer ACME — production key", "status": "active", "projectId": "proj_...", "projectName": "Customer ACME", "createdBy": "usr_...", "createdByEmail": "member@example.com", "maskedToken": "llmgtwy_...abcd", "usageLimit": "100.00", "usage": "42.13", "periodUsageLimit": "10.00", "periodUsageDurationValue": 1, "periodUsageDurationUnit": "day", "currentPeriodUsage": "3.50", "currentPeriodStartedAt": "2025-01-15T00:00:00.000Z", "currentPeriodResetAt": "2025-01-16T00:00:00.000Z", "createdAt": "...", "updatedAt": "..." } } ``` ### Create a gateway API key [#create-a-gateway-api-key] `POST /v1/master/keys` ```bash curl -X POST https://internal.passingright.io/v1/master/keys \ -H "Authorization: Bearer $MASTER_KEY" \ -H "Content-Type: application/json" \ -d '{ "projectId": "proj_...", "description": "Customer ACME — production key" }' ``` Body parameters: | Field | Type | Description | | -------------------------- | ------------------------------------------------- | -------------------------------------------- | | `projectId` | string | Must belong to the master key's organization | | `description` | string | API key description (1–255 chars) | | `usageLimit` | string (optional) | Lifetime usage limit | | `periodUsageLimit` | string (optional) | Recurring period usage limit | | `periodUsageDurationValue` | number (optional) | Required if `periodUsageLimit` is set | | `periodUsageDurationUnit` | `"hour" \| "day" \| "week" \| "month"` (optional) | Required if `periodUsageLimit` is set | The created gateway API key's plain token is returned in the response **only once**. Persist it immediately on your side. Response (201): ```json { "apiKey": { "id": "ak_...", "token": "llmgtwy_...", "description": "Customer ACME — production key", "status": "active", "projectId": "proj_...", "createdBy": "usr_...", "createdAt": "...", "updatedAt": "..." } } ``` ### Update a gateway API key [#update-a-gateway-api-key] `PATCH /v1/master/keys/{id}` Updates an API key in a project owned by the master key's organization. All body fields are optional; provide only the ones you want to change. ```bash curl -X PATCH https://internal.passingright.io/v1/master/keys/ak_... \ -H "Authorization: Bearer $MASTER_KEY" \ -H "Content-Type: application/json" \ -d '{ "status": "inactive", "usageLimit": "100.00" }' ``` Body parameters (all optional, at least one required): | Field | Type | Description | | -------------------------- | -------------------------------------- | -------------------------------------- | | `description` | string | 1–255 chars | | `status` | `"active" \| "inactive"` | | | `usageLimit` | string \| null | Lifetime usage limit (null to clear) | | `periodUsageLimit` | string \| null | Recurring period limit (null to clear) | | `periodUsageDurationValue` | number \| null | Required if `periodUsageLimit` is set | | `periodUsageDurationUnit` | `"hour" \| "day" \| "week" \| "month"` | Required if `periodUsageLimit` is set | Response (200): the updated API key, including its configured limits, consumed `usage` / `currentPeriodUsage`, and the `currentPeriodResetAt` window-reset time (same fields as the [list endpoint](#list-gateway-api-keys)). The plain token is **not** included — it is only returned at creation. Two dashboard-only key features have no master API equivalent yet: [expiration (TTL)](https://docs.passingright-staging.sandbloc.com/features/api-keys#expiration-ttl) and [rolling a key's secret](https://docs.passingright-staging.sandbloc.com/features/api-keys#rotating-rolling-api-keys). To replace a key programmatically, create a new one and delete the old one once your clients have switched over. ### Delete a gateway API key [#delete-a-gateway-api-key] `DELETE /v1/master/keys/{id}` Soft-deletes the API key (sets `status` to `"deleted"`). Any in-flight requests using the key will be rejected immediately on next auth check. ```bash curl -X DELETE https://internal.passingright.io/v1/master/keys/ak_... \ -H "Authorization: Bearer $MASTER_KEY" ``` Response (200): ```json { "message": "API key deleted successfully" } ``` The managed Playground API key cannot be updated or deleted via the master API. ## Usage and cost reporting [#usage-and-cost-reporting] `GET /v1/master/usage` Returns usage and cost for the master key's organization as a flat list of rows, grouped by any combination of member, model, provider, project, and API key, and bucketed hourly, daily, or not at all. This is the endpoint to point an internal reporting tool, data warehouse, or chargeback job at — it is the only usage surface that authenticates with a token rather than a dashboard session. Like every `/v1/master/*` endpoint, this requires an **Enterprise** plan. A master key on any other plan receives a `403`. ```bash # Cost per member for a month curl -G https://internal.passingright.io/v1/master/usage \ -H "Authorization: Bearer $MASTER_KEY" \ -d from=2026-07-01 -d to=2026-07-31 \ -d granularity=total -d groupBy=user ``` Query parameters: | Param | Type | Default | Description | | ------------- | ----------------------------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------- | | `from`, `to` | `YYYY-MM-DD` | last 7 days | Inclusive window, interpreted as wall-clock days in `timezone`. Max 366 days | | `timezone` | IANA timezone | `UTC` | Timezone the day/hour buckets are labelled in | | `granularity` | `hour` \| `day` \| `total` | `day` | Time bucket. `total` collapses the window into one row per group. `hour` caps at 31 days | | `groupBy` | comma-separated: `user`, `model`, `provider`, `project`, `apiKey` | `user,model` | Dimensions to break down by. Pass an empty value for organization-wide totals | | `projectId` | string | — | Restrict to one project. `404` if it is not in this organization | | `userId` | string | — | Restrict to one member | | `apiKeyId` | string | — | Restrict to one gateway API key | | `limit` | 1–10000 | `1000` | Rows per page | | `offset` | ≥ 0 | `0` | Row offset for paging | | `format` | `json` \| `csv` | `json` | `csv` returns a `text/csv` attachment with a fixed header | Response (200): ```json { "from": "2026-07-01", "to": "2026-07-31", "granularity": "day", "groupBy": ["user", "model"], "rows": [ { "date": "2026-07-01", "userId": "usr_...", "userName": "Ada Lovelace", "userEmail": "ada@example.com", "projectId": null, "projectName": null, "apiKeyId": null, "apiKeyName": null, "model": "gpt-5.6", "provider": "openai", "requestCount": 128, "errorCount": 2, "inputTokens": 481203, "outputTokens": 92844, "totalTokens": 574047, "cachedTokens": 120000, "reasoningTokens": 18320, "cost": 3.4127, "inputCost": 2.1, "outputCost": 1.3127, "creditsRequestCount": 128, "apiKeysRequestCount": 0, "creditsCost": 3.4127, "apiKeysCost": 0 } ], "pagination": { "limit": 1000, "offset": 0, "hasMore": false } } ``` Every row carries the full column set. Dimension fields you did not group by are `null`, and `date` is `null` when `granularity=total`, so the row shape stays stable for a schema-driven consumer. `creditsCost` / `apiKeysCost` split the blended `cost` into credit-billed and BYOK traffic. ### Per-member, per-model breakdown [#per-member-per-model-breakdown] Combine the two dimensions to get the cross-tab most reporting tools want — one row per member, model, and day: ```bash curl -G https://internal.passingright.io/v1/master/usage \ -H "Authorization: Bearer $MASTER_KEY" \ -d from=2026-07-01 -d to=2026-07-31 \ -d granularity=day -d groupBy=user,model \ -d timezone=Europe/Berlin ``` ### CSV export [#csv-export] ```bash curl -G https://internal.passingright.io/v1/master/usage \ -H "Authorization: Bearer $MASTER_KEY" \ -d from=2026-07-01 -d to=2026-07-31 \ -d granularity=day -d groupBy=user,model -d format=csv \ -o usage-july.csv ``` The CSV header is the same fixed column set regardless of the dimensions requested, so a scheduled export never changes shape. **Usage is attributed to the member who created the API key** (there is no per-caller identity on an inference request). A key shared by several people reports entirely under its creator. For accurate per-person reporting, issue one gateway API key per member — the per-member key limit under **Organization → Members** is the lever that enforces it. Two more things worth knowing: * These figures come from hourly rollups, not the request log, so they are **not** affected by your [data retention](https://docs.passingright-staging.sandbloc.com/features/data-retention) setting — per-member and per-model history stays available even with retention turned off. * Traffic from [embeddable payments](https://docs.passingright-staging.sandbloc.com/features/embeddable-payments) end-user keys rolls up to the member who provisioned the platform key. ## IAM rules [#iam-rules] Each gateway API key can have one or more IAM rules that restrict which models, providers, or pricing tiers it is allowed to use. Rules are evaluated at request time by the gateway. A key with no active rules has no IAM restrictions. Rule types: | `ruleType` | Description | | ----------------- | ----------------------------------------------------------- | | `allow_models` | Only the listed models are permitted | | `deny_models` | The listed models are blocked | | `allow_providers` | Only the listed providers are permitted | | `deny_providers` | The listed providers are blocked | | `allow_pricing` | Only models matching the pricing constraint are permitted | | `deny_pricing` | Models matching the pricing constraint are blocked | | `allow_ip_cidrs` | Only requests from the listed IPv4/IPv6 CIDRs are permitted | | `deny_ip_cidrs` | Requests from the listed IPv4/IPv6 CIDRs are blocked | The `ruleValue` JSON object holds the rule's parameters. The fields it accepts depend on the `ruleType`: | Field | Type | Used by | | ---------------- | ------------------ | ----------------------------------- | | `models` | string\[] | `allow_models`, `deny_models` | | `providers` | string\[] | `allow_providers`, `deny_providers` | | `pricingType` | `"free" \| "paid"` | `allow_pricing`, `deny_pricing` | | `maxInputPrice` | number | `allow_pricing`, `deny_pricing` | | `maxOutputPrice` | number | `allow_pricing`, `deny_pricing` | | `ipCidrs` | string\[] | `allow_ip_cidrs`, `deny_ip_cidrs` | ### IP CIDR rules [#ip-cidr-rules] IP CIDR rules restrict gateway requests by source IP. Both IPv4 (e.g. `192.0.2.0/24`) and IPv6 (e.g. `2001:db8::/32`) ranges are supported, and you can mix both in a single rule. To restrict to a single address, use a `/32` (IPv4) or `/128` (IPv6) prefix. The gateway reads the client IP from the first entry in the `X-Forwarded-For` header, which is set by the GCP load balancer. IPv4-mapped IPv6 addresses (`::ffff:1.2.3.4`) are normalized to IPv4 so a single `1.2.3.0/24` rule still matches when the upstream connection happens to be IPv6. When an `allow_ip_cidrs` rule is configured and the gateway cannot determine the client IP, the request is denied. Invalid CIDR syntax is rejected at rule-creation time with a `400` error. All endpoints scope by the master key's organization: a `404` is returned if the API key (or rule) is not part of the authenticated master key's organization. ### List IAM rules [#list-iam-rules] `GET /v1/master/keys/{id}/iam` ```bash curl https://internal.passingright.io/v1/master/keys/ak_.../iam \ -H "Authorization: Bearer $MASTER_KEY" ``` Response (200): ```json { "rules": [ { "id": "iam_...", "apiKeyId": "ak_...", "ruleType": "allow_models", "ruleValue": { "models": ["openai/gpt-4o", "anthropic/claude-3-5-sonnet"] }, "status": "active", "createdAt": "...", "updatedAt": "..." } ] } ``` ### Create an IAM rule [#create-an-iam-rule] `POST /v1/master/keys/{id}/iam` ```bash curl -X POST https://internal.passingright.io/v1/master/keys/ak_.../iam \ -H "Authorization: Bearer $MASTER_KEY" \ -H "Content-Type: application/json" \ -d '{ "ruleType": "allow_models", "ruleValue": { "models": ["openai/gpt-4o", "anthropic/claude-3-5-sonnet"] } }' ``` Body parameters: | Field | Type | Description | | ----------- | ------------------------ | ------------------------------------------------------- | | `ruleType` | rule type enum (above) | Required | | `ruleValue` | object (see table above) | Must include the fields appropriate for the chosen type | | `status` | `"active" \| "inactive"` | Optional, defaults to `"active"` | Restricting by source IP: ```bash curl -X POST https://internal.passingright.io/v1/master/keys/ak_.../iam \ -H "Authorization: Bearer $MASTER_KEY" \ -H "Content-Type: application/json" \ -d '{ "ruleType": "allow_ip_cidrs", "ruleValue": { "ipCidrs": ["192.0.2.0/24", "2001:db8::/32"] } }' ``` Response (201): the created IAM rule. ### Update an IAM rule [#update-an-iam-rule] `PATCH /v1/master/keys/{id}/iam/{ruleId}` All body fields are optional; provide only the ones you want to change. ```bash curl -X PATCH https://internal.passingright.io/v1/master/keys/ak_.../iam/iam_... \ -H "Authorization: Bearer $MASTER_KEY" \ -H "Content-Type: application/json" \ -d '{ "status": "inactive" }' ``` Body parameters (all optional, at least one required): | Field | Type | Description | | ----------- | ------------------------ | --------------------------------------- | | `ruleType` | rule type enum (above) | Change the rule type | | `ruleValue` | object (see table above) | Replace the rule value | | `status` | `"active" \| "inactive"` | Activate or deactivate without deleting | Response (200): the updated IAM rule. ### Delete an IAM rule [#delete-an-iam-rule] `DELETE /v1/master/keys/{id}/iam/{ruleId}` Permanently removes an IAM rule from the API key. ```bash curl -X DELETE https://internal.passingright.io/v1/master/keys/ak_.../iam/iam_... \ -H "Authorization: Bearer $MASTER_KEY" ``` Response (200): ```json { "message": "IAM rule deleted successfully" } ``` ## Member IAM rules [#member-iam-rules] The same rule types can be applied to an **organization member** instead of a single API key. Member-level rules are an organization-wide ceiling: a request must pass both the member's rules and the key's rules, so key rules can only narrow access further, never expand it. They apply to all regular API keys created by that member. See [Member-Level IAM Rules](https://docs.passingright-staging.sandbloc.com/features/api-keys#member-level-iam-rules) for the full semantics. The `{member}` path parameter accepts either the **membership id** or the member's **email address** (matched case-insensitively). Email references must resolve to a user who is a member of the master key's organization — an email belonging to a user outside the organization, or an unknown reference, returns a `404`. The `ruleType`, `ruleValue`, and `status` fields are identical to the per-key IAM endpoints above. ### List member IAM rules [#list-member-iam-rules] `GET /v1/master/members/{member}/iam` ```bash curl https://internal.passingright.io/v1/master/members/jane@example.com/iam \ -H "Authorization: Bearer $MASTER_KEY" ``` Response (200): ```json { "rules": [ { "id": "iam_...", "userOrganizationId": "uo_...", "ruleType": "allow_providers", "ruleValue": { "providers": ["openai"] }, "status": "active", "createdAt": "...", "updatedAt": "..." } ] } ``` ### Create a member IAM rule [#create-a-member-iam-rule] `POST /v1/master/members/{member}/iam` ```bash curl -X POST https://internal.passingright.io/v1/master/members/jane@example.com/iam \ -H "Authorization: Bearer $MASTER_KEY" \ -H "Content-Type: application/json" \ -d '{ "ruleType": "allow_providers", "ruleValue": { "providers": ["openai"] } }' ``` Response (201): the created member IAM rule. ### Update a member IAM rule [#update-a-member-iam-rule] `PATCH /v1/master/members/{member}/iam/{ruleId}` All body fields are optional; provide only the ones you want to change. ```bash curl -X PATCH https://internal.passingright.io/v1/master/members/uo_.../iam/iam_... \ -H "Authorization: Bearer $MASTER_KEY" \ -H "Content-Type: application/json" \ -d '{ "status": "inactive" }' ``` Response (200): the updated member IAM rule. ### Delete a member IAM rule [#delete-a-member-iam-rule] `DELETE /v1/master/members/{member}/iam/{ruleId}` ```bash curl -X DELETE https://internal.passingright.io/v1/master/members/uo_.../iam/iam_... \ -H "Authorization: Bearer $MASTER_KEY" ``` Response (200): ```json { "message": "Member IAM rule deleted successfully" } ``` ## Custom providers [#custom-providers] Custom providers are your own OpenAI-compatible endpoints, registered as BYOK provider keys with `provider: "custom"`. The gateway routes to them with the model string `/` (e.g. `acme/acme-large`). See [Custom Providers](https://docs.passingright-staging.sandbloc.com/features/custom-providers) for the dashboard flow and routing semantics. Only custom providers are exposed through the master API — catalog providers (OpenAI, Anthropic, …) require an interactive upstream credential check and must be added from the dashboard. All endpoints scope by the master key's organization: a `404` is returned if the provider key is not a non-deleted custom provider in that organization. The provider token is stored but never returned. Responses only include a `maskedToken` for identification. ### List custom providers [#list-custom-providers] `GET /v1/master/custom-providers` ```bash curl https://internal.passingright.io/v1/master/custom-providers \ -H "Authorization: Bearer $MASTER_KEY" ``` Response (200): ```json { "customProviders": [ { "id": "pk_...", "provider": "custom", "name": "acme", "baseUrl": "https://llm.acme.example.com", "maskedToken": "sk-a...cret", "status": "active", "customModelsOnly": true, "complianceAttestation": null, "organizationId": "org_...", "createdAt": "...", "updatedAt": "..." } ] } ``` ### Get a custom provider [#get-a-custom-provider] `GET /v1/master/custom-providers/{id}` Returns a single custom provider in the master key's organization. ### Create a custom provider [#create-a-custom-provider] `POST /v1/master/custom-providers` ```bash curl -X POST https://internal.passingright.io/v1/master/custom-providers \ -H "Authorization: Bearer $MASTER_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "acme", "baseUrl": "https://llm.acme.example.com", "token": "sk-acme-...", "customModelsOnly": true }' ``` Body parameters: | Field | Type | Description | | ----------------------- | ------------------ | -------------------------------------------------------------------------------------------- | | `name` | string | Lowercase letters and single hyphens only. Unique per organization; used in the model string | | `baseUrl` | string | OpenAI-compatible base URL. Internal/reserved addresses are rejected | | `token` | string | Upstream API key. Stored server-side, never returned | | `customModelsOnly` | boolean (optional) | Restrict the provider to models defined in your custom catalog. Default `false` | | `complianceAttestation` | object (optional) | Self-attested compliance posture. `attestedAt` / `attestedByUserId` are stamped server-side | Response (201): the created custom provider. ### Update a custom provider [#update-a-custom-provider] `PATCH /v1/master/custom-providers/{id}` All body fields are optional; provide only the ones you want to change. ```bash curl -X PATCH https://internal.passingright.io/v1/master/custom-providers/pk_... \ -H "Authorization: Bearer $MASTER_KEY" \ -H "Content-Type: application/json" \ -d '{ "baseUrl": "https://llm2.acme.example.com", "token": "sk-acme-rotated-..." }' ``` Body parameters (all optional, at least one required): | Field | Type | Description | | ----------------------- | ------------------------ | ---------------------------------------------- | | `baseUrl` | string | Repoint the provider at a new endpoint | | `token` | string | Rotate the upstream API key | | `status` | `"active" \| "inactive"` | Disable the provider without deleting it | | `customModelsOnly` | boolean | Toggle the custom-catalog restriction | | `complianceAttestation` | object \| null | Replace the attestation, or `null` to clear it | The provider `name` is immutable — it is the routing segment your model strings reference. Create a new provider instead of renaming. Response (200): the updated custom provider. ### Delete a custom provider [#delete-a-custom-provider] `DELETE /v1/master/custom-providers/{id}` Soft-deletes the provider (sets `status` to `"deleted"`). Its custom models stop resolving immediately. Response (200): ```json { "message": "Custom provider deleted successfully" } ``` ## Custom models [#custom-models] Custom models are your organization's catalogue entries for a custom provider: the context window, output limit, per-token pricing, and capability flags the gateway uses to route, validate, and bill requests to that model. When a provider has `customModelsOnly: true`, only models in this catalogue can be called through it. Query these endpoints to read the full catalogue you have registered — including `contextSize`, `maxOutput`, and every price field — for surfacing in your own dashboards or cost tooling. Per-token prices are strings in USD per token. Use `e-6` notation so the coefficient reads directly as USD per million tokens — `"3.0e-6"` is $3.00/M. ### List custom models [#list-custom-models] `GET /v1/master/custom-models` Returns the non-deleted custom models in the master key's organization. Pass an optional `providerKeyId` query parameter to scope the list to a single custom provider. ```bash curl "https://internal.passingright.io/v1/master/custom-models?providerKeyId=pk_..." \ -H "Authorization: Bearer $MASTER_KEY" ``` Response (200): ```json { "customModels": [ { "id": "cm_...", "providerKeyId": "pk_...", "organizationId": "org_...", "modelName": "acme-large", "displayName": "Acme Large", "contextSize": 200000, "maxOutput": 32000, "inputPrice": "3.0e-6", "outputPrice": "15.0e-6", "cachedInputPrice": "0.3e-6", "streaming": "true", "vision": true, "tools": true, "reasoning": null, "jsonOutput": true, "supportedParameters": ["temperature", "top_p"], "status": "active", "createdAt": "...", "updatedAt": "..." } ] } ``` ### Get a custom model [#get-a-custom-model] `GET /v1/master/custom-models/{id}` Returns a single custom model in the master key's organization. ### Create a custom model [#create-a-custom-model] `POST /v1/master/custom-models` ```bash curl -X POST https://internal.passingright.io/v1/master/custom-models \ -H "Authorization: Bearer $MASTER_KEY" \ -H "Content-Type: application/json" \ -d '{ "providerKeyId": "pk_...", "modelName": "acme-large", "displayName": "Acme Large", "contextSize": 200000, "maxOutput": 32000, "inputPrice": "3.0e-6", "outputPrice": "15.0e-6", "tools": true }' ``` Body parameters: | Field | Type | Description | | ------------------------ | ----------------------------------- | ---------------------------------------------------------- | | `providerKeyId` | string | Must be a custom provider in the master key's organization | | `modelName` | string | Model id sent upstream. Unique per provider | | `displayName` | string (optional) | Human-readable label | | `contextSize` | number (optional) | Context window in tokens | | `maxOutput` | number (optional) | Max output tokens | | `inputPrice` | string (optional) | USD per input token | | `outputPrice` | string (optional) | USD per output token | | `cachedInputPrice` | string (optional) | USD per cached input token | | `cacheReadInputPrice` | string (optional) | USD per cache-read token | | `cacheWriteInputPrice` | string (optional) | USD per cache-write token (5m TTL) | | `cacheWriteInputPrice1h` | string (optional) | USD per cache-write token (1h TTL) | | `requestPrice` | string (optional) | Flat USD charged per request | | `webSearchPrice` | string (optional) | USD per web search | | `imageInputPrice` | string (optional) | USD per image input token | | `audioInputPrice` | string (optional) | USD per audio input token | | `streaming` | `"true" \| "false" \| "only"` | Streaming support (`"only"` = streaming-only model) | | `vision` | boolean (optional) | Accepts image input | | `tools` | boolean (optional) | Supports tool calling | | `reasoning` | boolean (optional) | Emits reasoning output | | `jsonOutput` | boolean (optional) | Supports JSON / structured output | | `audio` | boolean (optional) | Accepts audio input | | `supportedParameters` | string\[] (optional) | Request parameters the upstream accepts | | `status` | `"active" \| "inactive"` (optional) | Defaults to `"active"` | Response (201): the created custom model. ### Update a custom model [#update-a-custom-model] `PATCH /v1/master/custom-models/{id}` Accepts the same fields as create (except `providerKeyId`), all optional — provide only the ones you want to change. Renaming to a `modelName` already used by another model on the same provider returns a `400`. ```bash curl -X PATCH https://internal.passingright.io/v1/master/custom-models/cm_... \ -H "Authorization: Bearer $MASTER_KEY" \ -H "Content-Type: application/json" \ -d '{ "contextSize": 400000, "outputPrice": "12.0e-6" }' ``` Response (200): the updated custom model. ### Delete a custom model [#delete-a-custom-model] `DELETE /v1/master/custom-models/{id}` Soft-deletes the model (sets `status` to `"deleted"`). Response (200): ```json { "message": "Custom model deleted successfully" } ``` # Metadata URL: https://docs.passingright-staging.sandbloc.com/features/metadata PassingRight supports sending additional metadata with your requests using custom headers. This allows you to include information like user sessions, application versions, tenant IDs, or other contextual data that can be useful for analytics and monitoring. Later, you can filter by specific values to return, such as for a specific user or session. Additionally, in the future, you will be able to segment your analytics and monitoring based on this metadata. For example, you could show cost and latency breakdowns per user, application, country, feature, or any other dimension you want to track. ## Custom Headers [#custom-headers] You can include custom headers with the `X-PassingRight-` prefix to send metadata alongside your LLM requests: ```bash curl -X POST https://api.passingright.io/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "X-PassingRight-Country: US" \ -H "X-PassingRight-User-ID: 9403f741-a524-4b18-b1b2-dbb71cdff2a4" \ -d '{ "model": "gpt-4o", "messages": [ { "role": "user", "content": "Hello, how are you?" } ] }' ``` ## Best Practices [#best-practices] ### Header Naming [#header-naming] * Use the `X-PassingRight-` prefix for all custom metadata * Use descriptive, consistent naming conventions * Avoid special characters; use hyphens to separate words ### Data Privacy [#data-privacy] * Be mindful of sensitive data in headers * Consider hashing or anonymizing user identifiers * Follow your organization's data privacy policies ### Performance [#performance] * Keep header values reasonably short * Avoid sending unnecessary metadata that won't be used for analytics * Consider the impact on request size, especially for high-volume applications ## Example: Multi-tenant Application [#example-multi-tenant-application] For a multi-tenant application, you might use metadata headers like this: ```bash curl -X POST https://api.passingright.io/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "X-PassingRight-Tenant-ID: acme-corp" \ -H "X-PassingRight-User-ID: user-12345" \ -H "X-PassingRight-App-Version: 2.1.4" \ -H "X-PassingRight-Feature: chat-assistant" \ -d '{ "model": "gpt-4o", "messages": [ { "role": "user", "content": "Summarize this document..." } ] }' ``` This allows you to track usage and costs per tenant, user, application version, and feature, providing detailed insights into how your LLM integration is being used across your platform. # Org Models Directory URL: https://docs.passingright-staging.sandbloc.com/features/models-directory The **Models** page in the dashboard (`Organization → Models`) lists every model your organization can route to in a single directory: the full PassingRight catalogue plus the models defined for your own [custom providers](https://docs.passingright-staging.sandbloc.com/features/custom-providers). It answers two questions for your whole team: * **What can we call?** — every catalogue and custom model, searchable and filterable by capability, provider, price, and context size, exactly like the public [models page](https://passingright.io/models). * **What are we allowed to call?** — when a [provider compliance policy](https://docs.passingright-staging.sandbloc.com/features/compliance) is active, each provider mapping is marked eligible or blocked using the same fail-closed evaluation the gateway applies at request time. ## Who can see it [#who-can-see-it] Every active organization member can browse the directory read-only — including project-scoped **developer** members, who see it as their only organization page. This is the recommended way for developers to discover available providers and models without being granted additional permissions. Management actions (custom-model catalog, attestations) remain restricted to organization owners and admins. ## Custom models in the directory [#custom-models-in-the-directory] Custom models appear alongside catalogue models, addressed as `{customProvider}/{modelName}` — the exact model string to send to the gateway: ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "internal-vllm/llama-4-maverick", "messages": [{ "role": "user", "content": "Hello!" }] }' ``` Their pricing, context and output limits, and capability flags come from the [custom model catalog](https://docs.passingright-staging.sandbloc.com/features/custom-providers#custom-model-catalog). A **Source** filter switches between catalogue models, custom models, or both. ## Compliance eligibility [#compliance-eligibility] With an enabled compliance policy (Enterprise), the directory evaluates every provider mapping against the policy — certifications, data-policy requirements, country restrictions, and provider/model allow and deny lists: * Blocked mappings render greyed out with a ban icon; its tooltip lists the exact failing requirements (for example "No SOC 2 Type 2 report" or "Not on the allowed-providers list"). * Custom providers are evaluated against their [compliance attestation](https://docs.passingright-staging.sandbloc.com/features/compliance#custom-providers); without one on file they are blocked while a policy is active. * An **Eligible only** filter narrows the directory to models the gateway will actually route to. The directory mirrors gateway enforcement — the same predicates decide both the markers on this page and the 403 responses at request time. If a model is marked blocked here, requests for it will be rejected while the policy is active. ## Discover models with an API key [#discover-models-with-an-api-key] Call `GET /v1/models` with your API key to list models available under your organization's compliance policy, inherited team and member IAM rules, and the key's own IAM rules. Blocked provider mappings are omitted; models with no remaining mappings are omitted too. Pricing and capabilities reflect the remaining mappings. Accessible custom models use `{customProvider}/{modelName}` IDs. ```bash curl "https://api.passingright.io/v1/models" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" ``` The endpoint also accepts the `x-api-key` header. Without either authentication header, it returns the public catalogue. Invalid, inactive, or expired credentials return `401`. Set `?include_restricted=true` to return the full public catalogue even with an authenticated API key. This skips organization, team, member, key, project, and plan filtering for discovery. Private custom models are omitted. Other query filters still apply, and inference requests still enforce all access restrictions. The default is `false`, which keeps authenticated results filtered. Use `?mapped=true` for provider-prefixed entries. Existing query filters, including `no_training`, apply alongside access restrictions. Discovery reflects model permissions and project configuration; it does not check remaining credits, usage budgets, or current provider health. ## Related [#related] * [Knowledge base → Models](https://docs.passingright-staging.sandbloc.com/learn/models) — a walkthrough of the page itself. * [Custom Providers](https://docs.passingright-staging.sandbloc.com/features/custom-providers) — bring your own OpenAI-compatible endpoints and define their model catalog. * [Compliance](https://docs.passingright-staging.sandbloc.com/features/compliance) — configure the provider compliance policy. # Moderations URL: https://docs.passingright-staging.sandbloc.com/features/moderations PassingRight supports the OpenAI-compatible `/v1/moderations` endpoint for text and multimodal safety classification. Use it when you want to: * Screen user prompts before they reach a model * Review generated output before displaying it * Apply the same moderation API shape you already use with OpenAI clients For the full request and response schema, see the [API reference](https://docs.passingright-staging.sandbloc.com/v1_moderations). ## Endpoint [#endpoint] `POST https://api.passingright.io/v1/moderations` Authenticate with your PassingRight API key: ```bash -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" ``` ## Supported Inputs [#supported-inputs] The `input` field accepts: * A single string * An array of strings * An array of multimodal content items with `text` and `image_url` The default model is `omni-moderation-latest`. ## Pricing [#pricing] Starting **Friday, August 7, 2026**, moderation requests are billed at a flat **$0.00001 per request**, independent of the input size, the number of inputs, or the moderation model you pick. Only successful requests are billed — failed and retried attempts cost nothing. Before that date, moderation requests are free. Because the endpoint becomes paid, it also starts requiring a credit balance from that date: moderation requests from an organization with no credits are rejected with `402`. Top up before August 7 to avoid an interruption. When your project runs in `api-keys` mode and the request is served with your own OpenAI key, no credits are deducted and no balance is required. ## curl [#curl] ### Single text input [#single-text-input] ```bash curl -X POST "https://api.passingright.io/v1/moderations" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "input": "I want to harm someone." }' ``` ### Multiple text inputs [#multiple-text-inputs] ```bash curl -X POST "https://api.passingright.io/v1/moderations" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "omni-moderation-latest", "input": [ "This is a harmless sentence.", "I want to attack somebody." ] }' ``` ### Multimodal input [#multimodal-input] ```bash curl -X POST "https://api.passingright.io/v1/moderations" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "input": [ { "type": "text", "text": "Check this image for violent content." }, { "type": "image_url", "image_url": { "url": "https://example.com/image.png" } } ] }' ``` ## OpenAI SDK [#openai-sdk] ```ts import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.passingright.io/v1", apiKey: process.env.LLM_GATEWAY_API_KEY, }); const response = await client.moderations.create({ model: "omni-moderation-latest", input: "I want to harm someone.", }); console.log(response.results[0]?.flagged); ``` ## Response Shape [#response-shape] The response follows the standard OpenAI moderation format: ```json { "id": "modr-123", "model": "omni-moderation-latest", "results": [ { "flagged": true, "categories": { "violence": true, "self_harm": false }, "category_scores": { "violence": 0.98, "self_harm": 0.01 } } ] } ``` ## When To Use This Instead Of Chat Content Filtering [#when-to-use-this-instead-of-chat-content-filtering] Use `/v1/moderations` when you want an explicit moderation decision in your own application flow. If you want moderation to happen automatically as part of model requests, use PassingRight content filtering on `/v1/chat/completions` instead. # Notifications URL: https://docs.passingright-staging.sandbloc.com/features/notifications Open the **Notifications** bell in the dashboard header, then select **Notification settings**. Choose **In-app**, **Email**, both, or neither for each alert type. Alerts start disabled, and email delivery requires a verified email address. Your preferences apply across projects you can access. | Alert | When it appears | | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | API key budgets | A regular API key reaches your selected percentage of its lifetime or recurring budget. The default threshold is 80%. | | Model retirements | A model mapping you used in the last 30 days is scheduled for deprecation or deactivation within 30 days. The alert links to the model catalogue so you can choose a newer model. | | Provider issues | A provider you used in the last 30 days has elevated upstream errors in its recent traffic. | Budget alerts repeat for each new recurring period, or when you change the limit or alert threshold. Unlimited keys do not trigger budget warnings. Each scheduled model retirement is announced once; ongoing provider issues produce at most one alert per day per project, or per accessible key for developers. Provider alerts require at least 20 non-cached, non-client-error requests and an upstream error rate of at least 20% in the latest hourly statistics window. Stale statistics do not trigger alerts. Checks run about once a minute; these warnings are not a guarantee that every outage or budget crossing will be caught before a request fails. Notifications follow your existing project permissions. Developers receive alerts only for their own keys in assigned projects. Removed project access also removes those alerts from the inbox and prevents pending email delivery. The inbox shows your 50 most recent in-app alerts. Open an alert to follow its action, or select **Mark all as read** to clear the unread indicator. Turn off a channel in notification settings to stop future delivery through it. # OCR URL: https://docs.passingright-staging.sandbloc.com/features/ocr PassingRight exposes a dedicated `/v1/ocr` endpoint for optical character recognition. It extracts text, tables, and layout from PDFs and images and returns them as clean markdown, one entry per page. Use it when you want to: * Turn scanned PDFs or photos into machine-readable markdown * Pull structured text out of receipts, invoices, forms, or screenshots * Feed document contents into a downstream model or RAG pipeline For the full request and response schema, see the [API reference](https://docs.passingright-staging.sandbloc.com/v1_ocr). ## Endpoint [#endpoint] `POST https://api.passingright.io/v1/ocr` Authenticate with your PassingRight API key: ```bash -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" ``` The current model is `mistral-ocr-latest`, billed at **$4 per 1,000 pages** processed. ## Document Input [#document-input] The `document` field accepts either a document URL (PDF) or an image: * `{ "type": "document_url", "document_url": "https://…/file.pdf" }` * `{ "type": "image_url", "image_url": "https://…/image.png" }` Both `document_url` and `image_url` accept a public URL or a base64 data URL (`data:application/pdf;base64,…` / `data:image/png;base64,…`). The `image_url` field may also be passed as an object: `{ "url": "…" }`. ### Scoping pages [#scoping-pages] By default the entire document is processed and every page is billed. Use the optional `pages` field to restrict (and cap the cost of) a request: * A list of zero-based indices: `"pages": [0, 1, 2]` * A range string: `"pages": "0-4"` ## curl [#curl] ### Document URL [#document-url] ```bash curl -X POST "https://api.passingright.io/v1/ocr" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "mistral-ocr-latest", "document": { "type": "document_url", "document_url": "https://arxiv.org/pdf/2201.04234" } }' ``` ### Only specific pages [#only-specific-pages] ```bash curl -X POST "https://api.passingright.io/v1/ocr" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "mistral-ocr-latest", "document": { "type": "document_url", "document_url": "https://arxiv.org/pdf/2201.04234" }, "pages": "0-4" }' ``` ### Image input [#image-input] ```bash curl -X POST "https://api.passingright.io/v1/ocr" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "mistral-ocr-latest", "document": { "type": "image_url", "image_url": "https://example.com/receipt.png" } }' ``` ### Inline (base64) document [#inline-base64-document] ```bash BASE64_PDF=$(base64 -i invoice.pdf) curl -X POST "https://api.passingright.io/v1/ocr" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d "{ \"model\": \"mistral-ocr-latest\", \"document\": { \"type\": \"document_url\", \"document_url\": \"data:application/pdf;base64,${BASE64_PDF}\" } }" ``` ## Response Shape [#response-shape] ```json { "pages": [ { "index": 0, "markdown": "# Document title\n\nExtracted body text…", "images": [], "dimensions": { "dpi": 200, "height": 2200, "width": 1700 } } ], "model": "mistral-ocr-latest", "document_annotation": null, "usage_info": { "pages_processed": 1, "doc_size_bytes": 125344 } } ``` Each entry in `pages` carries the markdown for one page. `usage_info.pages_processed` reflects exactly how many pages were billed for the request. ## Billing [#billing] OCR is billed per page processed, not per token. A request that processes 12 pages bills `12 × $0.004 = $0.048`. Scoping a request with `pages` reduces both the work and the cost. ## OCR Models Use This Endpoint, Not Chat [#ocr-models-use-this-endpoint-not-chat] OCR models are not chat models and cannot be called through `/v1/chat/completions` — doing so returns a `400` pointing you here. Send OCR requests to `/v1/ocr`. # Organization skills URL: https://docs.passingright-staging.sandbloc.com/features/organization-skills Organization skills are reusable instructions and supporting files that enterprise teams share through the CLI. Owners and admins publish skills in the dashboard; developers can discover and download them using a project API key from the same organization. ## Publish a skill [#publish-a-skill] 1. Open **Organization → Skills** in the dashboard. 2. Choose **Import SKILL.md** for a single skill, **Import folder** for a skill with references, scripts, or assets, or **New custom skill** to write instructions. 3. Review the content and save. New skills are enabled immediately. A skill folder must contain `SKILL.md` at its root. Its YAML header needs a unique lowercase, hyphenated `name` of up to 64 characters and a `description` of up to 1,024 characters. Write the instructions below the header: ```md --- name: code-review description: Review code changes against the team's standards. --- # Code review - Check correctness and error handling. - Explain the impact of each finding. - Suggest tests for important edge cases. ``` Bundles support up to 100 additional files and 1 MB of total content. `SKILL.md` supports up to 200,000 characters within that limit. Supporting paths must be relative to the skill folder; duplicate paths and parent-directory traversal are rejected. Existing YAML fields, such as `license` or `allowed-tools`, are preserved. ## Manage access [#manage-access] All active organization members can view skills. Owners and admins can edit, disable, or delete them. Keep a published skill's name unchanged; create a new skill to use a different name. Disabling a skill removes it from CLI discovery and prevents new downloads. Deleting it also removes its stored content. These actions cannot remove copies already downloaded to a developer's machine. Publishing and management actions appear in the organization's audit log without recording the skill content. ## CLI API contract [#cli-api-contract] These endpoints belong to the [platform API](https://internal.passingright.io/docs). Authenticate with `Authorization: Bearer `, using a regular project API key issued to an active member with access to that project. | Method | Path | Response | | ------ | ------------------- | ------------------------------------------------ | | GET | `/v1/skills` | `{ skills: [...] }` with enabled skill summaries | | GET | `/v1/skills/{name}` | `{ skill: { ...summary, content, files } }` | A summary contains `id`, `name`, `description`, `enabled`, `createdAt`, and `updatedAt`. Timestamps use ISO 8601. `content` is the complete `SKILL.md`, including its YAML header. Each supporting file contains a relative `path`, `content`, and optional `encoding` (`utf-8` by default, or `base64` for binary files). Clients should discover skills at the start of a session, download selected skills by name, and write each bundle into its own directory. Revalidate authorization before reusing organization content and remove cached availability when access is denied. Treat supporting scripts as code to review before running. The server derives the organization from the API key. Expired or revoked keys return `401`; removed project access or missing enterprise access returns `403`; missing or disabled skills return `404`. Responses use `Cache-Control: private, no-store`. Dashboard management uses session authentication at `/orgs/{organizationId}/skills`: `GET` lists summaries and `POST` creates a bundle. At `/orgs/{organizationId}/skills/{id}`, `GET` reads a bundle, `PUT` replaces `{ content, files }`, `PATCH` sets `{ enabled }`, and `DELETE` removes it. Duplicate names return `409`. # Project access URL: https://docs.passingright-staging.sandbloc.com/features/project-access Use **Project admin** to let someone manage specific projects without giving them organization administration. Owners and admins assign the role and its project grants from [Team → Members](https://docs.passingright-staging.sandbloc.com/learn/team). | Role | Scope | Permissions | | ------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | Owner | All organization projects | Organization administration, billing settings, membership, and project management | | Admin | All organization projects | Organization and project management; cannot change billing settings or modify owners | | Project admin | Assigned projects | Project settings, routing, dynamic routes, guardrails, Payments SDK settings and platform keys, all project API keys, and project-wide usage | | Developer | Assigned projects | Own API keys and own usage; cannot change project settings | Project admin and Developer assignments require Enterprise access and at least one project grant. Existing feature and preview requirements still apply. Project admins cannot create or archive projects, change organization settings or provider keys, manage membership, or access organization billing controls. Archiving projects and deleting organizations require an Owner. ## Assign project access [#assign-project-access] In **Team → Members**, choose **Add Member** or an existing member's **Manage access** action. Select **Project admin**, choose the allowed projects, and save. Invitations carry the selected grants through acceptance. Removing a grant removes access to that project's settings, keys, and usage; the gateway also checks the creator's project access when a key is used, subject to normal cache propagation. For an authenticated management API session, use `project_admin` with `projectIds` when adding a member: ```json { "email": "member@example.com", "role": "project_admin", "projectIds": ["your-project-id"] } ``` Send this body to `POST /team/{organizationId}/members`. Use `PATCH /team/{organizationId}/members/{memberId}` with `role` and the complete `projectIds` list to replace an existing member's access. ## Budgets, teams, and SSO [#budgets-teams-and-sso] Personal member budgets and IAM rules still apply to the member's keys. Organization teams and default developer budgets apply only to Developers; promoting a Developer clears their team assignment. [SSO group mappings](https://docs.passingright-staging.sandbloc.com/features/sso) can assign `project_admin`. Role priority is Owner, Admin, Project admin, then Developer. Project-scoped roles still require explicit project grants: a role mapping does not grant every project. Project guardrails can display the organization policy they inherit, while changes to that policy remain restricted to organization administrators. # Realtime API URL: https://docs.passingright-staging.sandbloc.com/features/realtime PassingRight supports low-latency, speech-to-speech conversations through the OpenAI-compatible **`/v1/realtime`** WebSocket endpoint. Sessions support text and audio input/output, server-side voice activity detection (VAD), input audio transcription, and function calling — using the same event protocol as the OpenAI Realtime API. The same endpoint also serves [transcription-only sessions](#transcription-sessions) for live speech-to-text. The API is a drop-in replacement: point any OpenAI realtime client at `wss://api.passingright.io/v1/realtime`, authenticate with your PassingRight API key, and keep your existing event handling. Want to talk to a model right now? The [Realtime page](https://chat.passingright.io/realtime) in Lounge runs a full voice call in the browser using this API. ## Available Models [#available-models] Browse the available realtime models, with up-to-date pricing, on the [models page](https://passingright.io/models). Realtime sessions are billed per token — text and audio input, cached input, and output are metered separately at the model's listed rates, matching the provider's own pricing. Model names work like everywhere else on PassingRight: use the plain model id (e.g. `gpt-realtime`), a dated alias, or the `provider/model` pinned form (e.g. `openai/gpt-realtime`). ## Connecting [#connecting] Connect a WebSocket to: ``` wss://api.passingright.io/v1/realtime?model=gpt-realtime ``` Authenticate with your PassingRight API key in the `Authorization` header (an `x-api-key` header also works): ```javascript import WebSocket from "ws"; const url = "wss://api.passingright.io/v1/realtime?model=gpt-realtime"; const ws = new WebSocket(url, { headers: { Authorization: "Bearer " + process.env.LLM_GATEWAY_API_KEY, }, }); ws.on("open", () => { console.log("Connected to server."); }); ws.on("message", (message) => { const event = JSON.parse(message.toString()); console.log(event.type); }); ``` Once connected, the session speaks the standard realtime event protocol: send client events like `session.update`, `conversation.item.create`, `input_audio_buffer.append`, and `response.create`; receive server events like `session.created`, `response.output_audio.delta`, and `response.done`. ```javascript ws.on("open", () => { // Configure the session. ws.send( JSON.stringify({ type: "session.update", session: { type: "realtime", instructions: "You are a friendly assistant.", audio: { output: { voice: "marin" }, }, }, }), ); // Ask for a response. ws.send( JSON.stringify({ type: "conversation.item.create", item: { type: "message", role: "user", content: [{ type: "input_text", text: "Say hello!" }], }, }), ); ws.send(JSON.stringify({ type: "response.create" })); }); ``` All events are JSON text frames; audio travels base64-encoded inside events (binary WebSocket frames are rejected). The model is locked at connection time — a `session.update` that tries to change `session.model` is rejected with a `model_locked` error event. Credentials are never accepted as query parameters. A connection URL containing `token`, `api_key`, or `client_secret` query parameters is rejected with HTTP 400. Use the `Authorization` header on servers, or an ephemeral client secret (below) in browsers. ## Browser Clients and Client Secrets [#browser-clients-and-client-secrets] Never ship a long-lived API key to a browser. Instead, mint a short-lived **ephemeral client secret** from your backend with the OpenAI-compatible `POST /v1/realtime/client_secrets` endpoint: ```bash curl -X POST "https://api.passingright.io/v1/realtime/client_secrets" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "expires_after": { "anchor": "created_at", "seconds": 120 }, "session": { "type": "realtime", "model": "gpt-realtime" } }' ``` The response contains the secret (`ek_...`) and its expiry: ```json { "value": "ek_...", "expires_at": 1753500000, "session": { "type": "realtime", "model": "gpt-realtime" } } ``` The browser then connects using the standard `openai-insecure-api-key` WebSocket subprotocol — no `Authorization` header needed: ```javascript const ws = new WebSocket("wss://api.passingright.io/v1/realtime", [ "realtime", "openai-insecure-api-key." + clientSecret, ]); ``` Client secret behavior: * **TTL**: 10–300 seconds (default 60), set via `expires_after.seconds`. Secrets are reusable until they expire; expiry only gates opening the connection, not the session's duration. * **Model pinning**: the secret is minted for one model. The `model` query parameter is optional when connecting with a secret; if provided, it must match the minted model. * **Transcription pinning**: optionally pin an input transcription model at mint time via `session.audio.input.transcription.model`. The session can then only enable that transcription model. * **Instruction pinning**: optionally pin the session prompt at mint time via `session.instructions` — see below. * **Voice**: optionally set the session's default output voice at mint time via `session.audio.output.voice`. Unlike instructions this is only a default; the client may still change it. * All authentication, credit, model access, and compliance checks run at mint time and again at connection time. ### Server-Authoritative Instructions [#server-authoritative-instructions] By default the session prompt is set by whoever holds the socket, so a browser client has to send `session.instructions` itself. If your server owns the prompt, pin it at mint time instead: ```json { "session": { "type": "realtime", "model": "gpt-realtime", "instructions": "You are a support agent for ACME." } } ``` The gateway then applies the instructions itself when the session connects, so the prompt never has to travel through the browser. It is not echoed in the mint response, and it is stripped from the `session.created` and `session.updated` events the client receives. Once pinned, the instructions are locked for the session's lifetime, the same way `session.model` is. A client that tries to change them gets an `instructions_locked` error event and the change is not applied — this covers both `session.update` and per-response `response.create.instructions`. Instructions larger than 16,384 estimated tokens are rejected at mint time with a 400 `instructions_too_large`, so an oversized prompt fails server-to-server rather than mid-call. On Gemini realtime models the same pin applies to `setup.systemInstruction`, and a client-supplied `systemInstruction` is rejected with the same `instructions_locked` code. ## Input Audio Transcription [#input-audio-transcription] Enable transcription of the user's audio with `session.update`. An explicit, supported transcription model is required — check the [models page](https://passingright.io/models) for available realtime transcription models and their pricing: ```json { "type": "session.update", "session": { "type": "realtime", "audio": { "input": { "transcription": { "model": "gpt-4o-transcribe" } } } } } ``` Transcripts arrive as standard `conversation.item.input_audio_transcription.delta` and `.completed` events. * The provider's implicit default (e.g. `whisper-1`) is not available; omitting `transcription.model` is rejected with `transcription_model_required`. * The transcription model is pinned for the session on first use and cannot be switched afterwards; disabling transcription (`"transcription": null`) is always allowed. * Both the current nested form (`session.audio.input.transcription`) and the legacy top-level form (`session.input_audio_transcription`) are accepted. * Transcription models are billed the way their provider meters them, per token or per minute of audio, at the rates on the models page. ## Transcription Sessions [#transcription-sessions] For live speech-to-text without a speech model in the loop — captions, agent assist, voice notes — open a **transcription session**. It uses the same WebSocket endpoint and event protocol, but the session's only model is the transcription model, so you pay for transcription alone. The model is pinned at connection time, so it goes in the URL alongside `intent=transcription`: ``` wss://api.passingright.io/v1/realtime?intent=transcription&model=gpt-live-transcribe ``` The gateway applies the transcription model itself once the session is created. Configure the rest with `session.update`, then stream audio with `input_audio_buffer.append`: ```json { "type": "session.update", "session": { "type": "transcription", "audio": { "input": { "format": { "type": "audio/pcm", "rate": 24000 }, "transcription": { "model": "gpt-live-transcribe", "delay": "low", "languages": ["en"] }, "turn_detection": null } } } } ``` Transcripts arrive as `conversation.item.input_audio_transcription.delta` and `.completed` events, exactly as in a realtime session. * Any realtime transcription model on the [models page](https://passingright.io/models) can open a transcription session; the page also shows whether a model is billed per token or per minute of audio. * The model is locked for the session: a `session.update` that names another transcription model is rejected with `transcription_model_locked`, and disabling transcription is rejected with `transcription_model_required`. Other transcription settings (`delay`, `keywords`, `languages`, `prompt`, `turn_detection`, `noise_reduction`) are passed through to the provider. Streaming transcription models segment audio continuously and reject `turn_detection`: send `null` and commit turns yourself with `input_audio_buffer.commit`. * `response.create` is rejected with `response_not_supported`; a transcription session never generates. * Browser clients mint a client secret with `session.type: "transcription"` and connect with the `openai-insecure-api-key` subprotocol as usual. The `intent` and `model` query parameters are optional with a secret, but if present they must match what the secret was minted for: ```json { "session": { "type": "transcription", "audio": { "input": { "transcription": { "model": "gpt-live-transcribe" } } } } } ``` Session gating, limits and billing work as for realtime sessions: each completed transcription is billed before its transcript is forwarded, and a session that no longer clears the credit, spend or rate-limit checks is closed after the current turn. Streaming models transcribe audio before it is committed; audio that has already produced transcript deltas is committed by the gateway on disconnect and ahead of an `input_audio_buffer.clear`, so it is billed like any other turn. ## Function Calling [#function-calling] Plain function tools work exactly as in the OpenAI Realtime API — declare them in `session.update` (or per response in `response.create`), receive `response.function_call_arguments` events, and return results with `conversation.item.create`: ```json { "type": "session.update", "session": { "type": "realtime", "tools": [ { "type": "function", "name": "get_weather", "description": "Get the current weather for a location.", "parameters": { "type": "object", "properties": { "location": { "type": "string" } }, "required": ["location"] } } ], "tool_choice": "auto" } } ``` Hosted tools (MCP servers, web search, code interpreter, etc.) are not yet available and are rejected with `tool_type_not_supported`. ## Billing and Session Gating [#billing-and-session-gating] Realtime sessions bill your organization's pay-as-you-go credits, or your own provider key when the project uses [provider keys](https://docs.passingright-staging.sandbloc.com/learn/provider-keys). Every model generation — whether triggered by your `response.create` or by server-side VAD — passes the gateway's authorization gates first (credits, API key status, usage limits, IAM rules), so a session cannot run past an exhausted balance. A blocked generation surfaces as a standard `error` event on the session instead of a response. Default per-session safety limits: | Limit | Default | | ------------------------------- | ------- | | Maximum session duration | 1 hour | | Maximum spend per session | $10 | | Concurrent sessions per org | 20 | | Concurrent sessions per API key | 10 | Sessions that hit a limit are closed gracefully after in-flight responses are billed. Usage appears in your [activity feed](https://passingright.io/dashboard) like any other request, including separate line items for input transcription. ## Current Limitations [#current-limitations] * **WebSocket transport only** — WebRTC and SIP are not yet supported. * **Image input** is not yet supported in realtime sessions. * **Hosted tools** (MCP, web search, etc.) and **stored prompt references** (`session.prompt`) are not supported; inline your instructions and function tools instead. * Realtime requires a regular developer API key on a pay-as-you-go organization. End-user session tokens, platform keys, and DevPass/chat plan organizations are not supported yet. ## Self-Hosting [#self-hosting] Realtime is disabled by default on self-hosted deployments. Set `REALTIME_INLINE=true` to attach the `/v1/realtime` WebSocket listener (and the client-secret mint endpoint) to the gateway process — `pnpm dev` sets this automatically. Client secrets require Redis. See `.env.example` for the tunables (session caps, concurrency limits, shutdown grace period). # Reasoning URL: https://docs.passingright-staging.sandbloc.com/features/reasoning PassingRight supports reasoning-capable models that can show their step-by-step thought process before providing a final answer. This feature is particularly useful for complex problem-solving tasks, mathematical calculations, and logical reasoning. ## Reasoning-Enabled Models [#reasoning-enabled-models] You can find all reasoning-enabled models on our [models page with reasoning filter](https://passingright.io/models?filters=1\&reasoning=true). These models include: * OpenAI's GPT-5 series (e.g., `gpt-5`, `gpt-5-mini`) * Note: GPT-5 models use reasoning but currently do not return the reasoning content in the response. * Anthropic's Claude 3.7 Sonnet * Google's Gemini 2.0 Flash Thinking and Gemini 2.5 Pro * GPT OSS models such as `gpt-oss-120b` and `gpt-oss-20b` * Z.AI's reasoning models Some models may reason internally even if the `reasoning_effort` parameter is not specified. ## Using the Reasoning Parameter [#using-the-reasoning-parameter] There are two ways to control reasoning effort: ### Option 1: Top-level `reasoning_effort` [#option-1-top-level-reasoning_effort] Add the `reasoning_effort` parameter directly to your request: * `none` - Disable reasoning. Supported by OpenAI's newer reasoning models (e.g. `gpt-5.4-mini` and later, which accept `none` instead of `minimal`). For other providers this turns reasoning off. * `minimal` - Fastest reasoning with minimal thought process (only for GPT-5 models) * `low` - Light reasoning for simpler tasks * `medium` - Balanced reasoning for most tasks * `high` - Deep reasoning for complex problems * `xhigh` - Very deep reasoning for the most complex problems * `max` - Highest reasoning tier, above `xhigh`. Supported by Anthropic thinking models and OpenAI GPT-5.6 and later models. Effort tiers are never downgraded by the gateway: providers that accept an effort parameter receive the value unchanged (unsupported values result in a provider error), while providers that take a thinking budget instead (Anthropic, Google, Alibaba) have each tier translated to a native budget OpenAI's reasoning models do not all accept the same effort values. The original GPT-5 models support `minimal`, while newer models (e.g. `gpt-5.4-mini` and later) replace it with `none`. If you send an effort value the target model doesn't support, OpenAI returns an `unsupported_value` error. The exact values each provider mapping accepts are exposed as `reasoning_efforts` on the [`/v1/models`](https://api.passingright.io/v1/models) endpoint. ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-oss-120b", "messages": [ { "role": "user", "content": "What is 2/3 + 1/4 + 5/6?" } ], "reasoning_effort": "medium" }' ``` ### Option 2: Using the `reasoning` object [#option-2-using-the-reasoning-object] Use the unified `reasoning` configuration object with an `effort` field: * `none` - Disable reasoning * `minimal` - Fastest reasoning with minimal thought process * `low` - Light reasoning for simpler tasks * `medium` - Balanced reasoning for most tasks * `high` - Deep reasoning for complex problems * `xhigh` - Very deep reasoning for the most complex problems * `max` - Highest reasoning tier, above `xhigh`. Supported by Anthropic thinking models and OpenAI GPT-5.6 and later models. Effort tiers are never downgraded by the gateway: providers that accept an effort parameter receive the value unchanged (unsupported values result in a provider error), while providers that take a thinking budget instead (Anthropic, Google, Alibaba) have each tier translated to a native budget ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-5", "messages": [ { "role": "user", "content": "What is 2/3 + 1/4 + 5/6?" } ], "reasoning": { "effort": "medium" } }' ``` You cannot use both `reasoning_effort` and `reasoning.effort` in the same request. Choose one approach. However, you can combine `reasoning_effort` or `reasoning.effort` with `reasoning.max_tokens` — when `max_tokens` is specified, it takes priority over the effort level. ### Example Response [#example-response] The response will include a `reasoning` field in the message object containing the model's step-by-step thought process: ```json { "id": "chatcmpl-abc123", "object": "chat.completion", "created": 1234567890, "model": "gpt-oss-120b", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "The answer is 1.75 or 7/4.", "reasoning": "First, I need to find a common denominator for 2/3, 1/4, and 5/6. The LCD is 12. Converting: 2/3 = 8/12, 1/4 = 3/12, 5/6 = 10/12. Adding: 8/12 + 3/12 + 10/12 = 21/12 = 1.75 or 7/4." }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 20, "completion_tokens": 45, "reasoning_tokens": 35, "total_tokens": 65 } } ``` ## Specifying Reasoning Token Budget [#specifying-reasoning-token-budget] For models that support it, you can specify an exact token budget for reasoning using the `reasoning` object with `max_tokens`. This gives you precise control over how many tokens the model allocates to its thinking process. When `reasoning.max_tokens` is specified, it overrides `reasoning.effort` and `reasoning_effort`. Supported by Anthropic Claude and Google Gemini thinking models, plus Alibaba-hosted thinking models (forwarded as DashScope's `thinking_budget`). ### Example Request [#example-request] ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "anthropic/claude-sonnet-4-20250514", "messages": [ { "role": "user", "content": "Explain the P vs NP problem and why it matters." } ], "reasoning": { "max_tokens": 8000 } }' ``` ### Supported Models [#supported-models] The `reasoning.max_tokens` parameter is supported by: * **Anthropic Claude**: Claude 3.7 Sonnet, Claude Sonnet 4, Claude Opus 4, Claude Opus 4.5 * **Google Gemini**: Gemini 2.5 Pro, Gemini 2.5 Flash, Gemini 3 Pro Preview When using auto-routing or canonical models with `reasoning.max_tokens`, only providers that support this feature will be considered. ### Provider-Specific Constraints [#provider-specific-constraints] * **Anthropic**: Reasoning budget must be between 1,024 and 128,000 tokens. Values outside this range are automatically clamped. * **Google**: No specific constraints on the reasoning budget. ### Error Handling [#error-handling] If you specify `reasoning.max_tokens` for a model that doesn't support it, you'll receive an error: ```json { "error": { "message": "Model gpt-4o does not support reasoning.max_tokens. Remove the reasoning parameter or use a model that supports explicit reasoning token budgets.", "type": "invalid_request_error", "code": "model_not_supported" } } ``` ## Reasoning Mode [#reasoning-mode] OpenAI's GPT-5.6 and GPT-6 models accept a `reasoning.mode` of `standard` (the default) or `pro`. Pro mode spends additional model work on difficult tasks at higher latency and token usage. It is independent of effort: `mode` selects standard or pro execution, `effort` controls how much reasoning happens within that mode. ```json { "model": "gpt-5.6-sol", "messages": [ { "role": "user", "content": "Review this migration plan for failure modes." } ], "reasoning": { "mode": "pro", "effort": "medium" } } ``` The same `reasoning.mode` field works on the `/v1/responses` endpoint. Each mapping lists the values it accepts as `reasoning_modes` on [`/v1/models`](https://api.passingright.io/v1/models); auto-routing only considers mappings that list the requested mode, and a request for a model that does not list it is rejected with a 400 rather than silently run in standard mode: ```json { "error": { "message": "Model gpt-4o does not support reasoning.mode \"pro\". Remove the reasoning.mode parameter or use a model whose reasoning_modes on /v1/models include it.", "type": "invalid_request_error", "code": "model_not_supported" } } ``` ## Controlling Response Verbosity [#controlling-response-verbosity] For OpenAI GPT-5 and later models, you can control how detailed the model's final answer is with the top-level `verbosity` parameter. This is independent of `reasoning_effort`: `reasoning_effort` controls how much the model thinks, while `verbosity` controls how much it writes in its response. Accepted values: * `low` - Concise responses with minimal elaboration * `medium` - Balanced level of detail * `high` - Detailed, thorough responses ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-5", "messages": [ { "role": "user", "content": "Explain how a hash map works." } ], "verbosity": "low" }' ``` `verbosity` is only supported by OpenAI GPT-5 and later models. You can check which model mappings accept it via the [`/v1/models`](https://api.passingright.io/v1/models) endpoint. It can be combined freely with `reasoning_effort` or the `reasoning` object. ### Error Handling [#error-handling-1] If you specify `verbosity` for a model that doesn't support it, you'll receive a `400` error: ```json { "error": { "message": "Model gpt-4o does not support the verbosity parameter. Remove the verbosity parameter or use a model that supports it (OpenAI GPT-5 and later).", "type": "invalid_request_error", "code": "model_not_supported" } } ``` ## Streaming Reasoning Content [#streaming-reasoning-content] When streaming is enabled, reasoning content will be streamed as part of the response chunks: ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-oss-120b", "messages": [ { "role": "user", "content": "Solve this logic puzzle: If all roses are flowers and some flowers fade quickly, can we conclude that some roses fade quickly?" } ], "reasoning_effort": "high", "stream": true }' ``` The reasoning content will appear in the stream chunks before the final answer, allowing you to display the model's thought process in real-time. Example: ``` data: { "id": "chatcmpl-fb266880-1016-4797-9a70-f21a538edaf6", "object": "chat.completion.chunk", "created": 1761048126, "model": "openai/gpt-oss-20b", "choices": [ { "index": 0, "delta": { "reasoning": "It's ", "role": "assistant" }, "finish_reason": null } ] } ``` ## Preserving Reasoning Across Calls [#preserving-reasoning-across-calls] Agents work over many turns: the model reasons, calls a tool, and the tool result arrives in the next request. Reasoning models are trained to continue from the reasoning they produced on earlier turns, so dropping it mid-task degrades tool-calling and multi-turn performance. PassingRight preserves reasoning the same way OpenAI's Responses API does. The model returns an opaque **encrypted reasoning** payload, and you send it back unchanged along with the rest of the conversation on the next call. The payload is issued and verified by the provider — the gateway passes it through without inspecting or rewriting it. Readable reasoning summaries and opaque replay data are separate. Summaries remain available when the model provides them; the gateway cannot decrypt provider-issued reasoning payloads or Gemini thought signatures. ### Responses API [#responses-api] Ask for the payloads with `include: ["reasoning.encrypted_content"]`, and send `store: false` if you want the turn to stay stateless: ```bash curl -X POST "https://api.passingright.io/v1/responses" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-5.4-nano", "input": [{ "role": "user", "content": "What is the weather in Paris? Use the tool." }], "tools": [ { "type": "function", "name": "get_weather", "parameters": { "type": "object", "properties": { "city": { "type": "string" } }, "required": ["city"] } } ], "store": false, "include": ["reasoning.encrypted_content"], "reasoning": { "effort": "high" } }' ``` Reasoning items in `output` then carry `encrypted_content`: ```json { "type": "reasoning", "id": "rs_057d93d1ba22357e01...", "summary": [], "encrypted_content": "gAAAAABqfkEO3uEFtXeGSNE9..." } ``` On the next turn, replay the previous `output` items unchanged — reasoning items included — followed by your tool results: ```json { "model": "gpt-5.4-nano", "input": [ { "role": "user", "content": "What is the weather in Paris? Use the tool." }, { "type": "reasoning", "id": "rs_057d93d1ba22357e01...", "summary": [], "encrypted_content": "gAAAAABqfkEO3uEFtXeGSNE9..." }, { "type": "function_call", "call_id": "call_923JIrDbCvIHnGqfvDbPeaLh", "name": "get_weather", "arguments": "{\"city\":\"Paris\"}" }, { "type": "function_call_output", "call_id": "call_923JIrDbCvIHnGqfvDbPeaLh", "output": "18C sunny" } ], "store": false, "include": ["reasoning.encrypted_content"], "reasoning": { "effort": "high" } } ``` Without the `include` value, reasoning items come back without `encrypted_content` and there is nothing to replay. When streaming, the payload arrives on the reasoning item's `response.output_item.done` event. #### Stateful Alternative [#stateful-alternative] If you would rather not carry the payloads yourself, leave `store` at its default (`true`) and chain turns with `previous_response_id`. The gateway keeps the stored response — encrypted reasoning included — for [30 days](https://docs.passingright-staging.sandbloc.com/features/data-retention#retention-periods) and replays the reasoning for you, so `include` is not needed on that path. ### Chat Completions [#chat-completions] The Chat Completions format has no reasoning items, so the gateway carries the same payloads on the assistant message as `reasoning_details`: ```json { "role": "assistant", "content": null, "tool_calls": [ { "id": "call_923JIrDbCvIHnGqfvDbPeaLh", "type": "function", "function": { "name": "get_weather", "arguments": "{\"city\":\"Paris\"}" } } ], "reasoning_details": [ { "type": "reasoning.encrypted", "data": "gAAAAABqfkEO3uEFtXeGSNE9...", "id": "rs_057d93d1ba22357e01...", "format": "openai-responses-v1", "index": 0 } ] } ``` To preserve the reasoning, append the assistant message back into `messages` exactly as you received it (with `reasoning_details` intact) before the matching `tool` result. No opt-in parameter is required — the field is always present when the model produced one. When streaming, the entries arrive as a `delta.reasoning_details` chunk. The gateway converts supported `reasoning_details` formats to native provider fields and strips these entries from other upstream requests. ### Gemini Thought Signatures [#gemini-thought-signatures] Gemini text turns carry signatures in `reasoning_details` entries with `type: "reasoning.text"` and `format: "google-gemini-v1"`. Replay the entire assistant message, including these entries. Preserve each entry's `google_part` metadata: it lets the gateway restore the original signed text boundaries. Streaming clients must collect `delta.reasoning_details`, including entries arriving with empty text. When the gateway repairs JSON, clients receive the repaired answer. The entries also carry `google_response` metadata so replay can restore the original signed text. Editing the returned answer prevents that restoration. On the Responses API, replay the complete `output`: assistant message items carry `reasoning_details`, while function call items carry `extra_content.google.thought_signature`. These fields are returned without an `include` opt-in. Stored conversations using `previous_response_id` preserve them automatically. Gemini signatures are not converted into OpenAI `encrypted_content`. For Chat Completions, `content[]` text parts and `tool_calls[]` also accept `extra_content.google.thought_signature`. The tool-call signature cache remains a fallback, but replaying the metadata also works when signatures are not cached. Only replay signatures with the original content and the model/provider that issued them. ### Error Handling [#error-handling-2] Payloads are verified by the provider that issued them. A modified, truncated, or foreign payload is rejected upstream and the error is forwarded unchanged: ```json { "error": { "message": "The encrypted content for item rs_057d93d1ba22357e01... could not be verified. Reason: Encrypted content could not be decrypted or parsed.", "type": "invalid_request_error", "code": "invalid_encrypted_content" } } ``` Only replay payloads against the model and provider that produced them. ## Usage Tracking [#usage-tracking] ### Response Payload [#response-payload] The `usage` object in the response includes reasoning-specific token counts: * `reasoning_tokens` - Number of tokens used for the reasoning process * `completion_tokens` - Number of tokens in the final answer * `prompt_tokens` - Number of tokens in the input * `total_tokens` - Sum of all token counts ### Logs and Analytics [#logs-and-analytics] All requests using the `reasoning_effort` parameter are tracked in your dashboard logs with: * The `reasoningContent` field containing the full reasoning text * Separate token counts for reasoning vs. completion * Performance metrics for reasoning-enabled requests You can view detailed logs for each request in the [dashboard](https://passingright.io/dashboard) to analyze how models are reasoning through problems. ## Auto-Routing with Reasoning [#auto-routing-with-reasoning] When using auto routing (`"model": "auto"`), if the selected model supports reasoning, you did not set a reasoning effort yourself, and the request has no web search tool (web search is incompatible with `minimal` effort), PassingRight will: 1. Automatically set `reasoning_effort` to `minimal` for GPT-5 models 2. Set `reasoning_effort` to `low` for other auto-routed reasoning models 3. Only route to providers that support reasoning when `reasoning_effort` is specified This ensures optimal performance and cost when using auto-routing with reasoning-capable models. ## Model-Specific Behavior [#model-specific-behavior] Not all reasoning models return reasoning content in the same way. Some models (like OpenAI models) may reason internally but not expose the reasoning content in the response. PassingRight makes sure the response is unified across different providers, but the depth and format of reasoning may vary. ## Best Practices [#best-practices] 1. **Choose appropriate reasoning effort**: Use `low` or `minimal` for simple tasks, `medium` for most tasks, and `high` only for complex problems that require deep reasoning 2. **Monitor token usage**: Reasoning can significantly increase token consumption - monitor your `reasoning_tokens` in the usage object 3. **Stream for better UX**: When building user-facing applications, enable streaming to show the reasoning process in real-time 4. **Check logs**: Review the `reasoningContent` in your dashboard logs to understand how models are solving problems ## Error Handling [#error-handling-3] If you specify `reasoning_effort` for a model that doesn't support reasoning, you'll receive an error: ```json { "error": { "message": "Model gpt-4o does not support reasoning. Remove the reasoning_effort parameter or use a reasoning-capable model.", "type": "invalid_request_error", "code": "model_not_supported" } } ``` To avoid this error, only use the `reasoning_effort` parameter with [reasoning-enabled models](https://passingright.io/models?filters=1\&reasoning=true). # Rerank URL: https://docs.passingright-staging.sandbloc.com/features/rerank PassingRight exposes a Cohere-compatible `/v1/rerank` endpoint that scores a list of candidate documents against a query and returns them ordered by relevance. Rerankers are cross-encoders: they read the query and a document *together* rather than embedding each in isolation. That makes them far more accurate than vector similarity, but too slow to run over a whole corpus. The usual pattern is two-stage retrieval — use [embeddings](https://docs.passingright-staging.sandbloc.com/features/embeddings) to cheaply fetch the top \~100 candidates, then rerank those down to the handful you actually put in the prompt. Browse available rerank models on the [models page](https://passingright.io/models?filters=1\&rerank=true). For the full request and response schema, see the [API reference](https://docs.passingright-staging.sandbloc.com/v1_rerank). ## Endpoint [#endpoint] `POST https://api.passingright.io/v1/rerank` ## cURL [#curl] ```bash curl -X POST "https://api.passingright.io/v1/rerank" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "qwen3-reranker-8b", "query": "What is the capital of France?", "documents": [ "Paris is the capital of France.", "Berlin is the capital of Germany.", "Madrid is the capital of Spain." ], "top_n": 2 }' ``` ```json { "id": "kK1sT9pQ2mR7vX4nB6cL8dF3gH5jW0yZaA2eS4uI", "results": [ { "index": 0, "relevance_score": 0.9781517386436462 }, { "index": 1, "relevance_score": 0.00010548105638008565 } ], "meta": { "api_version": { "version": "1" }, "billed_units": { "input_tokens": 255, "total_tokens": 255 } } } ``` Results are sorted by `relevance_score` descending. Each `index` refers back to the position of that document in the request's `documents` array, so you can map scores onto your own records without relying on the returned text. The response `id` echoes the `x-request-id` header when you send one, which is handy for correlating a rerank call with its entry in the activity log. ## Request fields [#request-fields] | Field | Type | Description | | -------------------- | ---------- | ------------------------------------------------------------------------- | | `model` | `string` | Rerank model to use. Optionally prefixed with a provider (`deepinfra/…`). | | `query` | `string` | The search query to rank documents against. | | `documents` | `string[]` | Candidate documents to score. At least one. | | `top_n` | `number` | Return only the top N results. Defaults to all documents. | | `return_documents` | `boolean` | Include the document text on each result. Defaults to `false`. | | `max_chunks_per_doc` | `number` | Maximum chunks per document. Accepted, but not every provider honors it. | ## Two-stage retrieval [#two-stage-retrieval] ```ts // 1. Cheap recall: embed the query and pull the top 100 candidates // from your vector store. const candidates = await vectorStore.search(queryEmbedding, { limit: 100 }); // 2. Precise ordering: rerank those candidates and keep the best 5. const res = await fetch("https://api.passingright.io/v1/rerank", { method: "POST", headers: { Authorization: `Bearer ${process.env.LLM_GATEWAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: "qwen3-reranker-8b", query, documents: candidates.map((c) => c.text), top_n: 5, }), }); const { results } = await res.json(); const topDocuments = results.map((r) => candidates[r.index]); ``` Rerank models are billed on input tokens only — the query and every document you send are scored as input. There are no output tokens, since the response is a list of scores rather than generated text. Sending 100 long documents costs real money, so keep the first-stage candidate set tight. Rerank models only work on `/v1/rerank`. Requesting one on `/v1/chat/completions` returns a 400 pointing you at the right endpoint, and they are not available in the playground. # Response Healing URL: https://docs.passingright-staging.sandbloc.com/features/response-healing Response Healing is a plugin that automatically validates and repairs malformed JSON responses from AI models. When enabled, PassingRight ensures that API responses conform to your specified schemas even when the model's formatting is imperfect. ## Why Response Healing? [#why-response-healing] Large language models occasionally produce invalid JSON, especially in complex scenarios: * **Markdown wrapping**: Models often wrap JSON in code blocks like \`\`\`json...\`\`\` * **Mixed content**: JSON may be preceded or followed by explanatory text * **Syntax errors**: Trailing commas, unquoted keys, or single quotes instead of double quotes * **Truncated output**: Token limits may cut off responses mid-JSON Response Healing automatically detects and fixes these issues, saving you from implementing error handling for every possible malformed response. ## Enabling Response Healing [#enabling-response-healing] To enable Response Healing, add `response-healing` to the `plugins` array in your request: ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o", "messages": [{"role": "user", "content": "Return a JSON object with name and age"}], "response_format": {"type": "json_object"}, "plugins": [{"id": "response-healing"}] }' ``` Response Healing only activates when `response_format` is set to `json_object` or `json_schema`. For regular text responses, the plugin has no effect. ## How It Works [#how-it-works] When Response Healing is enabled, PassingRight applies a series of repair strategies to malformed JSON responses: ### 1. Markdown Extraction [#1-markdown-extraction] Extracts JSON from markdown code blocks: ```text Here's the data: \`\`\`json {"name": "Alice", "age": 30} \`\`\` ``` Becomes: ```json { "name": "Alice", "age": 30 } ``` ### 2. Mixed Content Extraction [#2-mixed-content-extraction] Separates JSON from surrounding text: ```text Sure! Here is the JSON you requested: {"name": "Alice", "age": 30} Let me know if you need anything else. ``` Becomes: ```json { "name": "Alice", "age": 30 } ``` ### 3. Syntax Fixes [#3-syntax-fixes] Repairs common JSON syntax violations: | Issue | Before | After | | --------------- | ------------------- | ------------------- | | Trailing commas | `{"a": 1,}` | `{"a": 1}` | | Unquoted keys | `{name: "Alice"}` | `{"name": "Alice"}` | | Single quotes | `{'name': 'Alice'}` | `{"name": "Alice"}` | ### 4. Truncation Completion [#4-truncation-completion] Adds missing closing brackets for truncated responses: ```text {"name": "Alice", "data": {"nested": true ``` Becomes: ```json { "name": "Alice", "data": { "nested": true } } ``` ## Usage Examples [#usage-examples] ### With JSON Object Format [#with-json-object-format] Request a structured response with automatic healing: ```typescript const response = await fetch("https://api.passingright.io/v1/chat/completions", { method: "POST", headers: { Authorization: `Bearer ${process.env.LLM_GATEWAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: "gpt-4o", messages: [ { role: "user", content: "Return a JSON object with fields: name (string) and age (number)", }, ], response_format: { type: "json_object" }, plugins: [{ id: "response-healing" }], }), }); const result = await response.json(); // Response is guaranteed to be valid JSON const data = JSON.parse(result.choices[0].message.content); ``` ### With JSON Schema [#with-json-schema] For stricter validation, combine with `json_schema`: ```typescript const response = await fetch("https://api.passingright.io/v1/chat/completions", { method: "POST", headers: { Authorization: `Bearer ${process.env.LLM_GATEWAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: "gpt-4o", messages: [ { role: "user", content: "Generate a user profile", }, ], response_format: { type: "json_schema", json_schema: { name: "user_profile", schema: { type: "object", required: ["name", "email"], properties: { name: { type: "string" }, email: { type: "string" }, age: { type: "number" }, }, }, }, }, plugins: [{ id: "response-healing" }], }), }); const result = await response.json(); ``` ## Healing Metadata [#healing-metadata] When a response is healed, the healing method is logged for debugging. The following healing methods may be applied: | Method | Description | | -------------------------- | ------------------------------------------- | | `markdown_extraction` | JSON extracted from markdown code blocks | | `mixed_content_extraction` | JSON extracted from surrounding text | | `syntax_fix` | Trailing commas, quotes, or keys were fixed | | `truncation_completion` | Missing closing brackets were added | | `combined_strategies` | Multiple strategies were applied | ## Limitations [#limitations] Response Healing works for streaming requests too, but it changes how the stream is delivered: the gateway buffers the whole content stream, repairs it, and replays the healed content as a single chunk — so token-by-token streaming is lost for that request. Healing is disabled for multi-choice streams (`n` greater than 1). Healing also runs automatically — without the `plugins` entry — for a few providers and models whose native JSON mode is known to emit malformed output; in those cases the gateway repairs `response_format` JSON responses on its own. Response Healing works best for: * Simple to moderately complex JSON structures * Common formatting issues from LLMs It may not be able to repair: * Severely corrupted or nonsensical output * Complex nested structures with multiple issues * Responses that don't contain any recognizable JSON ## Best Practices [#best-practices] ### Use with Structured Prompts [#use-with-structured-prompts] Combine Response Healing with clear instructions for best results: ```typescript const response = await fetch("https://api.passingright.io/v1/chat/completions", { method: "POST", headers: { Authorization: `Bearer ${process.env.LLM_GATEWAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: "gpt-4o", messages: [ { role: "system", content: "Always respond with valid JSON. No explanations.", }, { role: "user", content: "List three colors as a JSON array", }, ], response_format: { type: "json_object" }, plugins: [{ id: "response-healing" }], }), }); const result = await response.json(); ``` ### Validate Critical Data [#validate-critical-data] For critical applications, validate the healed JSON in your code: ```typescript const result = await response.json(); const content = result.choices[0].message.content; const data = JSON.parse(content); // Add your own validation if (!data.name || typeof data.name !== "string") { throw new Error("Invalid response: missing name"); } ``` ### Monitor Healing Rates [#monitor-healing-rates] If you notice frequent healing in your logs, consider: * Improving your prompts to request cleaner JSON * Using models with better JSON output (e.g., GPT-4o, Claude 3.5) * Adding explicit JSON examples in your prompts # Routing URL: https://docs.passingright-staging.sandbloc.com/features/routing PassingRight provides flexible and intelligent routing options to help you get the best performance and cost efficiency from your AI applications. Whether you want to use specific models, providers, or let our system automatically optimize your requests, we've got you covered. PassingRight also includes **automatic retry and fallback** — if a provider fails, your request is seamlessly retried on the next best provider, all within the same API call. ## Global Provider Rate Limits [#global-provider-rate-limits] Administrators can set a global RPM or RPD limit to `0` to block matching provider/model requests, with either **Global** or **Per-organization** enforcement. Remove the limit or set a positive value to resume traffic; changes propagate through the rate-limit cache (default 60 seconds). Existing precedence still applies: organization-specific limits and more specific global provider/model limits can override a provider-wide limit for the same window. Routing can fall back to another available provider. If a zero-limited provider remains selected, the request returns `429` without calling it, even when every candidate is rate-limited. No configured limit still means unlimited; other rate-limit settings retain their existing behavior. ## Model Selection [#model-selection] ### Any Model Name [#any-model-name] You can use any model name from our [models page](https://passingright.io/models) or discover available models programmatically through the [/v1/models endpoint](https://docs.passingright-staging.sandbloc.com/v1_models). ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o", "messages": [{"role": "user", "content": "Hello!"}] }' ``` ### Model ID Routing [#model-id-routing] Choose a specific model ID to route to the **best available provider** for that model. PassingRight's smart routing algorithm considers multiple factors to find the optimal provider across all configured options. #### Smart Routing Algorithm [#smart-routing-algorithm] When you use a model ID without a provider prefix, PassingRight's intelligent routing system analyzes multiple factors to select the best provider. **Weighted Scoring System**: Each factor has a **relative weight**. The factors are scored as ratios against the best provider in the candidate set (e.g. a provider that is twice as expensive as the cheapest scores `1.0` on price), and each ratio is multiplied by its weight divided by the sum of all active weights. The provider with the lowest (best) total score wins. The default weights are: | Factor | Default weight | Notes | | --------------- | -------------- | --------------------------------------------------------------------------------- | | **Price** | `0.6` | Token cost for the expected input/output mix, including cache reads when relevant | | **Uptime** | `0.5` | Provider reliability / low error rate | | **Throughput** | `0.05` | Tokens per second generation speed | | **Latency** | `0.025` | Time to first token — **only applied for streaming requests** | | **Cache** | `0` | Optional cache-support preference; cache-read savings already count toward price | | **Image price** | `1.0` | Replaces the price weight for image-generation models | Because the weights are relative and normalized by the sum of the active weights, price and uptime dominate routing decisions in practice, while throughput and latency act as tie-breakers between otherwise comparable providers. **Latency Weight for Non-Streaming Requests**: The latency weight only applies to streaming requests (time-to-first-token is only measured there). For non-streaming requests the latency weight is dropped and its share is redistributed proportionally across the remaining factors. **Time-Decayed Metrics Window**: Provider metrics (uptime, throughput, latency) are not a flat "last N minutes" snapshot. They are aggregated over a rolling **60-minute window** with a time-decay weighting so very recent behavior dominates while older data still contributes: * The most recent **1 minute** is weighted **10×** * The most recent **5 minutes** are weighted **3×** * The remainder of the 60-minute window is weighted **1×** This makes routing react quickly to a provider that just started failing or slowing down, without overreacting to a single noisy data point. **Prompt Caching and Token Costs**: For estimated prompts of at least **5,000 tokens**, or when choosing a session's provider, routing blends each provider's uncached and cached input prices and weights output by the expected output:input token ratio. Coding sessions that mostly reuse prompt tokens can therefore favor a provider with cheaper cache reads even when its uncached input price is higher. Providers without a cached input price use their full input price. These estimates use the project's **last 24 hours of model usage**, once it includes at least **20 successful requests and 20,000 input tokens**: * **Cache-hit rate** is cached input tokens divided by total input tokens. Each provider uses its own rate, counted across all of its regions, once it meets the same sample thresholds; otherwise it uses the project's combined rate for that model. * **Output:input ratio** uses the project's combined output and input tokens for that model across providers. Routing caches the usage lookup for 60 seconds. It reads hourly aggregates, which work with payload retention disabled. Hourly buckets containing gateway response-cache hits are excluded because they cannot isolate upstream usage, so projects with response caching enabled may keep using the defaults. During lookup failures, routing uses previously cached observations when available, then falls back to configured estimates. These are workload estimates, not guarantees that a particular prompt will hit a provider's cache. Without enough history, routing uses these initial workload estimates: | Workload | Cached input | Output:input ratio | | ------------------------------------- | ------------ | ------------------ | | General API / unknown | 10% | 20% | | DevPass or a recognized coding client | 90% | 2% | | Chat organization | 50% | 10% | Recognized coding clients use the coding defaults even on regular API projects. A session id alone does not identify coding traffic. These are starting assumptions; sufficient project/model observations replace them. The dashboard reports organization defaults, while recognized coding requests use the coding profile at request time. Explicit Enterprise overrides for `thresholds.cacheHitRate` and `thresholds.cacheOutputRatio` take precedence over both workload defaults and observations. Setting them to `0` and `1`, respectively, restores list-price ranking. Both `auto` and `price` routing use these token-cost estimates. The separate **cache weight** defaults to `0`, so cache support alone does not outweigh lower estimated costs. Enterprise projects can explicitly enable that additional preference under `auto`; `price` routing always sets it to zero. Cache support appears as `cacheSupported` in routing metadata. When choosing a [session's provider](#sticky-session-routing), routing applies the workload estimate even to a short opening prompt, using observations when available and workload defaults otherwise. This estimates the session's token mix; the opening request may still incur cache misses. Small requests outside a session weight input and output prices equally and omit the cache weight. **Exponential Uptime Penalty**: Providers with uptime below 95% receive an additional exponential penalty that increases rapidly as uptime drops: * 95-100% uptime: No penalty * 90% uptime: \~0.07 penalty * 80% uptime: \~0.62 penalty * 70% uptime: \~1.73 penalty * 50% uptime: \~5.61 penalty This ensures providers experiencing significant issues are strongly deprioritized while minor fluctuations have minimal impact. The penalty threshold (default `95%`) is configurable. **Provider Priority**: Each provider has a **priority** value (default `1`) that nudges routing toward or away from it independently of live metrics: * A provider's priority is applied as a `(1 - priority)` adjustment to its score — higher priority lowers the score (more preferred), lower priority raises it (less preferred). * A priority of **0** disables the provider entirely, removing it from routing for that model. Provider priorities are surfaced in the routing metadata so you can see how they influenced a decision. **Epsilon-Greedy Exploration** (1% of requests by default): To solve the "cold start problem" where new or unused providers never get traffic to build up metrics, the system randomly explores different providers a small fraction of the time (default 1%, configurable). This ensures: * All providers periodically receive traffic * New providers can prove their reliability * The system adapts to changing provider performance * You benefit from improved routing decisions over time The exploration rate is configurable per project through the routing configuration (`thresholds.explorationRate`), and self-hosted deployments can override it globally with the `EXPLORATION_RATE` environment variable (a number between `0` and `1`). **Stable Provider Preference**: To avoid unnecessary churn between providers that score similarly, PassingRight remembers the best provider chosen for each model and sticks with it across requests — even if another provider edges ahead slightly on the next score calculation. On every routing decision, the system checks whether the previously selected provider is still acceptable: * **Uptime hard switch**: if the preferred provider's uptime drops below **85%**, routing switches to the current best-scoring provider immediately. * **Score margin soft switch**: the preferred provider is replaced only when a better option's score is more than **0.15** ahead. Small fluctuations caused by metric noise or minor price differences do not trigger a switch. * **Periodic re-evaluation**: the preference expires after **1 hour**, at which point the next request picks the best-scoring provider fresh and stores it as the new preferred. Requests that are part of the epsilon-greedy exploration bypass this preference entirely so that all providers continue to receive periodic traffic and build up metrics. The selection reason in routing metadata will show `stable-preferred` when a request was served by the stored preference rather than the top-scored provider at that moment. Self-hosted deployments can tune this behavior with three environment variables: `PREFERRED_PROVIDER_TTL` (preference lifetime in seconds, default `3600`), `PREFERRED_PROVIDER_UPTIME_THRESHOLD` (hard-switch uptime floor, default `85`), and `PREFERRED_PROVIDER_SCORE_MARGIN` (soft-switch score gap, default `0.15`). On the **Enterprise plan**, these same values can be customized per project from the dashboard — see [Per-Project Routing Configuration](#per-project-routing-configuration-enterprise). **Routing Metadata**: Every request includes detailed routing metadata in the logs, showing: * Available providers that were considered * Selected provider and selection reason * Scores for each provider (including uptime, throughput, latency, price, priority, and cache support) This transparency allows you to understand and debug routing decisions. Using model IDs without a provider prefix automatically routes to the optimal provider based on reliability, speed, and cost. The system continuously learns and adapts based on real-time performance metrics. Smart routing prioritizes reliability over cost, ensuring your requests are routed to providers with proven uptime and performance, while still considering cost efficiency. ### Routing Strategy [#routing-strategy] By default, model-ID routing uses the full weighted score described above (`routing: "auto"`). When you care about a single dimension, set the `routing` field — named after the factor it optimizes — to bias provider selection toward it: | Strategy | Behavior | | ---------------------------- | ------------------------------------------------------------------------------------------ | | `auto` *(default)* | Full weighted smart-routing score (price, uptime, throughput, latency, cache). | | `price` | Gives price a **90% relative weight**, including estimated cache-read costs when relevant. | | `throughput` | Gives throughput a **90% relative weight**, so the fastest-generating provider wins. | | `latency` | Gives latency a **90% relative weight**, so the lowest time-to-first-token wins. | Each non-`auto` strategy keeps a small (10%) uptime weight, and the [exponential uptime penalty](#smart-routing-algorithm) still applies on top. This means the dominant pick is still skipped in favor of another provider when it has extremely bad uptime — you get the cheapest (or fastest) provider that is actually healthy, not one that is effectively down. Because time-to-first-token is only measured for streaming requests, `routing: "latency"` only biases streaming requests; for non-streaming requests it falls back to selecting on uptime. ```bash # Always pick the cheapest healthy provider for this model curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "deepseek-v3.2", "messages": [{"role": "user", "content": "Hello!"}], "routing": "price" }' ``` ```bash # Always pick the highest-throughput healthy provider for this model curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "deepseek-v3.2", "messages": [{"role": "user", "content": "Hello!"}], "routing": "throughput" }' ``` The `routing` field only applies to model-id routing. Combining it with a specific provider (e.g. `openai/gpt-4o`) returns a `400` error, since the strategy can't influence a pinned provider — remove the provider prefix to use a strategy. On **coding (dev) plans**, only `auto` and `price` are allowed; the other strategies return a `400` error because they would bypass the prompt-cache–aware routing those plans depend on. ### Sticky Session Routing [#sticky-session-routing] When a model is served by multiple providers, every request is normally scored independently — so a multi-turn conversation can bounce between providers. That defeats provider-side **prompt caching**, which only pays off when consecutive requests with a shared prefix hit the **same** provider. Sticky session routing solves this: attach a session identifier and PassingRight pins all requests for that session to a single provider (and region), keeping the upstream prompt cache warm across the whole conversation. #### Setting the session id [#setting-the-session-id] For chat completions, the session key is resolved in priority order: 1. The `x-session-id` header 2. The `x-session-affinity` header (sent automatically by coding agents such as opencode) 3. The `session_id` or `session-id` header 4. The `prompt_cache_key` body field (OpenAI-compatible) 5. The `user` body field (OpenAI-compatible) ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -H "x-session-id: conversation-9f8e7d6c" \ -d '{ "model": "claude-sonnet-4-6", "messages": [{"role": "user", "content": "Hello!"}] }' ``` For the Anthropic Messages endpoint (`/v1/messages`), the session key is derived automatically from `metadata.user_id` — coding agents such as Claude Code embed the session id there — and forwarded internally. An explicit `x-session-id` header still takes precedence. #### How pinning works [#how-pinning-works] On a session's **first** request the provider is chosen by the normal weighted smart-routing score — the same price-, priority-, uptime-, and throughput-aware algorithm used for non-sticky requests. That choice is then **persisted for the session** and reused on every subsequent request, so the upstream prompt cache stays warm without bouncing the conversation between providers. The first selection uses the expected cache-hit rate and output/input mix even if the opening prompt is short. Routing uses observed project/model usage when sufficiently sampled, otherwise the workload defaults above. New usage observations affect future provider selections; a healthy existing pin stays in place as described below. Because the pinned provider is replayed directly, sticky requests **skip the epsilon-greedy exploration** — a session is never randomly bounced to a different provider mid-conversation. Request compatibility takes precedence over the saved pin. The gateway first filters mappings for requirements such as input modalities, service tiers, regions, and a non-`auto` `tool_choice`, then looks for the pinned provider in that eligible set. If the pinned mapping cannot honor the request but another mapping can, the session moves to the capable mapping and the pin is updated. For a fixed model or dynamic route where **no** mapping can honor `tool_choice`, the gateway preserves availability instead: it keeps the mappings, downgrades `tool_choice` to `auto`, and sticky routing may retain the existing pin. Automatic model selection does not use that fallback because it can choose a capable model instead. #### Falling back when a provider is down [#falling-back-when-a-provider-is-down] An established pin yields only when its provider can no longer serve the session well. A session is re-scored and re-pinned to the current weighted-best provider when its provider: * Drops below the session uptime threshold (default 85%), or * Is filtered out of the candidate set (health or compatibility filtering). Sticky requests never enter the cross-provider [automatic retry & fallback](#automatic-retry--fallback) loop — a transient failure is retried against the pinned provider only, on another configured key when several exist, or on the same platform key when only one is configured. The failure still degrades that provider's uptime metrics, which is what triggers re-pinning on a subsequent request once the uptime threshold is crossed. Re-pinning runs the same weighted algorithm again, so the replacement is the best currently available provider — not an arbitrary one. The selection reason in routing metadata shows `session-sticky` when a request was pinned via a session id. Sticky routing optimizes for cache locality over per-request churn. Once a session is pinned it stays on its provider even if a cheaper or faster alternative becomes momentarily available, since the prompt-cache savings typically outweigh the difference — but the initial pick still respects price and priority. Requests without a session id are unaffected and continue to use the weighted smart-routing algorithm. ### Provider-Specific Routing [#provider-specific-routing] To use a specific provider without any fallbacks, prefix the model name with the provider name followed by a slash: ```bash # Use OpenAI specifically curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-4o", "messages": [{"role": "user", "content": "Hello!"}] }' # Use DeepSeek provider specifically curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "deepseek/deepseek-v3.2", "messages": [{"role": "user", "content": "Hello!"}] }' ``` Provider-specific routing is not available on **DevPass coding plans** — provider-prefixed model ids (and custom provider routing) return a `403` there. DevPass always uses the prompt-cache–aware smart routing above with plain model ids. Provider pinning requires the pay-as-you-go API. See [Provider routing on DevPass](https://docs.passingright-staging.sandbloc.com/learn/model-categories#provider-routing-on-devpass). #### Regions [#regions] Some providers expose the same model in multiple regions. In that case, PassingRight supports two routing modes: * `provider/model` selects the best eligible region for that provider using the same routing inputs used elsewhere: recent uptime, throughput, latency, and price * `provider/model:region` pins the request to one exact region ```bash # Let PassingRight choose the best Alibaba region for DeepSeek V3.2 curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "alibaba/deepseek-v3.2", "messages": [{"role": "user", "content": "Hello!"}] }' # Force a specific Alibaba region curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "alibaba/deepseek-v3.2:cn-beijing", "messages": [{"role": "user", "content": "Hello!"}] }' ``` If your provider key stores an explicit region, that region acts like a lock and PassingRight will only use that region for provider-specific requests. If no explicit region is configured on the provider key, provider-specific requests can still score all eligible regions for that provider. Routing metadata reflects this: * Dynamic provider-region selection shows all eligible regional scores that were considered * Explicitly pinned regions show only the pinned region in the score list Region-aware routing only compares regions that are actually available for the current project mode and provider setup. In credits mode, that means only regions backed by configured environment keys. In API keys and hybrid mode, an explicit provider-key region restricts the request to that region. A few regions are served by an endpoint that belongs to your own account rather than a shared one — Alibaba Cloud's EU (Frankfurt) region has no shared DashScope domain and is served by your Model Studio workspace's dedicated host. Such a region still works from an API key alone, via the provider's shared entry point, but that endpoint is rate-limited and carries no SLA. Set the workspace ID on the provider key (copy it from the API Host shown when you create the key) to route through your own endpoint instead. #### Low-Uptime Protection [#low-uptime-protection] When you specify a provider explicitly, PassingRight checks the provider's recent uptime (from the time-decayed metrics window described above). If the uptime falls below 90%, the system automatically routes your request to the best available alternative provider to ensure reliability. This protects your application from providers experiencing temporary issues. The fallback threshold (default `90%`) is configurable. If the requested provider has low uptime but no alternative providers are available for that model, the request will still be sent to the originally requested provider. #### Disabling Fallback with X-No-Fallback Header [#disabling-fallback-with-x-no-fallback-header] If you need to bypass this protection and always use the exact provider you specified regardless of its current uptime, you can use the `X-No-Fallback` header: ```bash # Force use of a specific provider even if it has low uptime curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -H "X-No-Fallback: true" \ -d '{ "model": "openai/gpt-4o", "messages": [{"role": "user", "content": "Hello!"}] }' ``` Using `X-No-Fallback: true` disables automatic provider failover. Your requests will be sent to the specified provider even if it is experiencing issues, which may result in higher error rates. Retries may still occur against another key for the same provider when multiple keys are configured. When the `X-No-Fallback` header is used, the routing metadata in logs will include `noFallback: true` to indicate that fallback was disabled for that request. ## Automatic Retry & Fallback [#automatic-retry--fallback] When using model ID routing (without a provider prefix), PassingRight automatically retries failed requests on alternate providers. This happens transparently within the same API call — your application receives the successful response as if nothing went wrong. ### How Retry Works [#how-retry-works] 1. Your request is routed to the best available provider using the smart routing algorithm 2. If that provider fails with a retryable error (see [What Triggers a Retry](#what-triggers-a-retry) below), the gateway marks the provider as failed 3. The next best available provider is selected and the request is retried 4. Up to **2 retries** by default (configurable per project via the routing configuration) are attempted before returning an error to the client ``` Request → Provider A (500 error) → Provider B (200 OK) → Response ``` Both streaming and non-streaming requests support automatic retry. ### What Triggers a Retry [#what-triggers-a-retry] Retries are triggered by failures classified as **provider-side or gateway-side problems**: * **5xx errors** (500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable, etc.) * **Timeouts** (upstream provider took too long to respond) * **Connection failures** (network errors, DNS failures, etc.) * **Upstream rate limits** (a `429` from the provider) * **Provider account and mapping problems** — an upstream `401`/`403` (bad provider credentials), `402` (provider account out of funds), `404`/`405` (model or endpoint mapping gap), and a few specific `400` bodies that indicate the same kinds of gateway-side problems Retries are **not** triggered by: * **4xx client errors** — a request that is genuinely invalid (validation errors, unsupported parameters) fails the same way everywhere, so it is passed through with its original status instead of retried * **Content filter responses** (Azure ResponsibleAI, etc.) ### When Retry Is Disabled [#when-retry-is-disabled] Automatic retry to a different provider is disabled when: * The `X-No-Fallback: true` header is set * A specific provider is requested (e.g., `openai/gpt-4o`) * The request carries a session id and [sticky session routing](#sticky-session-routing) is enabled — the session stays pinned to its provider * No alternative providers are available for the requested model * The maximum retry count (default 2) has been exhausted Retries can still happen within the same provider when multiple keys are configured and the current key fails with a retryable error. ### Routing Transparency [#routing-transparency] Every provider attempt — both failed and successful — is recorded in the `routing` array in the response metadata (streaming and non-streaming alike) and activity logs: ```json { "metadata": { "routing": [ { "provider": "openai", "model": "gpt-4o", "status_code": 500, "error_type": "server_error", "succeeded": false, "credentialSource": "byok", "apiKeyHash": "f029ee9", "providerKeyId": "pk_2f9a...", "providerKeyLabel": "billing-team-key" }, { "provider": "azure", "model": "gpt-4o", "status_code": 200, "error_type": "none", "succeeded": true, "credentialSource": "platform", "apiKeyHash": "ecb88d5" } ] } } ``` #### Whose key served each attempt [#whose-key-served-each-attempt] `credentialSource` says who owns the provider credential an attempt was sent with: | Value | Meaning | | ---------- | --------------------------------------------------------------------------------------------------------- | | `byok` | Your own provider key. The provider bills you directly and the attempt is not deducted from your credits. | | `platform` | An PassingRight credential. The attempt runs on credits and is deducted from your balance. | This matters most in **hybrid** mode, where a request that fails on your own key falls back to PassingRight's credential: both attempts appear in the same `routing` array, and only `credentialSource` tells them apart — `apiKeyHash` is an opaque fingerprint that says two attempts used different keys, not which key was yours. The same value is stored on the log as `routingMetadata.usedCredentialSource` for the credential that ultimately served the request, and is shown as a **your key** / **PassingRight key** badge in the dashboard's routing view. #### Which of your keys ran [#which-of-your-keys-ran] A `byok` attempt also carries the key itself: `providerKeyId`, and `providerKeyLabel` — the key as it is named on your [provider keys](https://passingright.io/dashboard) page (its name, or its masked token when it has none). So when several of your keys are configured for a provider and the gateway rotates between them, each attempt says which one it used instead of leaving you to decode a fingerprint. Chat requests additionally record `routingMetadata.eligibleProviderKeys` on the log: your keys that were candidates for the provider that served the request, in selection order. It is omitted for credits-mode projects, which route on PassingRight credentials, and for custom providers, whose keys are scoped by their own catalogue. These fields describe **your** keys only. PassingRight's own credentials — the ones that serve credits-mode traffic — are never named: a `platform` attempt still reports `credentialSource` and `apiKeyHash`, but never `providerKeyId` or `providerKeyLabel`. ### Retried Log Tracking [#retried-log-tracking] Each provider attempt creates its own log entry. Failed attempts that were retried are marked with: * **`retried: true`** — indicates this failed request was retried on another provider * **`retriedByLogId`** — the ID of the final successful log entry This allows you to distinguish between unrecovered failures and failures that were transparently recovered via retry. In the dashboard, retried logs display a "Retried" badge with a link to the successful log. ### Impact on Provider Health [#impact-on-provider-health] Failed attempts still count against the provider's uptime score, even when the request was successfully retried on another provider. This means: * A provider that keeps failing will see its uptime score drop * Only gateway and upstream errors count: requests rejected as client errors (invalid request bodies, unsupported parameters) are excluded from both the error count and the request total, so your own bad requests never mark a provider as down * The exponential uptime penalty kicks in below 95% (see [Smart Routing Algorithm](#smart-routing-algorithm)) * Future requests are automatically routed away from unreliable providers * Your application stays reliable without any code changes on your side Automatic retry and fallback works together with smart routing to provide self-healing behavior. Failing providers are automatically avoided, and your requests are transparently recovered on reliable alternatives. ## Per-Project Routing Configuration (Enterprise) [#per-project-routing-configuration-enterprise] All plans use observed token usage for cache pricing when sufficient history exists. On the **Enterprise plan**, you can override the settings listed below **per project** from the dashboard under **Project Settings → Routing**, including explicit cache-pricing assumptions. The 24-hour usage window and minimum sample requirements are fixed; the **History** settings control uptime, throughput, and latency metrics. Overrides are merged on top of the defaults, so you only set the values you want to change. When a custom configuration is disabled, the project falls back to the defaults. The following groups can be customized per project: | Group | What it controls | Defaults | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Weights** | Relative importance of each scoring factor | `price 0.6`, `imagePrice 1.0`, `uptime 0.5`, `throughput 0.05`, `latency 0.025`, `cache 0` | | **Thresholds** | Cache prompt size and pricing overrides, uptime-penalty threshold, exploration rate, and fallback metrics | `cachePromptTokens 5000`, `cacheHitRate 0.1`, `cacheOutputRatio 0.2` (coding: `0.9` / `0.02`; Chat: `0.5` / `0.1`), `uptimePenalty 95`, `defaultUptime 100`, `defaultLatency 1000`, `defaultThroughput 50`, `explorationRate 0.01` | | **Retry** | Max cross-provider fallback attempts and the low-uptime reroute threshold | `maxRetries 2`, `lowUptimeFallbackThreshold 90` | | **Timeouts** | Per-request time limits (end-to-end, streaming, non-streaming) — see [Request Timeouts](https://docs.passingright-staging.sandbloc.com/features/timeouts). Capped at the infrastructure defaults — an override can only lower them | `gatewayMs 1,500,000`, `streamingMs 1,200,000`, `plainMs 600,000` | | **History** | The metrics window and the time-decay tier boundaries and weights | `windowMinutes 60` (max 120), `tier1Minutes 1`, `tier2Minutes 5`, `tier1Weight 10`, `tier2Weight 3`, `tier3Weight 1` | | **Sticky** | Stable-provider preference: on/off, TTL, hard-switch uptime floor, soft-switch score margin | `enabled true`, `ttlSeconds 3600`, `uptimeThreshold 85`, `scoreMargin 0.15` | | **Session** | [Sticky session routing](#sticky-session-routing): on/off, pin TTL, re-pin uptime floor | `enabled true`, `ttlSeconds 3600`, `uptimeThreshold 85` | | **Provider priorities** | Per-provider priority multipliers; set a provider to `0` to disable it for that project | `1` for every provider | Per-project routing configuration requires the Enterprise plan. If you'd like to tune routing for your workloads, contact us at [support@passingright.io](mailto:support@passingright.io). ## Optimized Auto Routing [#optimized-auto-routing] Auto routing automatically selects the best model for your specific use case without you having to specify a model at all. ### Current Implementation [#current-implementation] The auto routing system currently: * **Chooses cost-effective models** by default for optimal price-to-performance ratio * **Automatically scales to more powerful models** based on your request's context size * **Handles large contexts intelligently** by selecting models with appropriate context windows ```bash # Let PassingRight choose the optimal model curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "auto", "messages": [{"role": "user", "content": "Your request here..."}] }' ``` ### Free Models Only [#free-models-only] When using auto routing, you can restrict the selection to only free models (models with zero input and output pricing) by setting the `free_models_only` parameter to `true`: ```bash # Auto route to free models only curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "auto", "messages": [{"role": "user", "content": "Hello!"}], "free_models_only": true }' ``` Adding even a small amount of credits to your account (e.g., $10) will immediately upgrade your free model rate limits from 5 requests per 10 minutes to 20 requests per minute (free-model use still requires a verified email). The `free_models_only` parameter only works with auto routing (`"model": "auto"`). If no free models are available that meet your request requirements, the API will return an error. ### Reasoning models only [#reasoning-models-only] Just specify the `reasoning_effort` value and only a model which supports reasoning will be chosen. This parameter is not specific to the auto model. ```bash # Auto route only to reasoning models curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "auto", "messages": [{"role": "user", "content": "Hello!"}], "reasoning_effort": "medium" }' ``` ### Exclude Reasoning Models [#exclude-reasoning-models] When using auto routing, you can exclude reasoning models from selection by setting the `no_reasoning` parameter to `true`. This is useful when you want faster responses or need to avoid the additional cost and latency of reasoning models: ```bash # Auto route excluding reasoning models curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "auto", "messages": [{"role": "user", "content": "Hello!"}], "no_reasoning": true }' ``` The `no_reasoning` parameter only works with auto routing (`"model": "auto"`). If no non-reasoning models are available that meet your request requirements, the API will return an error. Auto routing analyzes your payload and automatically chooses between cost-effective models for simple requests and more powerful models for complex or large-context requests. ### Coming Soon: Advanced Optimization [#coming-soon-advanced-optimization] We're continuously improving our auto routing capabilities. Soon you'll benefit from: * **Tool call optimization**: Automatically select models that excel at function calling and structured outputs * **Content-aware routing**: Analyze message content to determine the best model for specific types of requests (coding, creative writing, analysis, etc.) * **Performance-based routing**: Route based on historical performance data for similar requests * **Multi-model orchestration**: Intelligently combine multiple models for complex workflows ### How It Works [#how-it-works] 1. **Request Analysis**: The system analyzes your request including message content, context size, and any special parameters 2. **Model Selection**: Based on the analysis, it selects the most appropriate model considering cost, performance, and capabilities 3. **Transparent Routing**: Your request is seamlessly routed to the chosen model and provider 4. **Optimized Response**: You receive the best possible response while maintaining cost efficiency Auto routing decisions are transparent in your usage logs, so you can always see which model was selected for each request. ## Best Practices [#best-practices] ### For Development [#for-development] * Use specific model names during development and testing * Leverage auto routing for production workloads to optimize costs ### For Production [#for-production] * Use auto routing (`"model": "auto"`) for the best balance of cost and performance * Monitor your usage patterns through the dashboard to understand routing decisions * Set up provider keys for multiple providers to maximize routing options ### For Cost Optimization [#for-cost-optimization] * Let auto routing handle model selection to automatically use the most cost-effective options * Use model IDs without provider prefixes to always get the cheapest available provider * Monitor your usage analytics to track cost savings from intelligent routing # Service Tiers URL: https://docs.passingright-staging.sandbloc.com/features/service-tiers Some OpenAI, Google, and Fireworks models support selectable **processing tiers** that trade latency and availability against price. You pick one per request with the OpenAI-compatible `service_tier` parameter, and PassingRight forwards it only when the selected provider/model mapping supports that tier. | Tier | `service_tier` | Cost vs. standard | Latency / availability | | ------------ | ------------------------- | ----------------- | ------------------------------------------- | | Standard | `default` / `auto` / omit | baseline | Normal on-demand latency | | **Flex** | `flex` | **−50%** | Best-effort; may be preempted under load | | **Priority** | `priority` | varies by model | Prioritized above standard and flex traffic | ## Using the `service_tier` parameter [#using-the-service_tier-parameter] ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "google-vertex/gemini-3.1-pro-preview", "service_tier": "priority", "messages": [ { "role": "user", "content": "Summarize this incident report." } ] }' ``` Accepted values are `flex`, `priority`, and `default`/`auto` (standard). If you request `flex` or `priority` for a provider/model mapping that does not support that tier, the gateway returns a 400 `unsupported_service_tier` error and logs the request as a client error. The parameter works the same on the OpenAI-compatible **Responses API** (`/v1/responses`): the tier is forwarded to the provider and the response's `service_tier` field echoes the tier that was actually served. The **Images API** (`/v1/images/generations` and `/v1/images/edits`) accepts it too and forwards it to the underlying image model, so image mappings that list Flex can be generated at the Flex rate. In Lounge, the Image Studio shows a **Flex** toggle whenever the selected model offers the tier. Coding (dev) plans are limited to `default`/`auto` and `flex` — the premium `priority` tier is not part of the plan and a request that asks for it returns a 403. ## Supported providers [#supported-providers] Service tiers are explicit per provider/model mapping. Check the model page for the exact tiers exposed by each provider card. * **OpenAI** (`openai`) — sent as the OpenAI `service_tier` request field for supported OpenAI models. Flex is billed at 0.5x standard token prices and Priority uses the model-specific multiplier shown on the model page. * **Google Vertex AI** (`google-vertex`) — sent as the `X-Vertex-AI-LLM-Shared-Request-Type` request header, together with `X-Vertex-AI-LLM-Request-Type: shared` so the request bypasses any Provisioned Throughput on the project and actually reaches the shared Flex/Priority tier. Flex and Priority are served only on the **global** endpoint, which is the gateway default. Google Flex PayGo applies a 0.5x multiplier; Google Priority PayGo applies a 1.8x multiplier. * **Google AI Studio / Gemini API** (`google-ai-studio`) — sent as a `service_tier` field in the request body for configured models that opt in. * **Fireworks AI** (`fireworks`) — sent as a `service_tier` field in the request body on its OpenAI-compatible chat completions endpoint. Only Priority is offered (Fireworks publishes no Flex rate card), billed at a 1.25x multiplier. Fireworks does not report the tier it served, but a Priority request is either served at Priority or shed with a 503, so an accepted request is billed at the tier it was sent at. Tiers are supported on a **subset** of models, and the Flex and Priority subsets differ by provider. For example, Google Flex PayGo lists Gemini 3 image / Nano Banana models, but Google Priority PayGo does not; those configured image mappings are Flex-only. Flex and Priority are only honored when the request reaches the provider directly, so a provider key with a **custom base URL** (a proxy) is excluded from service-tier routing — a proxy may silently drop the tier and serve standard. This applies to every provider that offers tiers, including OpenAI: the tier travels as a `service_tier` body field that an OpenAI-compatible proxy is free to ignore. With multiple providers/keys, the gateway routes around the ineligible key automatically; if a request pins a provider whose only key uses a custom base URL, it returns a 400 instead of silently downgrading. Keys with no custom base URL (the managed default) are always eligible. ## Retries and fallback never downgrade the tier [#retries-and-fallback-never-downgrade-the-tier] A request can change provider or credential mid-flight: PassingRight falls back to another provider when one fails, and rotates to another key for the same provider when a credential returns a 429 or an auth error. A requested tier is carried through all of it. * Provider routing is narrowed to mappings that support the requested tier **before** a provider is picked, so no fallback candidate can be one that would serve the request as standard. * Key selection — BYOK keys, platform-managed credentials, and env credentials alike — skips any credential that cannot carry the tier (a proxy base URL, or a Vertex credential pinned to a regional endpoint), on the first attempt and on every retry. * Every attempt re-resolves the tier against the provider, region and credential it actually resolved to, and fails rather than sending at a lower tier. If no eligible candidate is left, you get the upstream error instead of a silently downgraded response. This applies to a tier you requested yourself. The optional coding-plan default tier is a cost preference rather than a requirement, so a request that cannot be served at that tier runs at standard instead of failing; `used_service_tier` in the response metadata always reports what was actually served. A tier sent on the request always takes precedence over that default, within the tiers the plan allows. Rotating to another key for the same provider is a separate upstream account as far as prompt caching is concerned, so a retried request re-writes its cached prefix rather than reading the original one. Send `x-no-fallback: true` to have the original upstream error returned to you — note that this disables cross-provider fallback, not key rotation within a provider. ## Pricing uses multipliers [#pricing-uses-multipliers] Service tiers do not define separate model prices in PassingRight. They multiply the provider mapping's standard token prices: * Standard / `default` / `auto`: 1x * Flex: 0.5x * Priority: model/provider-specific, shown on the model page The multiplier scales per-token costs, including input, output, cached, and image tokens. Flat per-request and web-search fees are not tier-scaled. ## Billing follows the served tier [#billing-follows-the-served-tier] When a provider reports the tier that was actually served, PassingRight bills that returned tier instead of blindly billing the requested value: * A `priority` request that runs as priority is billed at that provider mapping's priority multiplier, shown on the [model page](https://passingright.io/models). * A `flex` request that runs as flex is billed at 0.5x. * A request that is served as standard is billed at the standard 1x rate. The served tier is read back from the provider response — Vertex reports it in `usageMetadata.trafficType` (`ON_DEMAND_PRIORITY` / `ON_DEMAND_FLEX` / `ON_DEMAND`), Google AI Studio reports it in the `x-gemini-service-tier` response header, and OpenAI can return `service_tier` in response payloads or stream events. Providers that report no tier at all (Fireworks) never downgrade silently — they reject the request instead — so an accepted request is billed at the tier it was sent at. PassingRight rejects unsupported tier requests before provider routing. For example, `gemini-3-pro-image` exposes Flex and Priority for Google AI Studio, but only Flex for Vertex. Other mappings expose neither tier. You can see per-tier pricing for each model on its [model page](https://passingright.io/models). Supported provider cards include a Service Tier selector in the card header and show the active multiplier next to each tier. ## Sources [#sources] * [OpenAI API pricing](https://openai.com/api/pricing/) * [Google Flex PayGo](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/flex-paygo) * [Google Priority PayGo](https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/priority-paygo) * [Fireworks Serverless Priority and Fast](https://docs.fireworks.ai/serverless/priority-and-fast) # Sessions URL: https://docs.passingright-staging.sandbloc.com/features/sessions A **session** ties together the requests that belong to the same conversation or workflow. By attaching a stable session identifier to your requests, PassingRight can treat them as a unit — keeping provider routing consistent across turns and letting you trace and filter the whole conversation in the dashboard. Sessions are the foundation for several features. Today they power **sticky provider routing** and **session-level observability**; more session-scoped capabilities will build on the same identifier over time. ## Setting the session id [#setting-the-session-id] For chat completions, the session key is resolved in priority order — the first present value wins: 1. The `x-session-id` header 2. The `x-session-affinity` header (sent automatically by coding agents such as opencode) 3. The `session_id` or `session-id` header (sent by some coding agents alongside `x-session-affinity`) 4. The `prompt_cache_key` body field (OpenAI-compatible) 5. The `user` body field (OpenAI-compatible) ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -H "x-session-id: conversation-9f8e7d6c" \ -d '{ "model": "claude-sonnet-4-6", "messages": [{"role": "user", "content": "Hello!"}] }' ``` Reuse the same session id for every request in a conversation. If you don't set any of the values above, the request simply has no session and behaves exactly as before. ### Anthropic Messages endpoint [#anthropic-messages-endpoint] For the [Anthropic Messages endpoint](https://docs.passingright-staging.sandbloc.com/features/anthropic-endpoint) (`/v1/messages`), the session key is derived automatically from `metadata.user_id`. Coding agents such as Claude Code send a JSON object there (e.g. `{"session_id":"",…}`); the gateway uses its `session_id` field. An explicit `x-session-id` header still takes precedence. ## Sticky provider routing [#sticky-provider-routing] When a model is served by multiple providers, requests are normally scored independently, so a multi-turn conversation can bounce between providers. That defeats provider-side **prompt caching**, which only pays off when consecutive requests with a shared prefix reach the **same** provider. With a session id set, PassingRight scores the session's first request with the normal weighted smart-routing algorithm (price, priority, uptime, throughput) and then **pins that provider for the session**, reusing it on every subsequent request to keep the prompt cache warm. The session stays on that provider — skipping the epsilon-greedy exploration — and only moves when its provider drops below the session uptime threshold or leaves the available pool (health or compatibility filtering), at which point the session is re-scored and re-pinned to the current best provider. Sticky requests skip cross-provider retry/fallback entirely — a failure is retried against the pinned provider only (on another configured key, or the same platform key when only one is configured), and the degraded uptime it records is what triggers re-pinning later. A session pin cannot bypass a [zero global provider limit](https://docs.passingright-staging.sandbloc.com/features/routing#global-provider-rate-limits). Routing can move to an available alternative; if the selected provider remains blocked, the request returns `429`. That first selection uses expected cache-hit rates and output/input proportions even for a short opening prompt: observed project/model usage when sufficiently sampled, otherwise workload defaults. Recognized coding clients receive coding defaults on regular API projects as well as DevPass. See [Routing → Smart Routing Algorithm](https://docs.passingright-staging.sandbloc.com/features/routing#smart-routing-algorithm) for the sample requirements and pricing defaults, and [Sticky Session Routing](https://docs.passingright-staging.sandbloc.com/features/routing#sticky-session-routing) for pinning and fallback behavior. Session stickiness is **on by default**. Enterprise projects can turn it off per project under **Settings → Routing → Session Stickiness**; when disabled, every request is scored independently regardless of session id (the id is still recorded for observability). Sticky routing optimizes for cache locality over per-request price. A session stays on its provider even if a cheaper or faster alternative is momentarily available, since the prompt-cache savings typically outweigh the difference. ## Upstream prompt-cache routing [#upstream-prompt-cache-routing] Some providers use an OpenAI-style `prompt_cache_key` to route requests to the cache shard that already holds your prompt prefix — without it, repeat requests can land on different backends and miss the cache entirely (Meta requires it for cache hits in practice; OpenAI and Azure use it to improve hit rates under load). When a request has a session id and you didn't send a `prompt_cache_key` yourself, PassingRight forwards a **keyed hash** (HMAC-SHA256 with a gateway-side secret) of the session id as the `prompt_cache_key` to providers that support it (currently OpenAI, Azure, AWS Mantle, and Meta — Mantle and Meta always use the Responses API upstream, which is where the key is sent). Hashing means your raw session ids are never exposed to providers; the hash is stable per session, which is all cache routing needs. A `prompt_cache_key` you set explicitly takes precedence on those same surfaces, but it is hashed the same way before being forwarded — providers never see the raw value. On provider surfaces that don't support the field, no key is sent at all — whether derived or explicit. This currently applies to Sakana (the field is not part of its API) and to Azure chat-completions requests, which can be served by legacy deployment-based API versions that reject unknown body fields; Azure requests on the Responses API always carry the key. Providers not listed above use different caching mechanisms (for example Anthropic `cache_control` breakpoints or Google implicit caching), so the `prompt_cache_key` doesn't apply to them either. For Meta, requests without any session id still get a cache key derived from the conversation's first messages, so multi-turn conversations hit Meta's prompt cache even when no session signal is present. ## Observing sessions in the activity log [#observing-sessions-in-the-activity-log] Every request is logged with its resolved session id. In the dashboard **Activity** view you can: * See the **Session ID** on each request's metadata, alongside the request and trace IDs. * **Filter by session id** using the search field next to the custom-metadata search, to pull up every request that belongs to a conversation in one place. This makes it easy to follow a full conversation end-to-end — inspecting how each turn was routed, what it cost, and which provider served it. The session id is distinct from freeform [metadata](https://docs.passingright-staging.sandbloc.com/features/metadata). Use metadata custom headers for arbitrary tags (user, tenant, app version); use the session id for the one value that should keep a conversation pinned and traceable. # Source Attribution URL: https://docs.passingright-staging.sandbloc.com/features/source The `X-Source` header allows you to identify your domain when making requests to PassingRight. This information is used to generate public usage statistics showing how PassingRight is being used across different websites and applications. ## X-Source Header [#x-source-header] Include the `X-Source` header with your domain name in your requests: ```bash curl -X POST https://api.passingright.io/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "X-Source: example.com" \ -d '{ "model": "gpt-4o", "messages": [ { "role": "user", "content": "Hello, how are you?" } ] }' ``` ## Domain Format [#domain-format] The `X-Source` header accepts domain names in various formats. All of the following are valid and will be normalized to the same domain: * `example.com` * `https://example.com` * `https://www.example.com` * `www.example.com` All variations will be stripped down to the base domain (`example.com`) for aggregation purposes. After stripping the protocol and `www.` prefix, the value must contain only alphanumeric characters, hyphens, dots, and slashes — a value with other characters (underscores, spaces, query strings) fails the whole request with a `400`. When no `X-Source` header is sent, the gateway falls back to the `HTTP-Referer` header, and recognizes well-known coding agents from the `User-Agent` header for source attribution. ## Public Statistics [#public-statistics] Data from the `X-Source` header is used to generate public statistics about PassingRight usage, including: * **Popular Domains**: Which websites and applications are using PassingRight most frequently * **Model Usage**: What models are being used by different domains * **Geographic Distribution**: Where requests are coming from across different sources * **Growth Trends**: How usage is growing over time for different domains These statistics help demonstrate the adoption and impact of PassingRight across the ecosystem. ## Privacy Considerations [#privacy-considerations] ### What's Public [#whats-public] * Domain names (stripped of protocol and www prefixes) * Aggregated request counts and model usage * General geographic regions (country-level data) ### What's Private [#whats-private] * Individual request content or responses * User identifiers or personal information * Detailed usage patterns beyond aggregated counts * API keys or authentication details ## Benefits [#benefits] Including the `X-Source` header provides several benefits: ### For Your Project [#for-your-project] * **Recognition**: Your domain will appear in public usage statistics * **Credibility**: Demonstrates real-world usage of your application * **Community**: Contributes to the broader PassingRight ecosystem ### For the Community [#for-the-community] * **Transparency**: Shows real adoption and usage patterns * **Inspiration**: Other developers can see successful implementations * **Growth**: Helps demonstrate the value of open-source LLM infrastructure ## Optional but Recommended [#optional-but-recommended] While the `X-Source` header is optional, we strongly encourage its use to: * Support transparency in the PassingRight ecosystem * Help showcase successful integrations * Contribute to understanding of LLM usage patterns * Demonstrate the real-world impact of your application Your participation helps build a more transparent and collaborative LLM ecosystem. # Speech Generation URL: https://docs.passingright-staging.sandbloc.com/features/speech-generation PassingRight supports text-to-speech (TTS) through the OpenAI-compatible **`/v1/audio/speech`** endpoint, powered by ElevenLabs, Google Gemini, OpenAI, and Alibaba Qwen speech models. Want to hear the voices before writing code? The [Audio Studio](https://chat.passingright.io/audio) in Lounge generates speech from up to three models side by side, with per-model voice, format, and speed controls. ## Available Models [#available-models] Browse all speech generation models, with up-to-date pricing, on the [models page](https://passingright.io/models?filters=1\&audioGeneration=true). Billing varies by model family. Some models are billed on token usage reported by the provider (input text tokens and output audio tokens), while others are billed on input character count (those return audio bytes without usage data). See the [models page](https://passingright.io/models?filters=1\&audioGeneration=true) for each model's exact pricing. ## Parameters [#parameters] | Parameter | Type | Default | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `model` | string | required | The speech model to use | | `input` | string | required | The text to synthesize into speech | | `voice` | string | model | A prebuilt voice. Defaults to `Kore` (Gemini), `alloy` (OpenAI), `Sarah` (ElevenLabs), or the model's first voice on Qwen (`longanlingxin` on Plus, `longanhuan_v3.6` on Flash) | | `response_format` | string | model | Audio format. OpenAI: `mp3` (default), `opus`, `aac`, `flac`, `wav`, `pcm`. ElevenLabs: `mp3` (default), `wav`, `pcm`, `opus`. Gemini: `wav` (default), `pcm`. Qwen: `wav` | | `instructions` | string | — | Optional style/delivery directive prepended to the input (e.g. `"Say cheerfully"`) | | `speed` | number | — | Accepted for OpenAI compatibility, but not applied by Gemini speech models | Gemini speech models return raw PCM audio. PassingRight wraps it in a WAV container by default (`response_format: "wav"`), or returns the raw 16-bit little-endian PCM at 24 kHz when `response_format: "pcm"` is requested. Other formats such as `mp3` are only available on the OpenAI models, which return the audio already encoded in the requested format. ## curl [#curl] ```bash curl -X POST "https://api.passingright.io/v1/audio/speech" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gemini-2.5-flash-preview-tts", "input": "Hello, welcome to PassingRight!", "voice": "Kore" }' \ --output speech.wav ``` ## OpenAI SDK [#openai-sdk] Works with the standard OpenAI client library — just point the base URL to PassingRight. ```ts import OpenAI from "openai"; import { writeFileSync } from "fs"; const openai = new OpenAI({ apiKey: process.env.LLM_GATEWAY_API_KEY, baseURL: "https://api.passingright.io/v1", }); const response = await openai.audio.speech.create({ model: "gemini-2.5-flash-preview-tts", voice: "Kore", input: "Hello, welcome to PassingRight!", }); const buffer = Buffer.from(await response.arrayBuffer()); writeFileSync("speech.wav", buffer); ``` ## Streaming [#streaming] Streaming speech responses (chunked audio or `stream_format: "sse"`) are not supported yet. The endpoint always returns the complete audio file in a single response, so there is no low-latency, play-as-you-go output for now. ## Voices [#voices] Gemini exposes 30 prebuilt voices. A few common ones: `Kore`, `Puck`, `Zephyr`, `Charon`, `Fenrir`, `Leda`, `Orus`, `Aoede`. When `voice` is omitted on a Gemini model, `Kore` is used. OpenAI voices include `alloy`, `ash`, `ballad`, `coral`, `echo`, `fable`, `nova`, `onyx`, `sage`, `shimmer`, and `verse`. When `voice` is omitted on an OpenAI model, `alloy` is used. ElevenLabs models accept 20 named voices, including `Sarah`, `Aria`, `Roger`, `Laura`, `Charlie`, `George`, `Charlotte`, `Jessica`, `Brian`, and `Lily`. When `voice` is omitted on an ElevenLabs model, `Sarah` is used. A raw ElevenLabs voice id is also accepted directly. Qwen-Audio-3.0-TTS voices are model-specific and cannot be mixed between models: `longanlingxin` and `longanlufeng` on Plus (default `longanlingxin`), and `longanhuan_v3.6`, `longjielidou_v3.6`, `loongeva_v3.6`, and `loongjohn` on Flash (default `longanhuan_v3.6`). ## ElevenLabs [#elevenlabs] The four ElevenLabs models are billed per **input character** (see the [models page](https://passingright.io/models?filters=1\&audioGeneration=true) for rates): * `eleven-multilingual-v2` — most lifelike, rich emotional expression, 29 languages * `eleven-v3` — most expressive and human-like, 70+ languages * `eleven-flash-v2-5` — ultra-low latency, 32 languages * `eleven-turbo-v2-5` — fast and balanced, 32 languages ```bash curl -X POST "https://api.passingright.io/v1/audio/speech" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "eleven-multilingual-v2", "input": "Hello, welcome to PassingRight!", "voice": "Sarah" }' \ --output speech.mp3 ``` # System One URL: https://docs.passingright-staging.sandbloc.com/features/system-one PassingRight exposes a `/v1/systemone` endpoint for **System One** models: models that read a state, answer questions you name, and return typed decisions with calibrated probabilities. There is no free-form text in the response — every answer is a value your code can branch on directly. Use it when a model call exists only to make a decision: routing a support ticket, scoring a retrieved passage, checking whether a citation supports a claim, or classifying a record. A chat model can do these too, but you then have to parse prose, and you get no probability to threshold on. Browse available decision models on the [models page](https://passingright.io/models?filters=1). For the full request and response schema, see the [API reference](https://docs.passingright-staging.sandbloc.com/v1_systemone). ## Endpoint [#endpoint] `POST https://api.passingright.io/v1/systemone` ## Question types [#question-types] Every question has a `type`, `instructions`, and — for choice and score — its own `criteria`. Answers come back under the ids you chose. | Type | Ask | Answer | | -------- | -------------------------------- | -------------------------------------------------------------------------- | | `noul` | A yes/no question | `noul`: probability the answer is yes, 0 to 1 | | `choice` | One option from a set you define | `choice`, `probabilities` per option, `confidence` | | `score` | A rating across ordered levels | `score` (can land between levels), `probabilities`, `legend`, `confidence` | ## cURL [#curl] ```bash curl -X POST "https://api.passingright.io/v1/systemone" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "jev-1.13.0", "state": "Help! My payouts have been failing for 3 days.", "questions": { "department": { "type": "choice", "instructions": "Which team should handle this?", "criteria": { "billing": "Payments, invoicing, refunds", "technical": "Bugs, outages, integrations", "sales": "Pricing, upgrades, new accounts" } }, "is_urgent": { "type": "noul", "instructions": "Does this convey urgency?" }, "impact": { "type": "score", "instructions": "Rate the operational impact.", "criteria": ["None", "Limited", "Critical"] } } }' ``` ```json { "model": "typesafe/jev-1.13.0", "answers": { "department": { "type": "choice", "choice": "billing", "probabilities": { "billing": 0.88, "technical": 0.12, "sales": 0.0 }, "confidence": 0.81 }, "is_urgent": { "type": "noul", "noul": 0.95 }, "impact": { "type": "score", "score": 1.9, "legend": { "0": "None", "1": "Limited", "2": "Critical" }, "probabilities": { "0": 0.0, "1": 0.1, "2": 0.9 }, "confidence": 0.88 } }, "usage": { "input_tokens": 318, "output_tokens": 34 } } ``` The `model` field reports the pinned model that answered, prefixed with the provider it was served by. Provider aliases that move between releases (for example `jev-latest`) are accepted and resolve to the pinned version, so a request is always billed and logged against the version you can read prices for. ## Request fields [#request-fields] | Field | Type | Description | | ----------- | --------------------------- | -------------------------------------------------------------------------- | | `model` | `string` | Decision model to use. Optionally prefixed with a provider (`typesafe/…`). | | `state` | `string \| object \| array` | The content to evaluate. Structured data lets questions reference fields. | | `questions` | `map` | At least one typed question, keyed by ids you choose. | ## Deciding in code [#deciding-in-code] The point of a typed answer is that the policy stays in your code, not in a prompt. Threshold the probability, and use `confidence` as a second axis to send uncertain cases to a human instead of acting on a coin flip: ```ts const res = await fetch("https://api.passingright.io/v1/systemone", { method: "POST", headers: { Authorization: `Bearer ${process.env.LLM_GATEWAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: "jev-1.13.0", state: ticket, questions: { department: { type: "choice", instructions: "Which team should handle this?", criteria: { billing: null, technical: null, sales: null }, }, is_urgent: { type: "noul", instructions: "Does this convey urgency?" }, }, }), }); const { answers } = await res.json(); if (answers.department.confidence < 0.7) { await queueForTriage(ticket); } else { await route(ticket, answers.department.choice, { priority: answers.is_urgent.noul > 0.8 ? "high" : "normal", }); } ``` Ask everything you might need in one request: the state is read once and every question is evaluated against it, so a batch of questions costs far less than one request each — including speculative questions you only read when another answer makes them relevant. Decision models are billed on input tokens only — the state plus every question. Output tokens are free, since the response is a set of values rather than generated text. Question text counts as input on every call, so a large rubric has a real per-request cost. Decision models only work on `/v1/systemone`. Requesting one on `/v1/chat/completions` returns a 400 pointing you at the right endpoint, they cannot generate text or call tools, and they are not available in the playground. Input is text only. # Request Timeouts URL: https://docs.passingright-staging.sandbloc.com/features/timeouts The gateway enforces hard time limits on every request. These protect the platform from stuck upstream connections, but they matter to you directly if you run long generations or agentic pipelines: a single completion that runs longer than the limit is terminated. | Limit | Default | Applies to | | ----------------- | ---------- | -------------------------------------------------------- | | **Streaming** | 20 minutes | Streaming completions (`stream: true`) | | **Non-streaming** | 10 minutes | Non-streaming completions | | **End-to-end** | 25 minutes | Overall ceiling for the whole request, including retries | ## How the limits behave [#how-the-limits-behave] * **They are total-duration limits, not idle limits.** The timer starts when the gateway opens the upstream provider request and keeps running for the entire response — a stream is cut at the limit even while it is actively producing tokens. * **They apply per request, not per session or conversation.** Every completion call gets a fresh window. A long-running agent that makes many calls over hours is unaffected; only a *single* call exceeding the limit fails. * **Tool execution doesn't count.** When a completion finishes with `tool_calls`, the HTTP request ends. The time your application spends running tools (or coordinating subagents) between requests never consumes the window. * **On timeout** the gateway returns a `504` with type `timeout_error` (see [Error Handling](https://docs.passingright-staging.sandbloc.com/resources/error-handling)). If the limit is hit mid-stream, the stream terminates. ## Long-running agentic workloads [#long-running-agentic-workloads] Coordinator/subagent architectures often hold a "master" completion open while work happens elsewhere. To stay within the limits: * **Keep any single completion under 20 minutes.** Structure the coordinator as a loop of discrete completions (the standard tool-calling pattern) rather than one long-lived request that spans the whole investigation. * **Stream long generations.** Non-streaming requests are capped at 10 minutes; streaming raises the per-request budget to 20 minutes and delivers partial output as it is produced. * **Split very long outputs.** If a single generation legitimately needs more than 20 minutes (very large outputs on slow models, extensive reasoning), break it into continuation requests. ## Changing the limits [#changing-the-limits] **Per-project overrides (Enterprise)** can be set under **Project Settings → Routing** — see [Per-Project Routing Configuration](https://docs.passingright-staging.sandbloc.com/features/routing#per-project-routing-configuration-enterprise). Overrides can only *lower* the timeouts: the defaults above are the infrastructure ceiling on the hosted platform and cannot be raised per project. **Self-hosted deployments** control the limits with environment variables on the gateway service: | Variable | Default | Controls | | ------------------------- | --------- | ------------------------- | | `AI_STREAMING_TIMEOUT_MS` | `1200000` | Streaming completions | | `AI_TIMEOUT_MS` | `600000` | Non-streaming completions | | `GATEWAY_TIMEOUT_MS` | `1500000` | End-to-end ceiling | When raising the limits on a self-hosted deployment, raise the surrounding infrastructure in lockstep: your load balancer's backend/response timeout and the gateway's shutdown grace period (`SHUTDOWN_GRACE_PERIOD_MS`, and e.g. Kubernetes `terminationGracePeriodSeconds`) must all be at least as long as the longest stream you allow, or rollouts and intermediaries will still cut long requests. If your workload genuinely needs single completions longer than 20 minutes on the hosted platform, contact us at [support@passingright.io](mailto:support@passingright.io) — the ceiling is an infrastructure setting, not a per-model constraint. # Transcription URL: https://docs.passingright-staging.sandbloc.com/features/transcription PassingRight exposes a dedicated `/v1/audio/transcriptions` endpoint for speech-to-text. It transcribes audio files into text with word-level timestamps, optional speaker diarization, and inverse text normalization (spoken numbers and currencies formatted in their written form). Use it when you want to: * Turn recordings, voicemails, or podcast episodes into text * Generate captions or searchable transcripts with per-word timing * Feed spoken content into a downstream model or RAG pipeline For the full request and response schema, see the [API reference](https://docs.passingright-staging.sandbloc.com/v1_audio_transcriptions). ## Endpoint [#endpoint] `POST https://api.passingright.io/v1/audio/transcriptions` Authenticate with your PassingRight API key: ```bash -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" ``` The current model is `grok-stt-1-0`, billed at **$0.10 per hour** of input audio (against the duration reported by the provider). ## Parameters [#parameters] The request body is `multipart/form-data`. Either `file` or `url` must be provided. | Parameter | Type | Default | Description | | -------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------- | | `model` | string | required | The transcription model to use | | `file` | file | — | The audio file to transcribe (WAV, MP3, OGG, Opus, FLAC, AAC, MP4, M4A, and more) | | `url` | string | — | URL of an audio file to download and transcribe instead of uploading one | | `language` | string | — | Language code (e.g. `en`). When set, enables formatting of numbers and currencies into their written form | | `diarize` | string | `false` | When `"true"`, each word in the response includes a `speaker` field identifying the detected speaker | | `filler_words` | string | `false` | When `"true"`, filler words (e.g. "uh", "um") are kept in the transcript instead of being removed | | `keyterm` | string | — | A key term to bias transcription toward (e.g. product names). Repeat the field for multiple terms | ## Response [#response] The response includes the full transcript, audio duration, and word-level timestamps: ```json { "text": "The balance is $167,983.15.", "language": "English", "duration": 3.45, "words": [ { "text": "The", "start": 0.24, "end": 0.48 }, { "text": "balance", "start": 0.48, "end": 0.96 }, { "text": "is", "start": 0.96, "end": 1.12 }, { "text": "$167,983.15.", "start": 1.12, "end": 3.2 } ] } ``` ## curl [#curl] ```bash curl -X POST "https://api.passingright.io/v1/audio/transcriptions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -F model=grok-stt-1-0 \ -F language=en \ -F file=@audio.mp3 ``` ### Transcribing from a URL [#transcribing-from-a-url] ```bash curl -X POST "https://api.passingright.io/v1/audio/transcriptions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -F model=grok-stt-1-0 \ -F url="https://example.com/audio.mp3" ``` # Video Generation URL: https://docs.passingright-staging.sandbloc.com/features/video-generation PassingRight supports asynchronous video generation through an OpenAI-compatible `POST /v1/videos` flow. Currently available models: * **Veo 3.1** through `google-vertex` (720p, 1080p, 4k) * **Seedance 2.5** through `bytedance` (480p, 720p, 1080p; clips up to 30 seconds) * **Seedance 2.0** and **Seedance 1.5 Pro** through `bytedance` (720p, 1080p), **Seedance 2.0 Fast** through `bytedance` (720p only) * **Seedance 2.0 Mini** through `bytedance` (480p, 720p) * **KLING v3.0** and **KLING v3.0 Turbo** through `atlascloud` (720p, 1080p; KLING v3.0 also 4k) * **MiniMax H3 Max** through `minimax` (480p, 768p; clips from 5 to 15 seconds, always with audio) You can find the current list of video-capable models on our [models page with the video filter enabled](https://passingright.io/models?filters=1\&videoGeneration=true) or programmatically through the [/v1/models endpoint](https://docs.passingright-staging.sandbloc.com/v1_models). ## What Works Today [#what-works-today] * `POST /v1/videos` * `GET /v1/videos/{video_id}` * `GET /v1/videos/{video_id}/content` * Optional signed callbacks with `callback_url` and `callback_secret` ## Request Format [#request-format] PassingRight currently supports a focused subset of the OpenAI video API. ### Supported fields [#supported-fields] | Field | Type | Required | Description | | ------------------ | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------- | | `model` | string | yes | Any video-capable model from the filtered models page | | `prompt` | string | yes | Text prompt for the video | | `seconds` | number | yes | Duration in seconds. Supported values depend on the model (see below) | | `size` | string | no | `widthxheight`, limited to the sizes supported by the selected model and provider | | `audio` | boolean | no | Whether to include audio in the output (default `true`). Only honored when the model supports both audio and silent output | | `image` | object | no | Optional first frame for image-to-video generation (see first/last frame inputs below) | | `last_frame` | object | no | Optional ending frame when `image` is provided (see first/last frame inputs below) | | `reference_images` | array | no | One to three provider-specific image inputs | | `input_reference` | object | no | Alias for one or more `reference_images` | | `reference_videos` | array | no | One to three reference video HTTPS URLs (Seedance 2.x only, see below) | | `reference_audios` | array | no | One to three reference audio HTTPS URLs (Seedance 2.x only, see below) | | `callback_url` | string | no | PassingRight extension for completion webhooks | | `callback_secret` | string | no | PassingRight extension used to sign webhook deliveries | ### Sizes and durations by model [#sizes-and-durations-by-model] | Model family | Provider | Supported sizes | Supported durations | | ----------------- | --------------- | --------------------------------------------------------------------------------- | ------------------- | | Veo 3.1 | `google-vertex` | `1280x720`, `720x1280`, `1920x1080`, `1080x1920`, `3840x2160`, `2160x3840` | `4`, `6`, `8`, `10` | | Seedance 2.5 | `bytedance` | `848x480`, `854x480`, `480x854`, `1280x720`, `720x1280`, `1920x1080`, `1080x1920` | `4`–`30` | | Seedance 2.0 | `bytedance` | `1280x720`, `720x1280`, `1920x1080`, `1080x1920` | `4`–`15` | | Seedance 2.0 Fast | `bytedance` | `1280x720`, `720x1280` | `4`–`15` | | Seedance 2.0 Mini | `bytedance` | `1280x720`, `720x1280`, `848x480`, `854x480`, `480x854` | `4`–`15` | | Seedance 1.5 Pro | `bytedance` | `1280x720`, `720x1280`, `1920x1080`, `1080x1920` | `5`, `10` | | KLING v3.0 | `atlascloud` | `1280x720`, `720x1280`, `1920x1080`, `1080x1920`, `3840x2160`, `2160x3840` | `5`, `10` | | KLING v3.0 Turbo | `atlascloud` | `1280x720`, `720x1280`, `1920x1080`, `1080x1920` | `5`, `10` | | MiniMax H3 Max | `minimax` | `848x480`, `854x480`, `480x854`, `1366x768`, `768x1366` | `5`–`15` | Requests return `400` when the selected provider cannot serve the requested `size` or `seconds`. Seedance and KLING v3.0 derive `aspect_ratio` from the requested `size` (16:9 for landscape, 9:16 for portrait). **KLING v3.0 Turbo** and **MiniMax H3 Max** always generate audio and do not support `audio: false`; silent requests return a `400`. Use **KLING v3.0** (`kling-v3-0`) when you need silent output. ### First/last frame inputs [#firstlast-frame-inputs] Frame inputs interpolate a video between a starting frame and an optional ending frame. You provide the first frame as `image` and, optionally, the ending frame as `last_frame`. The gateway tags each one with the correct role for the provider, so you don't set roles yourself. | Field | Required | Accepted input | Available on | | ------------ | ------------------ | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `image` | for frame mode | HTTPS URL **or** base64 data URL | **Seedance 2.x** (`bytedance`), **KLING v3.0 / Turbo** (`atlascloud`), Veo 3.1 (`google-vertex`), `minimax`, `xai` | | `last_frame` | no (needs `image`) | HTTPS URL **or** base64 data URL | **Seedance 2.x** (`bytedance`), **KLING v3.0 / Turbo** (`atlascloud`), Veo 3.1 (`google-vertex`), **MiniMax H3 Max** (`minimax`) | #### Rules and limits [#rules-and-limits] * **Seedance scope.** Frame inputs are supported on **Seedance 2.5**, **Seedance 2.0**, **Seedance 2.0 Fast**, and **Seedance 2.0 Mini** (`seedance-2-5`, `seedance-2-0`, `seedance-2-0-fast`, `seedance-2-0-mini`). Sending `image`/`last_frame` to Seedance 1.5 Pro or any other ByteDance model returns a `400`. * **KLING scope.** Frame inputs are supported on **KLING v3.0** and **KLING v3.0 Turbo** (`kling-v3-0`, `kling-v3-0-turbo`). The gateway uploads base64 frames to AtlasCloud's media endpoint automatically, so both HTTPS URLs and base64 data URLs are accepted. * **`last_frame` requires `image`.** Providing `last_frame` without `image` returns a `400`. * **Not combinable with references.** First/last frame inputs (`image`, `last_frame`) cannot be combined with reference inputs (`reference_images`, `input_reference`, `reference_videos`, `reference_audios`). #### Example (Seedance 2.0) [#example-seedance-20] ```bash curl -X POST "https://api.passingright.io/v1/videos" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "seedance-2-0", "prompt": "Morph smoothly from the first frame into the last frame", "seconds": 5, "size": "1280x720", "image": { "image_url": "https://example.com/first-frame.png" }, "last_frame": { "image_url": "https://example.com/last-frame.png" } }' ``` #### Example (KLING v3.0 image-to-video) [#example-kling-v30-image-to-video] ```bash curl -X POST "https://api.passingright.io/v1/videos" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "kling-v3-0", "prompt": "Animate the scene with gentle camera motion", "seconds": 5, "size": "1280x720", "image": { "image_url": "https://example.com/first-frame.png" } }' ``` ### Reference-guided generation (Seedance 2.x) [#reference-guided-generation-seedance-2x] Seedance 2.x (`seedance-2-5`, `seedance-2-0`, `seedance-2-0-fast`, `seedance-2-0-mini`) can generate a video that is guided by reference **images**, **videos**, and **audio** — sometimes called omni-reference. You attach references as top-level fields in the same `POST /v1/videos` payload; the gateway forwards each one to the provider tagged with the correct role, so you don't set roles yourself. | Reference type | Payload field | Count | Accepted input | Available on | | -------------- | -------------------------------------------- | ----- | -------------------------------- | --------------------------------------- | | Image | `reference_images` (`input_reference` alias) | 1–3 | HTTPS URL **or** base64 data URL | Seedance 2.x, Veo 3.1 (`google-vertex`) | | Video | `reference_videos` | 1–3 | HTTPS URL only | Seedance 2.x | | Audio | `reference_audios` | 1–3 | HTTPS URL only | Seedance 2.x | Each list item accepts either a bare URL string or an object form: * `reference_images`: `"https://…/subject.png"` or `{ "image_url": "https://…/subject.png" }` * `reference_videos`: `"https://…/motion.mp4"` or `{ "video_url": "https://…/motion.mp4" }` * `reference_audios`: `"https://…/track.mp3"` or `{ "audio_url": "https://…/track.mp3" }` You can mix all three reference types in one request. The `prompt` can be a light instruction (for example `"adapt this to show more detail"`) — the references drive the result. #### Rules and limits [#rules-and-limits-1] * **HTTPS only for video and audio.** `reference_videos` and `reference_audios` must be publicly reachable HTTPS URLs (the provider fetches them). base64 data URLs are rejected for video/audio; images may be HTTPS URLs or base64 data URLs. * **Reference video resolution.** Seedance requires reference video frames to be at least \~409,600 pixels (roughly 480p or larger). Low-resolution clips such as 360p are rejected with a `400`. * **Not combinable with frames.** Reference inputs (`reference_images`, `reference_videos`, `reference_audios`) cannot be combined with the first/last frame inputs (`image`, `last_frame`). * **Provider scope.** Reference videos and audio are only supported on Seedance 2.x models; sending them to other models returns a `400`. **KLING v3.0** does not support any reference inputs (`reference_images`, `reference_videos`, `reference_audios`); use first/last frame inputs instead. * **Moderation still applies.** The output is subject to the provider's content moderation. Blocked generations finish as `failed` and are logged with a `content_filter` finish reason. The [gateway content filter](https://docs.passingright-staging.sandbloc.com/resources/error-handling#gateway-content-filter) may also reject the request with a `403` before a job is created. #### Examples [#examples] Reference images only (subjects / style): ```bash curl -X POST "https://api.passingright.io/v1/videos" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "seedance-2-0", "prompt": "The subject walks through a neon-lit market at night", "seconds": 5, "size": "1280x720", "reference_images": [ { "image_url": "https://example.com/subject.png" }, { "image_url": "https://example.com/style.png" } ] }' ``` Reference video only (motion / scene — let the clip drive the output): ```bash curl -X POST "https://api.passingright.io/v1/videos" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "seedance-2-0", "prompt": "adapt this to show more detail", "seconds": 5, "size": "1280x720", "reference_videos": ["https://example.com/reference-motion.mp4"] }' ``` All three reference types combined: ```bash curl -X POST "https://api.passingright.io/v1/videos" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "seedance-2-0", "prompt": "The subject performs the choreography from the reference video", "seconds": 5, "size": "1280x720", "reference_images": [ { "image_url": "https://example.com/subject.png" } ], "reference_videos": [ "https://example.com/reference-motion.mp4" ], "reference_audios": [ "https://example.com/reference-track.mp3" ] }' ``` ### Not supported yet [#not-supported-yet] * multipart uploads * `n` values other than `1` * remix/list/delete video endpoints ## Create a Video [#create-a-video] Credits-billed video jobs are gated on their estimated cost before anything is submitted upstream. The estimate is the model's per-second rate for the requested resolution (the audio-inclusive rate when the model prices audio separately) times the duration, plus any input-image price. Your organization must hold at least that amount, and never less than `$1.00`, beyond what its still-running video jobs already reserve; otherwise the request fails with `402`. The estimate is reserved when the job is created and settled to the actual cost when the job finishes, so a burst of submissions cannot spend more than the balance covers. Pricing is per second of generated video. For Seedance and KLING v3.0, enabling audio can increase the per-second rate on models that price audio and video separately. Veo 3.1: | Model | Provider | Supported sizes | Price | | ------------------------------- | --------------- | ------------------------------------------------ | ---------------- | | `veo-3.1-generate-preview` | `google-vertex` | `1280x720`, `720x1280`, `1920x1080`, `1080x1920` | `$0.40 / second` | | `veo-3.1-fast-generate-preview` | `google-vertex` | `1280x720`, `720x1280`, `1920x1080`, `1080x1920` | `$0.15 / second` | | `veo-3.1-generate-preview` | `google-vertex` | `3840x2160`, `2160x3840` | `$0.60 / second` | | `veo-3.1-fast-generate-preview` | `google-vertex` | `3840x2160`, `2160x3840` | `$0.35 / second` | Seedance (ByteDance): | Model | Provider | Resolution | With audio | Video only | | ------------------- | ----------- | ---------- | ------------------- | ------------------- | | `seedance-2-5` | `bytedance` | 480p | `$0.1028 / second` | `$0.1028 / second` | | `seedance-2-5` | `bytedance` | 720p | `$0.2311 / second` | `$0.2311 / second` | | `seedance-2-5` | `bytedance` | 1080p | `$0.52 / second` | `$0.52 / second` | | `seedance-2-0` | `bytedance` | 720p | `$0.1512 / second` | `$0.1512 / second` | | `seedance-2-0` | `bytedance` | 1080p | `$0.3402 / second` | `$0.3402 / second` | | `seedance-2-0-fast` | `bytedance` | 720p | `$0.121 / second` | `$0.121 / second` | | `seedance-2-0-mini` | `bytedance` | 480p | `$0.0378 / second` | `$0.0378 / second` | | `seedance-2-0-mini` | `bytedance` | 720p | `$0.0756 / second` | `$0.0756 / second` | | `seedance-1-5-pro` | `bytedance` | 720p | `$0.05184 / second` | `$0.02592 / second` | | `seedance-1-5-pro` | `bytedance` | 1080p | `$0.1166 / second` | `$0.05832 / second` | KLING (AtlasCloud): | Model | Provider | Resolution | With audio | Video only | | ------------------ | ------------ | ---------- | ----------------- | ----------------- | | `kling-v3-0` | `atlascloud` | 720p | `$0.126 / second` | `$0.084 / second` | | `kling-v3-0` | `atlascloud` | 1080p | `$0.168 / second` | `$0.112 / second` | | `kling-v3-0` | `atlascloud` | 4k | `$0.42 / second` | `$0.42 / second` | | `kling-v3-0-turbo` | `atlascloud` | 720p | `$0.168 / second` | n/a (audio only) | | `kling-v3-0-turbo` | `atlascloud` | 1080p | `$0.21 / second` | n/a (audio only) | ```bash curl -X POST "https://api.passingright.io/v1/videos" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "veo-3.1-generate-preview", "prompt": "A cinematic aerial shot flying above a rainforest waterfall at sunrise", "seconds": 8, "size": "1920x1080" }' ``` Example response: ```json { "id": "v_123", "object": "video", "model": "veo-3.1-generate-preview", "status": "queued", "progress": 0, "created_at": 1773600000, "completed_at": null, "expires_at": null, "error": null } ``` ## Retrieve Job Status [#retrieve-job-status] ```bash curl "https://api.passingright.io/v1/videos/v_123" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" ``` Typical statuses: * `queued` * `in_progress` * `completed` * `failed` * `canceled` * `expired` Once a job reaches a terminal status and has been billed, the response (and the signed callback payload) includes its cost in USD: ```json "usage": { "cost": 0.4, "cost_details": { "video_output_cost": 0.4, "image_input_cost": 0 } } ``` Failed, canceled, and expired jobs report a cost of `0`. `google-vertex` follows Vertex AI's long-running operation flow. The gateway submits Veo generation with `predictLongRunning`, polls with `fetchPredictOperation`, and streams the final bytes through the gateway content endpoint once the operation is done. `bytedance` uses the ModelArk `/contents/generations/tasks` endpoint. The gateway submits the job, polls the upstream task status, and exposes the final video bytes through the gateway content endpoint once the task succeeds. `atlascloud` uses the AtlasCloud `/api/v1/model/generateVideo` endpoint and polls `/api/v1/model/prediction/{id}` for status. The gateway resolves the upstream KLING variant (standard, turbo, or 4k) and task type (text-to-video or image-to-video) from your request, then streams the final video bytes through the gateway content endpoint once the prediction completes. ## Download the Video [#download-the-video] Once the job is complete, stream the resulting video bytes from the content endpoint: ```bash curl "https://api.passingright.io/v1/videos/v_123/content" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ --output video.mp4 ``` ## Signed Callbacks [#signed-callbacks] PassingRight can notify your application when the job reaches a terminal state. ```bash curl -X POST "https://api.passingright.io/v1/videos" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "veo-3.1-fast-generate-preview", "prompt": "A slow-motion close-up of waves crashing against black volcanic rock", "seconds": 8, "callback_url": "https://example.com/webhooks/video", "callback_secret": "whsec_your_secret_here" }' ``` ### Delivery behavior [#delivery-behavior] * Callbacks are sent only for terminal states in v1 * Event types are `video.completed` and `video.failed` * Deliveries retry with exponential backoff on network errors, timeouts, and non-2xx responses * Each attempt is recorded internally in the webhook delivery log table ### Headers [#headers] * `webhook-id` * `webhook-timestamp` * `webhook-signature` ### Signature format [#signature-format] PassingRight signs the string: ```text {webhook-id}.{webhook-timestamp}.{raw-request-body} ``` using HMAC-SHA256 with your `callback_secret`, then sends: ```text webhook-signature: v1,{base64_signature} ``` ### Verification example [#verification-example] ```ts import { createHmac, timingSafeEqual } from "node:crypto"; function verifyWebhook( body: string, webhookId: string, webhookTimestamp: string, webhookSignature: string, secret: string, ) { const expected = createHmac("sha256", secret) .update(`${webhookId}.${webhookTimestamp}.${body}`) .digest("base64"); const provided = webhookSignature.replace(/^v1,/, ""); return timingSafeEqual(Buffer.from(expected), Buffer.from(provided)); } ``` ## Related Docs [#related-docs] * [Image Generation](https://docs.passingright-staging.sandbloc.com/features/image-generation) * [Routing](https://docs.passingright-staging.sandbloc.com/features/routing) * [Models API](https://docs.passingright-staging.sandbloc.com/v1_models) ### Playback and seeking [#playback-and-seeking] Video content endpoints forward `Range` and `If-Range` requests to the upstream storage service and preserve partial-content responses. Inline video results also support single byte ranges. A satisfiable range returns `206` with `Content-Range`; an unsatisfiable range returns `416`. This lets native players load and seek without downloading the entire video first when the upstream supports ranges. # Vision Support URL: https://docs.passingright-staging.sandbloc.com/features/vision PassingRight supports vision-enabled models that can analyze and describe images. You can provide images via HTTPS URLs or inline base64-encoded data. ## Vision-Enabled Models [#vision-enabled-models] You can find all vision-enabled models on our [models page with vision filter](https://passingright.io/models?filters=1\&vision=true). These models can process both text and image content in the same request. ## Image Formats [#image-formats] ### Using HTTPS URLs [#using-https-urls] You can provide any publicly accessible HTTPS URL pointing to an image: ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "What do you see in this image?" }, { "type": "image_url", "image_url": { "url": "https://example.com/image.jpg" } } ] } ] }' ``` ### Using Base64 Inline Data [#using-base64-inline-data] You can also provide images as base64-encoded data URIs: ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image" }, { "type": "image_url", "image_url": { "url": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD..." } } ] } ] }' ``` ## Content Array Format [#content-array-format] When using vision models, the `content` field should be an array containing both text and image content blocks: * **Text content**: `{"type": "text", "text": "Your message"}` * **Image content**: `{"type": "image_url", "image_url": {"url": "image_url_or_data_uri"}}` ## Multiple Images [#multiple-images] You can include multiple images in a single request: ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Compare these two images" }, { "type": "image_url", "image_url": { "url": "https://example.com/image1.jpg" } }, { "type": "image_url", "image_url": { "url": "https://example.com/image2.jpg" } } ] } ] }' ``` ## Simple String Content [#simple-string-content] For vision models, you can still use simple string content for text-only messages. The array format is only required when including images. ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o", "messages": [ { "role": "user", "content": "Hello! How can you help me today?" } ] }' ``` ## Supported Image Types [#supported-image-types] Vision models typically support common image formats including: * JPEG (.jpg, .jpeg) * PNG (.png) * WebP (.webp) * GIF (.gif) The specific formats supported may vary by model provider. Check the individual model documentation for format limitations and file size restrictions. ## Error Handling [#error-handling] If an image URL is inaccessible or the image format is unsupported, the gateway will handle the error gracefully and may substitute a placeholder or error message in the request to the underlying model. # Native Web Search URL: https://docs.passingright-staging.sandbloc.com/features/web-search PassingRight supports native web search capabilities that allow models to access real-time information from the internet. This feature is useful for answering questions about current events, recent news, live data, and other time-sensitive information that may not be in the model's training data. ## How It Works [#how-it-works] When you include the `web_search` tool in your request, the model can search the web to gather relevant information before generating a response: 1. You send a request with the `web_search` tool enabled 2. The model determines if web search is needed based on the query, unless you [require one](#requiring-a-search) 3. If needed, the model performs web searches to gather current information 4. The model synthesizes the search results and generates a response 5. Citations are included in the response to show information sources ## Supported Providers [#supported-providers] Native web search is available on select models. See all models with native web search support on our [models page](https://passingright.io/models?filters=1\&webSearch=true). **Perplexity Sonar changes on September 25, 2026.** Perplexity retires its Sonar chat completions API on September 27. On September 25, `perplexity/sonar` moves to Perplexity's Agent API: the model id, the request shape and the response fields stay the same, and the flat per-request fee is replaced by per-search billing. `perplexity/sonar-pro` and `perplexity/sonar-reasoning-pro` have no equivalent on that API and stop being routable on September 27. Read the [full announcement](https://passingright.io/blog/perplexity-sonar-api-retirement). ## Basic Usage [#basic-usage] To enable web search, add the `web_search` tool to your request: ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-5.2", "messages": [ { "role": "user", "content": "What is the current weather in San Francisco?" } ], "tools": [ { "type": "web_search" } ] }' ``` ### Example Response [#example-response] ```json { "id": "chatcmpl-abc123", "object": "chat.completion", "created": 1234567890, "model": "openai/gpt-5.2", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "The current weather in San Francisco is 57°F (14°C) with mostly cloudy skies...", "annotations": [ { "type": "url_citation", "url": "https://weather.com/...", "title": "San Francisco Weather" } ] }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 15, "completion_tokens": 150, "total_tokens": 165, "cost": 0.0315 } } ``` ## Web Search Options [#web-search-options] The `web_search` tool accepts optional configuration parameters: ### User Location [#user-location] Provide location context to get more relevant local search results: ```json { "type": "web_search", "user_location": { "city": "San Francisco", "region": "California", "country": "US", "timezone": "America/Los_Angeles" } } ``` ### Search Context Size [#search-context-size] Control the amount of web content retrieved (OpenAI only): ```json { "type": "web_search", "search_context_size": "medium" } ``` Available values: * `low` - Minimal search context, faster responses * `medium` - Balanced context (default) * `high` - Maximum search context, more comprehensive ### Max Uses [#max-uses] Limit the number of searches per request (provider-dependent): ```json { "type": "web_search", "max_uses": 3 } ``` ### Domain Filters [#domain-filters] Restrict which domains the model may search with `allowed_domains`, or exclude domains with `blocked_domains`. Both fields are accepted independently; provider support varies — currently the filters reach Anthropic-served requests, and other providers ignore them. Anthropic accepts only one of the two, so when both are set the gateway forwards `allowed_domains` and drops `blocked_domains`: ```json { "type": "web_search", "allowed_domains": ["example.com"] } ``` ### Shorthand: `web_search: true` [#shorthand-web_search-true] As a shortcut, set the top-level `web_search` body field to `true` instead of adding the tool — the gateway injects a default `web_search` tool for you (it has no effect if the tool is already present): ```json { "model": "gpt-5.2", "messages": [{ "role": "user", "content": "What happened in tech today?" }], "web_search": true } ``` ## Requiring a Search [#requiring-a-search] By default the `web_search` tool offers the model a search and lets it judge whether the question needs one. To require a search on every request, set `tool_choice`: ```json { "model": "...", "messages": [{ "role": "user", "content": "What shipped in AI this week?" }], "tools": [{ "type": "web_search" }], "tool_choice": { "type": "web_search" } } ``` Reach for this sparingly. A forced search is billed on every request that carries it, and the retrieved snippets are appended to your prompt, so they are billed as input tokens too — on a question the model could have answered from memory, you pay for both and gain nothing. If you are building a chat interface with a "web search" toggle, leaving the tool attached with the default `tool_choice` is usually what you want, so that follow-ups like "shorter, please" do not trigger a search. A few upstreams have no model-elected search at all and can only search when asked to. Requiring a search is the only way to use their search; without it they behave like a model that decided not to search, and the gateway prefers to route a merely offered tool to a provider that can make that decision for itself. You can find the models with native web search on the [models page](https://passingright.io/models?filters=1\&webSearch=true). ## Using with SDKs [#using-with-sdks] ### OpenAI SDK (Python) [#openai-sdk-python] ```python from openai import OpenAI client = OpenAI( base_url="https://api.passingright.io/v1", api_key="your-api-key" ) response = client.chat.completions.create( model="gpt-5.2", messages=[ {"role": "user", "content": "What are the latest news headlines today?"} ], tools=[{"type": "web_search"}] ) print(response.choices[0].message.content) ``` ### OpenAI SDK (TypeScript) [#openai-sdk-typescript] ```typescript import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.passingright.io/v1", apiKey: "your-api-key", }); const response = await client.chat.completions.create({ model: "gpt-5.2", messages: [{ role: "user", content: "What are the latest tech news?" }], tools: [{ type: "web_search" }], }); console.log(response.choices[0].message.content); ``` ## Streaming [#streaming] Web search works with streaming responses. Citations are included in the final chunks: ```bash curl -X POST "https://api.passingright.io/v1/chat/completions" \ -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-5.2", "messages": [ {"role": "user", "content": "What is the current stock price of Apple?"} ], "tools": [{"type": "web_search"}], "stream": true }' ``` ## Citations and Sources [#citations-and-sources] Web search responses include citations to show where information was sourced from. These appear in the `annotations` field of the message: ```json { "annotations": [ { "type": "url_citation", "url": "https://example.com/article", "title": "Article Title", "start_index": 0, "end_index": 50 } ] } ``` Citation format may vary slightly between providers, but PassingRight normalizes them into a consistent structure. ## Cost Tracking [#cost-tracking] Web search costs are rolled into the total `cost` reported in the usage object: ```json { "usage": { "prompt_tokens": 15, "completion_tokens": 150, "total_tokens": 165, "cost": 0.0125, "cost_details": { "upstream_inference_cost": 0.0115, "upstream_inference_prompt_cost": 0.0015, "upstream_inference_completions_cost": 0.01, "total_cost": 0.0125, "input_cost": 0.0015, "output_cost": 0.01, "web_search_cost": 0.001 } } } ``` Web search is billed at $0.01 per search call for reasoning models (GPT-5, o-series) and $0.025 per call for non-reasoning models. The web search charge is included in the top-level `cost` value and surfaced separately as `cost_details.web_search_cost`. ## Combining with Function Tools [#combining-with-function-tools] You can use web search alongside regular function tools: ```json { "tools": [ { "type": "web_search" }, { "type": "function", "function": { "name": "get_weather", "description": "Get weather for a location", "parameters": { "type": "object", "properties": { "location": { "type": "string" } } } } } ] } ``` Some dedicated search models only support web search and do not support additional function tools. Use `gpt-5.2` or other GPT-5 series models if you need both web search and function tools. ## Use Cases [#use-cases] ### Current Events and News [#current-events-and-news] ```json { "messages": [ { "role": "user", "content": "What are the major news stories today?" } ], "tools": [{ "type": "web_search" }] } ``` ### Real-Time Data [#real-time-data] ```json { "messages": [ { "role": "user", "content": "What is the current price of Bitcoin?" } ], "tools": [{ "type": "web_search" }] } ``` ### Research and Fact-Checking [#research-and-fact-checking] ```json { "messages": [ { "role": "user", "content": "What are the latest findings on climate change?" } ], "tools": [{ "type": "web_search" }] } ``` ### Local Information [#local-information] ```json { "messages": [ { "role": "user", "content": "What restaurants are open near me right now?" } ], "tools": [ { "type": "web_search", "user_location": { "city": "New York", "country": "US" } } ] } ``` ## Best Practices [#best-practices] 1. **Use GPT-5.2**: For the best web search experience with full tool support, use `gpt-5.2` 2. **Provide location context**: When queries are location-dependent, include `user_location` for more relevant results 3. **Monitor costs**: Web search incurs per-query costs in addition to token costs 4. **Check citations**: Always review the citations in responses to verify information sources 5. **Use streaming**: For user-facing applications, enable streaming to show responses as they're generated ## Error Handling [#error-handling] If you try to use web search with a model that doesn't support it: ```json { "error": { "message": "Model gpt-4o does not support native web search. Remove the web_search tool or use a model that supports it. See https://passingright.io/models?features=webSearch for supported models.", "type": "invalid_request_error" } } ``` To avoid this error, only use the `web_search` tool with [native web search enabled models](https://passingright.io/models?filters=1\&webSearch=true). # Agent Skills URL: https://docs.passingright-staging.sandbloc.com/guides/agent-skills **Agent Skills** are structured guidelines for AI coding agents, optimized for use with PassingRight and the AI SDK. They provide best practices and reusable instructions that help AI agents generate higher-quality code. ## What Are Agent Skills? [#what-are-agent-skills] Agent Skills are packaged sets of rules and guidelines that teach AI coding agents how to implement specific features correctly. Each skill covers: * API integration patterns * Frontend rendering best practices * Error handling strategies * Performance optimization techniques ## Available Skills [#available-skills] ### Image Generation [#image-generation] The Image Generation skill teaches AI agents how to properly implement image generation features: * **API Integration** — correctly calling image generation APIs * **Frontend Rendering** — displaying generated images efficiently * **Error Handling** — graceful degradation and retry logic * **Performance** — caching, lazy loading, and optimization ### Changelog Writer [#changelog-writer] The Changelog Writer skill drafts a polished PassingRight changelog entry and its OpenGraph image in one pass: * **House style** — writes in the established changelog voice: problem first, benefits over features, plain and confident * **Correct format** — generates the dated Markdown file with valid frontmatter (`id`, `slug`, `date`, `title`, `summary`, `image`) * **OG image prompt** — hands back a ready-to-run `gpt-image-2` prompt at the recommended **1536×1024** OpenGraph resolution * **Validation** — formats and builds the entry before you commit It ships as a [Claude Code skill](https://docs.claude.com/en/docs/claude-code/skills) — a single `SKILL.md` file. Trigger it by typing `changelog` (or "write a changelog entry") in Claude Code. ## Claude Code Skills [#claude-code-skills] Some skills ship as **[Claude Code skills](https://docs.claude.com/en/docs/claude-code/skills)**: a single `SKILL.md` file with YAML frontmatter that Claude Code loads automatically when its description matches what you ask. The **Changelog Writer** above is one of them. Install one by copying its folder into your project's `.claude/skills/` directory: ```bash # from the llmgateway-templates repo cp -r skills/changelog /path/to/your/project/.claude/skills/ ``` Then trigger it by name inside Claude Code: ``` > changelog ``` Place the folder in `~/.claude/skills/` instead to make the skill available across every project. Browse the available Claude Code skills in the [llmgateway-templates repository](https://github.com/theopenco/llmgateway-templates/tree/main/skills). ## Installation [#installation] ### Prerequisites [#prerequisites] Ensure you have Node.js 18+ and pnpm 9+ installed: ```bash node --version # v18.0.0 or higher pnpm --version # 9.0.0 or higher ``` ### Clone the Repository [#clone-the-repository] ```bash git clone https://github.com/theopenco/agent-skills.git cd agent-skills ``` ### Install Dependencies [#install-dependencies] ```bash pnpm install ``` ### Build Skills [#build-skills] Build all skills to generate the documentation: ```bash pnpm build:all ``` Or build a specific skill: ```bash pnpm build ``` ## Using Skills in Your Project [#using-skills-in-your-project] After building, each skill generates an `AGENTS.md` file that can be used with AI coding agents like Claude, Cursor, or Copilot. ### With Claude Code [#with-claude-code] Add the generated `AGENTS.md` content to your project's `CLAUDE.md` file: ```bash cat skills/image-generation/AGENTS.md >> CLAUDE.md ``` ### With Cursor [#with-cursor] Add the skill content to your `.cursorrules` file: ```bash cat skills/image-generation/AGENTS.md >> .cursorrules ``` ### With Other AI Agents [#with-other-ai-agents] Most AI coding tools support custom instructions. Copy the skill content into your tool's configuration. ## Project Structure [#project-structure] ``` agent-skills/ ├── packages/ │ └── skills-build/ # Build tooling ├── skills/ │ └── image-generation/ # Individual skill │ ├── rules/ # Rule files │ ├── AGENTS.md # Generated documentation │ └── metadata.json # Skill metadata └── package.json ``` ## Contributing [#contributing] ### Adding New Rules [#adding-new-rules] ### Fork and Clone [#fork-and-clone] Fork the repository and create a feature branch: ```bash git checkout -b feat/new-rule ``` ### Create a Rule File [#create-a-rule-file] Rules follow a standardized template with YAML frontmatter containing `title`, `impact` (high/medium/low), and `tags`. The body includes sections for Context, Incorrect examples, and Correct examples with TypeScript code blocks. See existing rules in `skills/image-generation/rules/` for reference. ### Validate and Build [#validate-and-build] ```bash pnpm validate pnpm build:all ``` ### Submit a Pull Request [#submit-a-pull-request] Push your changes and open a PR. ### Impact Levels [#impact-levels] When creating rules, use these impact levels: * **high** — Critical for correctness or security * **medium** — Important for quality and maintainability * **low** — Nice-to-have improvements ## Development Commands [#development-commands] | Command | Description | | ---------------- | --------------------------- | | `pnpm install` | Install dependencies | | `pnpm build:all` | Build all skills | | `pnpm build` | Build a specific skill | | `pnpm validate` | Validate rule files | | `pnpm dev` | Development mode with watch | ## More Resources [#more-resources] * [GitHub Repository](https://github.com/theopenco/agent-skills) — Source code and contributions * [PassingRight CLI](https://docs.passingright-staging.sandbloc.com/developers/cli) — Project scaffolding tool * [Templates](https://passingright.io/templates) — Production-ready starter projects Want to contribute a new skill or rule? Check out the [contribution guidelines](https://github.com/theopenco/agent-skills#contributing) on GitHub. # Anvil Integration URL: https://passingright.io/guides/anvil [Anvil](https://anvil.dev) is a chat-first desktop workspace for repo-aware agent delivery. It keeps conversations, repositories, work items, Git state, reviews, and terminals together while agents do the work. PassingRight is a built-in provider with one-click browser login — no keys to copy. ## Two Billing Modes [#two-billing-modes] Anvil's PassingRight connector has two billing modes sharing one login flow. **DevPass** lists canonical models (`claude-opus-5`) that the gateway routes for you on a flat coding plan; **Pay as you go** lists provider-pinned models (`anthropic/claude-opus-5`) billed against your credits. Pick the mode matching your account before connecting. **Using DevPass?** Keep the default **DevPass** mode. Provider-pinned routing is not available on coding plans, so canonical model IDs are the ones that work. ## Prerequisites [#prerequisites] * Anvil installed — [download the macOS Apple Silicon DMG](https://github.com/anthonyhumphreys/anvil-stack/releases/latest/download/Anvil-latest-arm64.dmg) or build from the [anvil-stack repository](https://github.com/anthonyhumphreys/anvil-stack) * An PassingRight account — [sign up free](https://passingright.io/signup) (no credit card required) ## Setup [#setup] ### Open the PassingRight Connector [#open-the-passingright-connector] Launch Anvil and open **Settings**. In the provider list, select **PassingRight** ("DevPass or pay-as-you-go models through one gateway connection"). The same connector is also offered during first-run onboarding. ### Pick a Billing Mode [#pick-a-billing-mode] Choose **DevPass** for a coding plan or **Pay as you go** for credits. The mode decides which model catalog Anvil loads. ### Connect in Browser [#connect-in-browser] Click **Connect in browser**. Anvil opens the PassingRight authorization page — check the account and organization, then approve the connection. Approval creates an API key and returns it to Anvil through a local callback; keep Anvil running until the callback finishes (the login times out after 5 minutes). Prefer a key you already have? Paste it into the **API key (alternative)** field instead — keys start with `llmgtwy_`. ### Select a Model [#select-a-model] Pick a model for the session. Anvil lists the gateway's agent-capable (tool-calling) models for the selected billing mode, with per-turn reasoning-effort controls where the model supports them. Check the [live catalogue](https://passingright.io/models?features=tools) for current capabilities. ### Start Building [#start-building] Run planning, implementation, or review sessions grounded in your checked-out repositories. All requests route through PassingRight and show up as **Anvil** in your [dashboard](https://passingright.io/dashboard)'s agents view, with usage, costs, and logs. ## Why Use PassingRight with Anvil [#why-use-passingright-with-anvil] * **One login for every model** — Claude, GPT, Gemini, DeepSeek, and more through a single connection * **DevPass or credits** — flat-price coding plan or pay-as-you-go, switchable in Settings * **Cost tracking** — see exactly what each agent session costs in your dashboard * **Automatic fallback** — if a provider is down, requests route to an alternative * **Volume discounts** — check [discounted models](https://passingright.io/models?discounted=true) for savings ## Troubleshooting [#troubleshooting] ### Login times out [#login-times-out] The browser login expires after 5 minutes. Click **Connect in browser** again to get a fresh authorization link, and complete the approval while Anvil is running. ### No models listed [#no-models-listed] Anvil only lists agent-capable (tool-calling) models for the selected billing mode. Switch the billing mode to match your account type, or refresh the connector status from Settings. ### Authentication errors [#authentication-errors] Make sure the connected key is active — manage your organization's API keys in the [dashboard](https://passingright.io/dashboard). Disconnect and reconnect from Settings to mint a fresh key. Need help? Join our [Discord community](https://passingright.io/discord) for support and troubleshooting assistance. # Autohand Code Integration URL: https://passingright.io/guides/autohand Autohand Code is an autonomous AI coding agent that works in your terminal, IDE, and Slack. With PassingRight, you can route all Autohand Code requests through a single gateway—use any of 200+ models from 40+ providers, with full cost tracking and smart routing. **Using DevPass?** This integration also works with a [DevPass](https://devpass.passingright.io) plan key. Use canonical model IDs without a provider prefix (`claude-sonnet-4-5`, not `anthropic/claude-sonnet-4-5`) — provider-pinned routing is not available on coding plans; the gateway picks the provider for you. ## Setup [#setup] ### Sign Up for PassingRight [#sign-up-for-passingright] [Sign up free](https://passingright.io/signup) — no credit card required. Create an API key in the dashboard and copy it when shown. ### Set Environment Variables [#set-environment-variables] Configure Autohand Code to use PassingRight: ```bash export OPENAI_BASE_URL=https://api.passingright.io/v1 export OPENAI_API_KEY=llmgtwy_your_api_key_here ``` ### Run Autohand Code [#run-autohand-code] ```bash autohand ``` All requests will now be routed through PassingRight. ## Why Use PassingRight with Autohand Code [#why-use-passingright-with-autohand-code] * **200+ models** — GPT-5, Claude Opus, Gemini, Llama, and more from 40+ providers * **Smart routing** — Automatically selects the best provider based on uptime, throughput, price, and latency * **Cost tracking** — Monitor exactly how much each autonomous agent costs * **Single bill** — No need to manage multiple API provider accounts * **Response caching** — Repeated requests hit cache automatically * **Automatic failover** — If one provider is down, requests route to another ## Configuration File [#configuration-file] You can also configure PassingRight in Autohand Code's config file: ```json { "provider": { "llmgateway": { "baseUrl": "https://api.passingright.io/v1", "apiKey": "llmgtwy_your_api_key_here" } }, "model": "gpt-5" } ``` ## Choosing Models [#choosing-models] You can use any model from the [models page](https://passingright.io/models). | Model | Best For | | ------------------------ | ------------------------------------------- | | `gpt-5` | Latest OpenAI flagship, highest quality | | `claude-opus-4-6` | Anthropic's most capable model | | `claude-sonnet-4-6` | Fast reasoning with extended thinking | | `gemini-3.1-pro-preview` | Google's latest flagship, 1M context window | | `o3` | Advanced reasoning tasks | | `gpt-5-mini` | Cost-effective, quick responses | | `gemini-3.6-flash` | Fast responses, good for high-volume | | `deepseek-v3.1` | Open-source with vision and tools | ## Autohand Code Features with PassingRight [#autohand-code-features-with-passingright] ### Terminal (CLI) [#terminal-cli] Autohand Code CLI works seamlessly with PassingRight. Set the environment variables and use all Autohand Code commands as normal—multi-file editing, agentic search, and autonomous code generation all work out of the box. ### IDE Integration [#ide-integration] Autohand Code's VS Code and Zed extensions respect the same environment variables. Set them in your shell profile and the IDE integration will automatically route through PassingRight. ### Slack Integration [#slack-integration] When using Autohand Code through Slack, configure the PassingRight base URL in your Autohand Code server settings to route all Slack-triggered coding tasks through the gateway. ## Monitoring Usage [#monitoring-usage] Once configured, all Autohand Code requests appear in your PassingRight dashboard: * **Request logs** — See every prompt and response * **Cost breakdown** — Track spending by model and time period * **Usage analytics** — Understand your AI usage patterns View all available models on the [models page](https://passingright.io/models). Need help? Join our [Discord community](https://passingright.io/discord) for support and troubleshooting assistance. # Claude Code Integration URL: https://passingright.io/guides/claude-code Claude Code can use PassingRight's Anthropic-compatible endpoint while the gateway routes requests to your selected model. This walkthrough was verified with Claude Code 2.1.263. ## Video walkthrough [#video-walkthrough]