English · 한국어
Use your LLM subscription tokens like an API. Qgrid is an LLM proxy server that exposes OpenAI/Anthropic subscription credits as an HTTP API.
Call GPT-5.5, Claude Opus, and more on a flat-rate subscription instead of pay-as-you-go API keys. Pool the quotas of N accounts and distribute requests in parallel.
Existing subscription-token proxies (claude-proxy and the like) are single-turn text proxies — they invoke a CLI once and return text. Subscription tokens aren't usable through an official API, only through the CLI/app, and a bare CLI invocation doesn't support API features like tool calls, structured output, or multi-turn agent loops.
Qgrid solves this with an AI SDK LanguageModelV3 custom provider over two subscription-backed runtimes:
- OpenAI — Direct HTTPS requests to
https://chatgpt.com/backend-api/codex/responses, with streaming responses decoded from SSE. Qgrid sends Codex CLI identity headers and replays the full conversation history on every turn. An opaque key derived fromsessionKeysupplies prompt-cache affinity without retaining provider threads. - Anthropic — Claude Code in
stream-jsonmode. Qgrid spawns a fresh, isolated process per request and replays the full conversation history, so multi-turn works without persistent sessions.
As a result:
- Tool Calling — The AI SDK's
toolsoption works as-is on both providers. The server produces tool-call shapes through structured output emulation, and the AI SDK manages tool execution. - Multi-step Agent Loop —
stopWhenandmaxStepsautomatically repeat tool-call → tool execution → next turn. You can build agents on a subscription token. - Structured Output — Request a JSON schema with
Output.object({ schema }). OpenAI enforces it through codex constrained decoding; Anthropic delivers the schema as prompt guidance (Claude Code runs in plain text mode — no--json-schemascoring or hidden retries), and validation happens in the consumer's zod. Non-conforming Anthropic output fails honestly instead of returning broken JSON. - Prompt Caching — Pass a
sessionKeyto derive stable, opaque OpenAI prompt-cache affinity while Qgrid replays the full history on every request. - Streaming — Real-time text streaming over SSE via the Sonamu Framework.
- Zero API key cost — Reuse the OpenAI/Anthropic subscription tokens you already pay for. No separate pay-as-you-go API key required.
- Tool Calling + Agent Loop — Run tool calls and multi-step agent loops on a subscription token. Not just a plain text proxy.
- AI SDK compatible — Swap a single
modelline in your existing code.generateText,streamText, structured output, and tool calls all work.model: qgrid("openai/gpt-5.6-luna") // just change this
- Pool N subscriptions — Combine teammates' subscription accounts for parallel processing. Smooth weighted routing distributes requests across tokens, while per-token quota thresholds exclude overloaded tokens.
- Request Log dashboard — Inspect token usage, cost, cache hits, TTFT, tool-call traces, and reasoning for every request in real time through a web UI.
- Image generation — Opt into Codex's
image_generationtool per request and receive PNG files through the standard AI SDK response. - OpenAI + Anthropic — Register subscription tokens for both. One-click OAuth login.
npm i -g @cartanova/qgrid-cliQgrid requires PostgreSQL to store OAuth tokens and request logs. If you already have a reachable PostgreSQL, connect to it directly; otherwise you can spin one up with Docker:
docker run --name qgrid-postgres \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=qgrid \
-p 5432:5432 \
-d postgres:18
qgrid --db postgres://postgres:postgres@localhost:5432/qgridOpen the dashboard at http://localhost:44900 → register tokens (OAuth login).
All authentication follows each provider's OAuth flow. PostgreSQL is required to persist the token received on successful login (postgres:18).
pnpm add @cartanova/qgrid-ai-sdk import { generateText } from "ai";
-import { openai } from "@ai-sdk/openai";
+import { qgrid } from "@cartanova/qgrid-ai-sdk";
const { text } = await generateText({
- model: openai("gpt-5.6-luna"),
+ model: qgrid("openai/gpt-5.6-luna"),
prompt: "What's the weather in Seoul?",
});Your existing AI SDK code stays the same. Change only model and requests go through the Qgrid server using your subscription token.
If you're already using the google/openai provider directly, add one line to see logs in the dashboard:
+import { createQgridLogger } from "@cartanova/qgrid-ai-sdk";
const { text } = await generateText({
model: google("gemini-3-flash"),
prompt: "A complex question",
+ experimental_telemetry: createQgridLogger({ serverUrl: "http://localhost:44900" }),
});- OpenAI — Calls
https://chatgpt.com/backend-api/codex/responsesdirectly. The defaultQGRID_OPENAI_TRANSPORT=websocketmode scheme-swaps that URL towssand reuses a connection for sequential requests with the same prompt-cache affinity. Requests without cache affinity use one connection each.QGRID_OPENAI_TRANSPORT=httpsremains available but does not preserve prompt-cache connection affinity. Qgrid does not replay ambiguous requests. Only a definitively rejected 401 handshake may refresh credentials and reconnect once. Requests run uncapped; new work uses smooth weighted routing across eligible tokens. Invalid selector values fail during dispatcher configuration. - Anthropic — Spawns a fresh, isolated Claude Code process per request (
stream-jsonin/out) with per-token config isolation. Conversation history is replayed each turn; OAuth tokens are refreshed automatically. - Quota threshold — Each token has a utilization threshold (default 80%). Tokens over the threshold are excluded from routing until their rolling window recovers.
- Request Log — Records each request's generate steps, tool-call steps, reasoning, token usage, cache metrics, TTFT, and cost in the DB. View them in the dashboard.
Private backend notice: The OpenAI route uses ChatGPT's private Codex backend rather than a documented public API. Its URL, request fields, identity-header requirements, SSE events, quota response, and availability may change without notice. This migration is covered by mocked protocol and transport tests; it is not a claim of verification against a live provider account.
For detailed usage, see the @cartanova/qgrid-ai-sdk README.
const { text } = await generateText({
model: qgrid("openai/gpt-5.6-luna"),
system: "You are an academic paper summarizer.",
prompt: paperText,
});const { output } = await generateText({
model: qgrid("openai/gpt-5.6-terra"),
prompt: paperText,
output: Output.object({
schema: z.object({
title: z.string(),
authors: z.array(z.string()),
keyFindings: z.array(z.string()),
}),
}),
});const { textStream } = streamText({
model: qgrid("openai/gpt-5.6-luna"),
prompt: "Explain the benefits of TypeScript",
});
for await (const chunk of textStream) {
process.stdout.write(chunk);
}const { text } = await generateText({
model: qgrid("openai/gpt-5.6-luna"),
prompt: "What's the weather in Seoul?",
tools: {
getWeather: tool({
description: "Get the current weather for a city",
inputSchema: z.object({ city: z.string() }),
execute: async ({ city }) => ({ temperature: 22, condition: "sunny" }),
}),
},
stopWhen: stepCountIs(3),
});// Replay full history with stable opaque prompt-cache affinity (OpenAI)
const { text } = await generateText({
model: qgrid("openai/gpt-5.6-luna"),
prompt: nextTurn,
providerOptions: { qgrid: { sessionKey: "chat-room-42" } },
});// OpenAI route, generateText only — enables Codex's image_generation tool for this request
const result = await generateText({
model: qgrid("openai/gpt-5.6-terra"),
prompt: "An illustration of a whale flying through space",
providerOptions: { qgrid: { imageGeneration: true } },
});
const image = result.files[0]; // mediaType: "image/png", base64Reference images are supported through AI SDK multimodal message parts:
const result = await generateText({
model: qgrid("openai/gpt-5.6-terra"),
messages: [
{
role: "user",
content: [
{ type: "text", text: "Use this image as a style reference and create a poster" },
{ type: "file", mediaType: "image/png", data: referenceImageBase64 },
],
},
],
providerOptions: { qgrid: { imageGeneration: true } },
});Reference images are sent as JSON data URLs, so compress or resize large photos before passing them in. The SDK rejects oversized base64 inputs with a clear error; WebP/JPEG is recommended for photos.
npm i -g @cartanova/qgrid-cli
qgrid --db postgres://user:password@host:port/dbname
qgrid --db postgres://... -p 3000 # specify portInstalling the CLI also syncs the qgrid agent skill for coding agents — into ~/.codex/skills/qgrid and ~/.claude/skills/qgrid on a global install, or into the project's .agents/skills and .claude/skills on a project install. See the @cartanova/qgrid-cli README for details.
You can configure the DB with environment variables:
export QGRID_DB_HOST=dev.example.com
export QGRID_DB_PORT=5432
export QGRID_DB_USER=postgres
export QGRID_DB_PASSWORD=postgres
export QGRID_DB_NAME=qgrid
qgridThe CLI translates the public QGRID_DB_* settings into Sonamu's internal
SONAMU_DB_* variables. Only source deployments that run packages/api
without the CLI use SONAMU_DB_* directly. Use NODE_ENV=staging for a remote
non-production environment such as dev0 and NODE_ENV=production for
production. The profile does not create a database; QGRID_DB_NAME selects the
database explicitly.
When teammates point at the same PostgreSQL, they share the token pool:
# On each teammate's machine
qgrid --db postgres://user:pw@dev.example.com:5432/qgrid
# In each teammate's project
QGRID_URL=http://localhost:44900
QGRID_PROJECT_NAME=my-service # labels request logs per projectIn the dashboard you can filter the whole team's request logs by project — set QGRID_PROJECT_NAME in each project so workloads stay distinguishable as traffic grows.
| Provider | Models |
|---|---|
| OpenAI | openai/gpt-6-astra, openai/gpt-5.6-sol, openai/gpt-5.6-terra, openai/gpt-5.6-luna, openai/gpt-5.5, openai/gpt-5.3-codex-spark |
| Anthropic | anthropic/claude-fable-5-1, anthropic/claude-fable-5, anthropic/claude-opus-5, anthropic/claude-sonnet-5, anthropic/claude-opus-4-8, anthropic/claude-opus-4-7, anthropic/claude-opus-4-6, anthropic/claude-opus-4-5, anthropic/claude-opus-4-1, anthropic/claude-opus-4, anthropic/claude-sonnet-4-7, anthropic/claude-sonnet-4-6, anthropic/claude-sonnet-4-5, anthropic/claude-sonnet-4, anthropic/claude-haiku-4-5 |
openai/gpt-5.4,openai/gpt-5.4-mini,openai/gpt-5.2, andopenai/gpt-5.3-codexare still accepted by the SDK type for backward compatibility, but the ChatGPT-subscription Codex route that qgrid uses no longer serves them.gpt-5.4andgpt-5.4-miniretired on 2026-08-31; useopenai/gpt-5.6-terraandopenai/gpt-5.6-lunainstead.
claude-fable-5-1,claude-fable-5,claude-opus-5,claude-sonnet-5,claude-sonnet-4-6,claude-opus-4-6, andclaude-opus-4-8automatically run with a 1M-token context window. Fable 5.1/5 always run with adaptive thinking and Opus 5 keeps its default adaptive thinking behavior; qgrid'seffortoption controls reasoning depth. Fable 5.1 shares Fable 5's $10/$50 per-1M-token pricing but bills cache reads at $0.25 instead of $1.
| Model | Context (Codex catalog) | Max output (public API) | Input / cached input / output per 1M tokens |
|---|---|---|---|
openai/gpt-6-astra |
272K | 128K | $10 / $1 / $50 |
The Codex catalog fetched on 2026-09-07 advertises a 272K context window and low, medium, high, xhigh, max, and ultra reasoning effort (backend default: medium). Qgrid's SDK still defaults to low; select an effort explicitly to override it. The public API model page separately lists a 1.05M context window, 922K maximum input, 128K maximum output, and reasoning effort through max (no ultra). Those public limits do not establish the subscription route's limits. Standard API pricing is used for qgrid's cost estimate: cache writes cost $12.50 per 1M tokens, and input over 272K tokens applies 2x input/cache and 1.5x output rates to the full request.
| Model | Context (Codex catalog) | Max output (public API) | Input / cached input / cache write / output per 1M tokens |
|---|---|---|---|
openai/gpt-5.6-sol |
272K | 128K | $4 / $0.40 / $5 / $20 |
openai/gpt-5.6-terra |
272K | 128K | $2 / $0.20 / $2.50 / $12 |
openai/gpt-5.6-luna |
272K | 128K | $0.20 / $0.02 / $0.25 / $1.20 |
The Codex catalog fetched on 2026-09-07 advertises a 272K context window for all three models. Sol and Terra support reasoning effort through ultra; Luna supports it through max. Backend defaults are low for Sol and medium for Terra and Luna, while qgrid's SDK defaults to low for all three. The public API model pages for Sol, Terra, and Luna separately list 1.05M context, 922K maximum input, and 128K maximum output; these do not establish the subscription route's limits.
Qgrid uses Standard API pricing for cost estimates. Sol's listed promotional pricing is available at least through 2026-11-21; no later rate is assumed. Input over 272K tokens applies 2x input/cache and 1.5x output rates to the full request. Cache writes cost 1.25x the uncached input rate.
| Variable | Description | Default |
|---|---|---|
QGRID_URL |
Qgrid server address (SDK) | http://localhost:44900 |
QGRID_PROJECT_NAME |
Request log project name (SDK/logger). Enables per-project filtering in the dashboard | (empty) |
HOST |
Server listen host. A non-loopback value exposes the dashboard and admin APIs | localhost |
NODE_ENV |
Sonamu runtime profile: development, test, staging, or production. Use staging for remote non-production API deployments |
development for direct API; production for CLI |
QGRID_DB_HOST |
PostgreSQL host (CLI) | localhost |
QGRID_DB_PORT |
PostgreSQL port (CLI) | 5432 |
QGRID_DB_USER |
PostgreSQL user (CLI) | postgres |
QGRID_DB_PASSWORD |
PostgreSQL password (CLI) | postgres |
QGRID_DB_NAME |
Database name (CLI) | qgrid |
SLACK_BOT_TOKEN |
Slack bot token for token-expiry notifications. Unset disables notifications | — |
SLACK_CHANNEL_ID |
Slack channel that receives those notifications | — |
SLACK_EXPIRY_REMINDER_INTERVAL_MINUTES |
Minutes between repeats of the session-expiry alert. Unset or 0 disables | — |
SLACK_USER_MAP |
tokenName:SlackUserId pairs; mapped tokens are mentioned by owner |
— |
Qgrid does not add a separate authentication guard to dashboard APIs. Keep
HOSTon loopback unless access is protected by a trusted network or reverse proxy. A public bind exposes every admin endpoint, including the Monit tab's server log feed.When running
packages/apidirectly, set the same values with Sonamu's nativeSONAMU_DB_*variables.
packages/
├── ai-sdk/ ← @cartanova/qgrid-ai-sdk (AI SDK v6 provider + logger)
├── api/ ← Sonamu server (QgridDispatcher, Request Log, OAuth)
├── web/ ← Dashboard React app (TanStack Router + Query)
└── cli/ ← @cartanova/qgrid-cli (bundles the server)
- Node.js >= 20
- PostgreSQL
- Docker (if running PostgreSQL locally as a container)
- Claude Code (for Anthropic models)
- OpenAI models: use the direct private Codex Responses backend. Sampling parameters like
temperatureandmaxOutputTokensare not supported by this route. - Anthropic models: Claude Code based. Requires OAuth login. Tool calling and object structured output are supported; OpenAI-style
sessionKeycache affinity does not apply because every request runs in a fresh process. - Structured output on Anthropic: unlike codex (constrained decoding), the Anthropic route has no enforcement mechanism. Qgrid renders the caller's original schema — and the tool envelope contract when tools are present — as text at the end of the system prompt, runs Claude Code in plain text mode, and strips code fences from the reply. The response is JSON text guided by the schema, not server-validated JSON: validate it with your own schema (the AI SDK's
Output.objectzod does this automatically). Non-conforming output surfaces as an explicit validation failure on your side instead of hidden Claude Code retries. - Positional tuples (OpenAI): OpenAI normalizes and enforces positional tuple constraints in supported positive schema positions. Tuples in negative, conditional, or otherwise non-normalizable positions fail with HTTP 400 instead of being rewritten with changed semantics. References from those positions are rejected for the same reason; definitions are normalized globally. Tuple nodes must explicitly use
type: "array"; nullable tuples useanyOf. The Anthropic route delivers the schema as prompt text without rewriting, so these restrictions do not apply there. - Schema references (OpenAI): structured schemas accept only local root-relative JSON Pointer
$refvalues targeting the document root or a chain of$defs/definitionsentry roots. References into properties, tuple internals, conditionals, or literal values fail with HTTP 400 because normalization can move or rewrite those targets. Resource IDs, anchors, external refs, dynamic refs, and recursive refs are also rejected. The Anthropic route passes the schema through verbatim, so any$refform the model can read is accepted. - Schema budget: output/tool schema serialization, tool names, descriptions, JSON escaping, and composition framing share one aggregate 512 KiB UTF-8 budget.
- Quota management: Subscription rate limits apply (5-hour / 7-day rolling window). Each token has a quota threshold (default 80%) that excludes it from routing when exceeded; tokens can also be disabled manually in the dashboard.