Response proxy
What happens between streamText() in the browser and the ChatGPT-backed Codex endpoint.
The browser provider sends AI SDK traffic to POST /api/chatgpt/responses.
The proxy exists so credentials never enter the browser, and so you have one
chokepoint to police what a signed-in page can spend.
Request lifecycle
For every proxied request, the handler:
- Verifies the signed session cookie. No cookie means
401. - Rejects cross-origin browser requests unless the origin is in
allowedOrigins(403). - Applies the per-session rate limit (
429withretry-after). - Validates the JSON body: object shape, size under
maxRequestBytes, model inallowedModelsif configured. - Loads tokens and refreshes them if needed.
- Normalizes the body for the stateless Codex backend, then adds the bearer
token, account id header, and
client_versionquery parameter. - Streams the upstream body back with
cache-control: no-store.
Guardrails and their defaults
Every proxied request spends the signed-in user's own plan, so the proxy is not a generic pass-through:
| Guardrail | Default | Option |
|---|---|---|
| Rate limit | 30 requests / 60s per session | responsesProxy.rateLimit (false to disable) |
| Body size | 40 MiB | responsesProxy.maxRequestBytes |
| Model allowlist | all models | responsesProxy.allowedModels (array or predicate) |
| Cross-origin browser calls | same-origin only | allowedOrigins |
| Default model injection | gpt-5.5 when the body omits model | defaultModel |
const auth = createChatGPTHandler({
secret: process.env.LWC_SECRET,
responsesProxy: {
allowedModels: ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini"],
rateLimit: { limit: 10, windowMs: 60_000 },
},
});The default rate-limit store is per instance
The rate limiter counts in memory. Behind multiple instances or serverless
invocations, each instance counts separately. Pass a shared store in
responsesProxy.rateLimit to enforce the limit globally.
If you need custom quotas, audit logging, or stricter endpoint controls, keep
the request-scoped proxy pattern and wrap auth.proxyFetch(request) in your own
route. Raw token export is available only through the explicit dangerous escape
hatch in the server reference.
Request normalization
The ChatGPT-backed Codex endpoint is stateless and rejects some standard
Responses API fields. createCodexFetch() from @opencoredev/loginwithchatgpt-core
rewrites every body the same way for both the proxy and the direct provider:
- Forces
store: false. Turns are stateless. - Fills
instructions,reasoning(effort: "medium",summary: "auto"), andtext.verbosity: "medium"when the body leaves them out. - Adds
reasoning.encrypted_contenttoinclude, so multi-turn reasoning survives statelessness. - Drops
item_referenceinput items and stripsidfields from the rest. - Removes
max_output_tokensandmax_completion_tokens, which the endpoint rejects.
SDK headers
Browser code cannot rewrite proxied bodies, so two narrow headers let it opt
into request settings. The server validates each value, writes it into the
body, and returns 400 for anything else. The values and UI patterns are in
Fast tier & thinking effort; the exact
contract is in the routes reference.
One behavior worth knowing here: if upstream rejects service_tier=fast for
the session, the handler retries once without it and adds a
x-login-with-chatgpt-service-tier-fallback: auto response header, so the
request still streams.