Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/docs/content/features/anthropic-endpoint.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,7 @@ Replaying the assistant turn verbatim on the next request is supported: the `ser
### Gateway Response Cache

If [gateway caching](/features/caching/gateway-caching) is enabled on the project, a byte-identical request is replayed from cache instead of being sent upstream. Because the replayed body is identical (same `id`, same `usage`), 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.

### Provider Fallback

Like `/v1/chat/completions`, this endpoint falls back to the next healthy provider if the pinned one fails. Send `x-no-fallback: true` to disable fallback and see the pinned provider's actual error — see [Disabling Fallback with X-No-Fallback Header](/features/routing#disabling-fallback-with-x-no-fallback-header) for details.
Comment on lines +185 to +187

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Scope provider fallback to the routing mode that supports it.

apps/docs/content/features/routing.mdx documents provider-prefixed requests as having no alternate-provider fallback. This paragraph does not distinguish a session pin from an explicit provider/model prefix. State that fallback applies to model-id or session-selected providers. State that an explicit provider prefix or X-No-Fallback: true disables cross-provider fallback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/docs/content/features/anthropic-endpoint.mdx` around lines 185 - 187,
Update the “Provider Fallback” paragraph to limit fallback to model-id or
session-selected providers. Explicitly state that an explicit provider/model
prefix, or the X-No-Fallback: true header, disables cross-provider fallback, and
preserve the existing reference to the header documentation.

50 changes: 38 additions & 12 deletions apps/docs/content/features/audit-logs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ Every significant action is logged with detailed metadata:

### Organization Management

- `organization.update` — Organization settings changed
- `organization.delete` — Organization deleted
- `organization.create` / `organization.update` / `organization.delete` — Organization created, settings changed, or deleted
- `organization.block` / `organization.manage` — Administrative organization actions
- `organization.sso_auto_join.update` — SSO auto-join setting changed

### Project Management

Expand All @@ -43,32 +44,57 @@ Every significant action is logged with detailed metadata:

### Team Management

- `team_member.add` — New member invited
- `team_member.add` / `team_member.remove` — Member added or removed
- `team_member.update` — Member role changed
- `team_member.remove` — Member removed
- `team_member.budget_update` — Member's spending limit changed
- `team_member.invite` / `team_member.invite_accept` / `team_member.invite_revoke` — Invite sent, accepted, or revoked
- `team_member.auto_join` — Member auto-joined via SSO
- `team_member.iam_rule.create` / `.update` / `.delete` — Member-level IAM rule added, modified, or removed

### API Key Management

- `api_key.create` — New API key created
- `api_key.roll` — API key value rotated
- `api_key.update_status` — API key enabled/disabled
- `api_key.update_limit` — Usage limit changed
- `api_key.update_description` — Key name 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
- `api_key.iam_rule.create` / `.update` / `.delete` — IAM rule added, modified, or removed

### Master Key Management

- `master_key.create` — [Master API key](/features/master-keys) created
- `master_key.update_status` — Master key enabled/disabled
- `master_key.delete` — Master key deleted

### Provider Key Management

- `provider_key.create` — Provider key added
- `provider_key.update` — Provider key status changed
- `provider_key.update` — Provider key status or settings changed
- `provider_key.delete` — Provider key removed

### Billing Events
### Custom Model Management

- `custom_model.create` / `custom_model.update` / `custom_model.delete` — Custom provider model created, changed, or removed

### Billing and Payments

- `subscription.create` — Subscription started
- `subscription.cancel` — Subscription cancelled
- `subscription.resume` — Subscription resumed
- `subscription.create` / `subscription.cancel` / `subscription.resume` / `subscription.upgrade_yearly` — Subscription lifecycle events
- `payment.method.set_default` / `payment.method.delete` — Payment method changed
- `payment.credit_topup` — Credits purchased
- `payment.auto_topup.update` / `payment.auto_topup.disable` — Auto top-up settings changed
- `payment.self_refund` — Organization owner self-served a refund
- `credits.gift` — Credits granted by an administrator
- `dev_plan.*` / `chat_plan.*` — DevPass and Chat plan subscription, tier, and Reset Pass events

### SSO and SCIM

- `sso_provider.create` / `.update` / `.delete` — SSO provider configuration changed
- `sso_role_mapping.create` / `.delete` — SSO role mapping changed
- `sso_default_projects.update` — SSO default project assignment changed
- `sso.sign_in` — Member signed in via SSO
- `scim_token.create` / `.revoke` — SCIM token issued or revoked
- `scim.user.*` / `scim.group.*` — Directory-synced user or group provisioning events from your identity provider

## Filtering and Search

Expand Down
12 changes: 5 additions & 7 deletions apps/docs/content/features/caching/gateway-caching.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,14 @@ To use caching:

The cache key is generated from these request parameters:

- Model identifier
- Provider and model identifier
- Messages array (roles and content)
- Temperature
- Temperature, Top P, frequency/presence penalty
- Max tokens
- Top P
- Tools/functions
- Tool choice
- Response format
- System prompt
- Other model-specific parameters
- Reasoning effort and reasoning max tokens
- Prompt-cache key, retention, and options
- `n` (number of choices) and service tier
Comment on lines +68 to +75

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Expect both fields in the serialized cache key, or an explicit cache bypass.
rg -n -C 8 \
  'cache[_-]?key|cacheKey|tool_choice|tools' \
  apps/api apps/gateway \
  --glob '*.{ts,tsx}'

Repository: theopenco/llmgateway

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- cache key generation definitions/usages ---\n'
rg -n "function generateCacheKey|function generateStreamingCacheKey|generateCacheKey|generateStreamingCacheKey|cachePayload|cacheKey" apps/gateway apps/api --glob '*.{ts,tsx}' | head -n 200

printf '\n--- file with cachePayload definition ---\n'
sed -n '5430,5475p' apps/gateway/src/chat/chat.ts

printf '\n--- cache key test files ---\n'
fd -a ".*cache.*spec.*|.*cache.*test.*|.*caching.*spec.*|.*caching.*test.*" apps --glob '*.{ts,tsx}' | sed 's#^\./##'

Repository: theopenco/llmgateway

Length of output: 249


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- cache key generation definitions/usages ---'
rg -n "function generateCacheKey|function generateStreamingCacheKey|generateCacheKey|generateStreamingCacheKey|cachePayload|cacheKey" apps/gateway apps/api --glob '*.{ts,tsx}' | head -n 200 || true

echo
echo '--- payload around gateway cachePayload definition ---'
sed -n '5430,5475p' apps/gateway/src/chat/chat.ts

echo
echo '--- cache spec/test files ---'
fd -a '.*cache.*spec.*|.*cache.*test.*|.*caching.*spec.*|.*caching.*test.*' apps --glob '*.{ts,tsx}' | sed 's#^\./##' | head -n 100 || true

Repository: theopenco/llmgateway

Length of output: 3033


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- locate cacheKey module ---'
rg -n "generateCacheKey|generateStreamingCacheKey" apps/gateway/src apps/gateway --glob '*.{ts,tsx}' | head -n 160

echo
echo '--- cache key implementation files ---'
git ls-files 'apps/gateway/src/**/*.ts' 'apps/gateway/src/**/*.tsx' | xargs rg -l "generateCacheKey|generateStreamingCacheKey" | sed 's#^\./##'

Repository: theopenco/llmgateway

Length of output: 849


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- apps/gateway/src/chat/chat.ts imports and cache key exports ---'
sed -n '1,150p' apps/gateway/src/chat/chat.ts

echo
echo '--- all cache-related references in gateway chat file ---'
rg -n -C 5 "cache|Cache|Caching|caching|no-cache|x-no-cache|generateCacheKey|generateStreamingCacheKey" apps/gateway/src/chat/chat.ts | head -n 240 || true

echo
echo '--- package/config hints ---'
git ls-files | rg -n '(^|/)cache|cache$|gateway-caching|caching' || true
rg -n "generateCacheKey|generateStreamingCacheKey" . --glob '!node_modules' --glob '!dist' --glob '!build' | head -n 200 || true

Repository: theopenco/llmgateway

Length of output: 15444


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- packages/cache/src/cache.ts ---'
cat -n packages/cache/src/cache.ts

echo
echo '--- deterministic cache key shape probe from source payload mapping ---'
python3 - <<'PY'
payload = {
    "provider": "provider-A",
    "model": "model-1",
    "messages": [{"role": "user", "content": "same"}],
    "temperature": 0.5,
    "max_tokens": 100,
    "top_p": 0.9,
    "frequency_penalty": 0,
    "presence_penalty": 0,
    "response_format": {"type": "json_object"},
    "reasoning_effort": "medium",
    "reasoning_max_tokens": 256,
    "prompt_cache_key": "key",
    "prompt_cache_retention": None,
    "prompt_cache_options": None,
    "n": 1,
    "service_tier": None,
}
payload_with_tools_a = {
    **payload,
    "tools": [{"type": "function", "name": "a"}],
    "tool_choice": None,
}
payload_with_tools_b = {
    **payload,
    "tools": [{"type": "function", "name": "b"}],
    "tool_choice": None,
}
# model implementation serializes only this fixed object key order via JSON.stringify(payload)
import json
print(json.dumps([
    json.dumps(payload),
    json.dumps(payload_with_tools_a),
    json.dumps(payload_with_tools_b),
    json.dumps(payload_with_tools_a) == json.dumps(payload_with_tools_b),
]))
PY

Repository: theopenco/llmgateway

Length of output: 4874


Add tools and tool_choice to the gateway cache contract.

cachePayload does not include tools or tool_choice, while generateCacheKey() hashes JSON.stringify(payload). Requests with the same messages and parameters but different tool schemas or tool selection can replay a cached response from another tool contract. Add both fields to the gateway-level cache key and a regression test for different tool contracts before publishing the documented key contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/docs/content/features/caching/gateway-caching.mdx` around lines 68 - 75,
Add tools and tool_choice to the gateway cache payload used by
generateCacheKey(), ensuring different tool schemas or selections produce
distinct cache keys. Update the documented gateway cache contract in
gateway-caching.mdx and add a regression test that verifies requests differing
only in tool contracts do not share cached responses.


<Callout type="info">
Requests with different parameter values, even slight variations, will not
Expand Down
10 changes: 10 additions & 0 deletions apps/docs/content/features/caching/provider-cache-control.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,16 @@ Anthropic returns a per-TTL breakdown of cache writes when you mix `5m` and `1h`

For providers that publish a separate explicit-cache read rate (for example, Alibaba Qwen charges 10% for explicit cache reads vs. 20% for automatic cache reads), LLM Gateway detects the `cache_control` markers on your request and applies the explicit rate automatically.

## OpenAI Prompt Caching Fields

For OpenAI models, LLM Gateway also forwards OpenAI's own prompt-caching request fields:

- `prompt_cache_key` — a string used to improve cache routing for requests that share a prompt prefix. It also doubles as a fallback [sticky session](/features/sessions) key if no session header is set.
- `prompt_cache_retention` — `"in_memory"` or `"24h"`, OpenAI's cache retention policy for eligible models.
- `prompt_cache_options` — explicit prompt caching controls for GPT-5.6 and later: `{ "mode": "implicit" | "explicit", "ttl": "30m" }`. In `implicit` mode (the default) OpenAI places an automatic cache breakpoint at the latest message. In `explicit` mode, only content parts you mark with a `prompt_cache_breakpoint` object are cached.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="apps/docs/content/features/caching/provider-cache-control.mdx"
if [ -f "$file" ]; then
  echo "== file excerpt =="
  sed -n '160,200p' "$file" | cat -n
else
  echo "missing $file"
fi

echo "== nearby references to prompt_cache_options / prompt_cache_breakpoint ========="
rg -n "prompt_cache_options|prompt_cache_breakpoint|GPT-5\.6|implicit|explicit" apps/docs/content -S || true

echo "== JSON syntax probe for the exact object text =="
python3 - <<'PY'
import json, pathlib, re
p=pathlib.Path('apps/docs/content/features/caching/provider-cache-control.mdx')
s=p.read_text()
m=re.search(r'`prompt_cache_options`.+?\[(.*?)\]', s, re.S)
print("has_match", bool(m))
if m:
    print("object_text")
    print(m.group(0))
    try:
        json.loads(m.group(0))
        print("json_parse=ok")
    except Exception as e:
        print("json_parse_error", type(e).__name__, e)
PY

Repository: theopenco/llmgateway

Length of output: 12345


Use valid JSON for prompt_cache_options.

The object at line 185 includes "implicit" | "explicit", which is schema notation and invalid JSON. Describe the allowed values in prose or show separate valid request payloads.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/docs/content/features/caching/provider-cache-control.mdx` at line 185,
Update the prompt_cache_options example in the caching documentation to use
valid JSON, removing the schema-style "implicit" | "explicit" value. Describe
the permitted mode values in prose or provide separate valid payload examples
while preserving the documented default and TTL behavior.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target file excerpt =="
sed -n '160,205p' apps/docs/content/features/caching/provider-cache-control.mdx || true

echo
echo "== occurrences of GPT-5.6 and later in target =="
rg -n "GPT-5\.6|later|prompt_cache_breakpoint|prompt_cache_options" apps/docs/content/features/caching/provider-cache-control.mdx || true

echo
echo "== docs catalogue/live-page clues =="
rg -n "catalogue|catalog|gpt-5|GPT-5|model.*capability|capability.*page|implicit|explicit" apps/docs/content -g '*.md' -g '*.mdx' | head -200 || true

Repository: theopenco/llmgateway

Length of output: 40209


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repository files related to catalog/models/caching =="
git ls-files | rg '(^|/)(catalog|model|provider|caching|capability|gateway|README|.*\.ts$|.*\.js$|.*\.json$)' | head -300

echo
echo "== prompt_cache_options definitions/usages in repo =="
rg -n "prompt_cache_options|prompt_cache_breakpoint|prompt_cache_retention|prompt_cache_key|cache_control|openai" . --glob '!**/node_modules/**' --glob '!**/.git/**' | head -300 || true

echo
echo "== docs catalogue/live link patterns nearby =="
rg -n "llmgateway\.io/models|models\.llmgateway|provider catalog|catalogue|catalog" apps/docs/content/features apps/docs/content/learn apps/docs/content/developers apps/docs/content/guides -g '*.md' -g '*.mdx' | head -200 || true

echo
echo "== read docs models/caching docs relevant links =="
for f in apps/docs/content/learn/models.mdx apps/docs/content/features/models-directory.mdx apps/docs/content/features/caching/index.mdx; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    sed -n '1,80p' "$f"
  fi
done

Repository: theopenco/llmgateway

Length of output: 11320


Replace the GPT-5.6 and later cutoff.

GPT-5.6 and later is a catalogue-derived model constraint. Link to the live model catalogue or model capability page instead, and describe support by capability.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/docs/content/features/caching/provider-cache-control.mdx` at line 185,
Update the prompt_cache_options documentation to remove the catalogue-derived
“GPT-5.6 and later” cutoff, link to the live model catalogue or model capability
page, and describe availability in terms of models supporting the prompt-caching
capability while preserving the implicit and explicit mode behavior.

Source: Coding guidelines


These fields are only forwarded to OpenAI models that support them; they have no effect on other providers.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 6 \
  'prompt_cache_key|prompt_cache_retention|prompt_cache_options' \
  apps/api apps/gateway apps/docs \
  --glob '*.{ts,tsx,md,mdx}'

Repository: theopenco/llmgateway

Length of output: 39968


Narrow the OpenAI-only forwarding statement.

The list includes prompt_cache_key, but apps/docs/content/features/sessions.mdx documents it on OpenAI, Meta, and Azure Responses as well as an Azure chat-completions caveat. Limit this sentence to prompt_cache_retention and prompt_cache_options, or update both docs and runtime forwarding to match.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/docs/content/features/caching/provider-cache-control.mdx` at line 187,
Update the provider forwarding statement in provider-cache-control.mdx to apply
only to prompt_cache_retention and prompt_cache_options, excluding
prompt_cache_key. Keep the existing documentation and runtime behavior unchanged
unless intentionally synchronizing all affected docs and forwarding logic.


## Related

- [Gateway Caching](/features/caching/gateway-caching) — serve byte-identical requests entirely from LLM Gateway at $0 cost
Expand Down
51 changes: 51 additions & 0 deletions apps/docs/content/features/multiple-choices.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
title: Multiple Choices
description: Generate several completions for one prompt in a single request with the `n` parameter.
icon: Copy
---

import { Callout } from "fumadocs-ui/components/callout";

# Multiple Choices

The OpenAI-compatible `n` parameter requests multiple independent completions for the same prompt in a single call, returned as separate entries in the `choices` array.

```bash
curl -X POST "https://api.llmgateway.io/v1/chat/completions" \
-H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5",
"n": 3,
"messages": [
{ "role": "user", "content": "Give me a tagline for a coffee shop." }
]
}'
```

Each entry in the response `choices` array has its own `index` and `message`, so you can compare or pick among the generated variants.

## Supported Providers

`n` is only forwarded when the resolved model supports it upstream:

- **OpenAI Chat Completions** models
- **Google Gemini** models (mapped to Gemini's native `candidateCount` parameter)

Comment on lines +28 to +34

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace the hardcoded provider list with a live catalogue link.

This section hardcodes a catalogue-derived provider list. Describe support in terms of resolved model mappings and link to the live model catalogue.

Proposed direction
 `n` is only forwarded when the resolved model supports it upstream:
 
-- **OpenAI Chat Completions** models
-- **Google Gemini** models (mapped to Gemini's native `candidateCount` parameter)
+See the [live model catalogue](/models) for current mappings that support `n`.

As per coding guidelines, apps/docs/**/*.{md,mdx} must never hardcode catalogue-derived lists of models or providers; documentation must link to live catalogue-generated pages instead.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## Supported Providers
`n` is only forwarded when the resolved model supports it upstream:
- **OpenAI Chat Completions** models
- **Google Gemini** models (mapped to Gemini's native `candidateCount` parameter)
## Supported Providers
`n` is only forwarded when the resolved model supports it upstream:
See the [live model catalogue](/models) for current mappings that support `n`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/docs/content/features/multiple-choices.mdx` around lines 28 - 34, Update
the “Supported Providers” section in multiple-choices.mdx to remove the
hardcoded OpenAI and Google provider list, describe support through resolved
model mappings, and link readers to the live model catalogue-generated page for
current provider coverage.

Source: Coding guidelines

Requests with `n` for any other provider/model are rejected with a `400` error.
Comment on lines +30 to +35

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Scope the rejection statement to n > 1.

The model mapping contract in Lines 400-420 of packages/models/src/models.ts rejects unsupported mappings when n > 1. The current sentence also covers n: 1.

-Requests with `n` for any other provider/model are rejected with a `400` error.
+Requests with `n > 1` for any other provider/model are rejected with a `400` error.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
`n` is only forwarded when the resolved model supports it upstream:
- **OpenAI Chat Completions** models
- **Google Gemini** models (mapped to Gemini's native `candidateCount` parameter)
Requests with `n` for any other provider/model are rejected with a `400` error.
`n` is only forwarded when the resolved model supports it upstream:
- **OpenAI Chat Completions** models
- **Google Gemini** models (mapped to Gemini's native `candidateCount` parameter)
Requests with `n > 1` for any other provider/model are rejected with a `400` error.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/docs/content/features/multiple-choices.mdx` around lines 30 - 35, Update
the rejection statement in the multiple-choices documentation to apply only when
n is greater than 1, matching the model mapping contract; keep the
supported-provider forwarding statements unchanged.


## Streaming

Streaming is supported for OpenAI models: choice deltas for all `n` choices are demultiplexed on a single SSE stream, distinguished by `choices[].index`.

<Callout type="warn">
A few combinations are rejected with a `400` error instead of silently
falling back to `n: 1`:
- `n > 1` with `stream: true` **and** function `tools` — the streaming
tool-call aggregator can't disambiguate concurrent tool calls across
choices. Native `web_search` tools and the `web_search: true` flag are
exempt from this restriction.
- `n > 1` with `stream: true` on Google models — Gemini's
`streamGenerateContent` does not support `candidateCount`.
- `n > 8` on Google models — Gemini caps `candidateCount` at 8.
</Callout>
6 changes: 3 additions & 3 deletions apps/docs/content/features/reasoning.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ If you specify `reasoning.max_tokens` for a model that doesn't support it, you'l
"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"
"code": null
}
}
```
Expand Down Expand Up @@ -236,7 +236,7 @@ If you specify `verbosity` for a model that doesn't support it, you'll receive a
"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"
"code": null
}
}
```
Expand Down Expand Up @@ -336,7 +336,7 @@ If you specify `reasoning_effort` for a model that doesn't support reasoning, yo
"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"
"code": null
}
}
```
Expand Down
14 changes: 11 additions & 3 deletions apps/docs/content/features/routing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,11 @@ Sticky session routing solves this: attach a session identifier and LLMGateway p
For chat completions, the session key is resolved in priority order:

1. The `x-session-id` header
2. The `prompt_cache_key` body field (OpenAI-compatible)
3. The `user` body field (OpenAI-compatible)
2. The `x-session-affinity` header (sent by some coding agents, e.g. opencode)
3. The `session_id` header
4. The `session-id` header
5. The `prompt_cache_key` body field (OpenAI-compatible)
6. The `user` body field (OpenAI-compatible)

```bash
curl -X POST "https://api.llmgateway.io/v1/chat/completions" \
Expand Down Expand Up @@ -400,7 +403,12 @@ Automatic retry to a different provider is disabled when:
- The maximum retry count (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.
configured and the current key fails with a retryable error — including a
**401/403 auth failure**, since that's often isolated to a single bad
credential rather than the whole provider. Same-key retries back off
exponentially (1s, 2s, 4s, ...) up to **2 retries** by default (configurable
Comment on lines 405 to +409
via the `SAME_KEY_MAX_RETRIES` environment variable), unlike cross-provider
fallback which retries immediately against a different upstream.
Comment on lines 405 to +411

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Distinguish same-provider key retries from cross-provider fallback.

Lines 391-394 state that retries do not trigger for 401/403 failures. This paragraph states that same-provider retries can include 401/403. Rewrite the earlier rule as applying to cross-provider fallback, and state that 401/403 retries occur only against another key for the same provider.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/docs/content/features/routing.mdx` around lines 405 - 411, Update the
earlier retry rule in the routing documentation to apply specifically to
cross-provider fallback, not same-provider key retries. Clarify that 401/403
failures are retried only with another configured key for the same provider,
while cross-provider fallback does not retry them.


### Routing Transparency

Expand Down
6 changes: 4 additions & 2 deletions apps/docs/content/features/sessions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ For chat completions, the session key is resolved in priority order — the firs

1. The `x-session-id` header
2. The `x-session-affinity` header (sent automatically by coding agents such as opencode)
3. The `prompt_cache_key` body field (OpenAI-compatible)
4. The `user` body field (OpenAI-compatible)
3. The `session_id` header
4. The `session-id` header
5. The `prompt_cache_key` body field (OpenAI-compatible)
6. The `user` body field (OpenAI-compatible)

```bash
curl -X POST "https://api.llmgateway.io/v1/chat/completions" \
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/content/learn/billing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ The Billing page is your central hub for managing credits, plans, and payment me

Displays your current credit balance. Credits are consumed as you make API requests through the gateway. Click **Top Up Credits** to add more credits to your account.

Once your organization has enough usage history, the Credits page also shows an estimated **runway** — approximately how many days your current balance will last at your recent average daily spend.

Comment on lines +17 to +18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Document the actual runway window and no-usage condition.

The Credits page reads avgDailySpend7d and displays No recent usage when that value is zero. The current wording does not state the seven-day window and may imply a separate minimum-history rule. Document the seven-day average and the no-usage behavior.

Suggested wording
-Once your organization has enough usage history, the Credits page also shows an estimated **runway** — approximately how many days your current balance will last at your recent average daily spend.
+When your organization has recent usage, the Credits page also shows an estimated **runway** — approximately how many days your current balance will last based on your average daily spend over the last seven days.
🧰 Tools
🪛 LanguageTool

[style] ~17-~17: This is not the usual sequence for adjectives that have no special emphasis.
Context: ... your current balance will last at your recent average daily spend. ## Fees Top-ups are char...

(EN_ADJ_ORDER)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/docs/content/learn/billing.mdx` around lines 17 - 18, Update the Credits
page runway description in the billing documentation to state that it is based
on the recent seven-day average spend and that the page displays “No recent
usage” when that average is zero; remove wording that implies a separate minimum
usage-history requirement.

## Fees

Top-ups are charged the credit amount plus the following fees:
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/learn/provider-keys.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Each configured key shows:

For each provider key:

- **Edit** — Update the key name, value, or base URL
- **Edit** — Toggle the key's active status. For a custom provider, you can also rename it, restrict it to catalog-defined models only, and set its compliance attestation. The API key value and base URL cannot be changed after creation — delete the key and add a new one instead.
- **Deactivate** — Temporarily disable the key without deleting it
- **Delete** — Permanently remove the key

Expand Down
14 changes: 14 additions & 0 deletions apps/docs/content/learn/team.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ description: Manage team members and their roles within your organization
icon: UsersRound
---

import { Callout } from "fumadocs-ui/components/callout";
import { ThemedImage } from "@/components/themed-image";

The Team page lets you invite team members, assign roles, and control access to your organization.
Expand All @@ -19,6 +20,15 @@ Click **Add Member** to invite someone by email. You'll need to:

Your plan includes up to **5 team seats**. The current count is displayed, and the Add button is disabled when all seats are used. Contact sales for additional seats.

<Callout type="info">
Assigning the **Developer** role requires the Enterprise plan, since it scopes
the member's access to a specific set of projects. On other plans, the
Developer option is disabled — invite as Admin instead, or upgrade to
Enterprise.
</Callout>

When inviting a Developer, you must also select at least one project — the developer is limited to those projects and can be granted or revoked access to additional projects later from their detail page.

## Team Members List

Each member shows:
Expand Down Expand Up @@ -58,3 +68,7 @@ For the full rule semantics, see [Member-Level IAM Rules](/features/api-keys#mem
| **Developer** | View and use resources only. Cannot modify settings or manage team |

Developers can also be given **restricted access** at the API key level, limiting which keys they can view and use.

## Default Developer Budget

Owners and admins on Enterprise can set an org-wide **default developer budget** — a usage limit and period usage limit applied to every Developer who doesn't have their own individual budget set. A member's own budget, when configured via **Manage budget**, always overrides the default.
Comment on lines +71 to +74

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 8 \
  'SSO_TEAM_DEFAULT_DEVELOPER_BUDGET|default developer budget|periodUsageLimit|Manage budget' \
  packages apps

Repository: theopenco/llmgateway

Length of output: 50379


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== candidate files =="
git ls-files | rg '(^packages/shared/src/member-budget-limits\.ts$|^packages/db/src/member-budget\.ts$|^apps/docs/content/learn/team\.mdx$|Manage budget|ManageBudget|memberBudget)' || true

echo
echo "== docs section =="
sed -n '55,85p' apps/docs/content/learn/team.mdx | cat -n

echo
echo "== UI/budget mentions excluding docs broad search =="
rg -n -C 4 \
  'defaultDeveloperMaxApiKeys|defaultDeveloperUsageLimit|defaultDeveloperPeriodUsageLimit|SSO_TEAM_DEFAULT_DEVELOPER_BUDGET|ManageBudget|Manage budget|Manage Budget|maxApiKeys|periodUsageLimit' \
  apps packages \
  --glob '!apps/docs/**' \
  --glob '!packages/db/src/seed.ts' \
  --max-count 200 || true

echo
echo "== focused budget resolver tests =="
if git ls-files | grep -q 'packages/db/src/member-budget.spec.ts'; then
  sed -n '1,220p' packages/db/src/member-budget.spec.ts | cat -n
fi

Repository: theopenco/llmgateway

Length of output: 50377


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== sso provisioning budget handling =="
sed -n '68,130p' apps/api/src/routes/sso.ts | cat -n
sed -n '420,448p' apps/api/src/routes/sso.ts | cat -n

echo
echo "== organization route default developer budget handling =="
sed -n '140,230p' apps/api/src/routes/organization.ts | cat -n
rg -n -C 4 'defaultDeveloper(MaxApiKeys|UsageLimit|PeriodUsageLimit|PeriodUsageDuration)' apps/api/src/routes/organization.ts

echo
echo "== route docs for defaultDeveloperMaxApiKeys =="
rg -n -C 8 'defaultDeveloperMaxApiKeys' apps/docs apps/api/src/routes -g '!apps/docs/content/**' || true

Repository: theopenco/llmgateway

Length of output: 21063


Document the full default developer budget cap.

The SSO default is a $500/month recurring budget plus a separate 3 active API key cap. Include both parts of the org default, and replace “usage limit” with the actual recurring/period budget wording so members do not expect an uncapped lifetime limit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/docs/content/learn/team.mdx` around lines 71 - 74, Update the “Default
Developer Budget” section to document both org-wide defaults: the $500/month
recurring budget and the separate cap of 3 active API keys. Replace the vague
“usage limit and period usage limit” wording with explicit recurring and period
budget terminology, while preserving the statement that an individual member
budget overrides the default.

22 changes: 15 additions & 7 deletions apps/docs/content/learn/transactions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,21 @@ Each transaction entry includes:

## Transaction Types

| Type | Description |
| ----------------------- | ----------------------------------- |
| **Credit Top-up** | Manual or automatic credit purchase |
| **Credit Refund** | Credits refunded to your account |
| **Subscription Start** | New plan subscription started |
| **Subscription Cancel** | Plan subscription canceled |
| **Subscription End** | Plan subscription period ended |
| Type | Description |
| --------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| **Credit Top-up / Refund / Gift** | A manual/automatic credit purchase, a refund, or credits granted by an admin |
| **Subscription Start / Cancel / End** | Legacy plan subscription lifecycle events |
| **Dev Plan Start / Upgrade / Downgrade / Cancel / Resume / End / Renewal** | [DevPass](https://llmgateway.io/devpass) subscription lifecycle events |
| **Dev Plan Reset Pass** | A purchased, rewarded, or gifted DevPass Reset Pass |
| **Chat Plan Start / Upgrade / Downgrade / Cancel / Resume / End / Renewal** | Chat plan subscription lifecycle events |
| **End-User Top-up / Refund / Bonus** | LLM SDK end-user wallet activity, for organizations issuing end-user wallets |
| **End-User Margin Accrual / Payout** | Margin earned on end-user wallet usage and its payout to the organization |
Comment on lines +28 to +36

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 6 \
  'transaction\.type|devpass|dev_plan|chat_plan|end.?user|credit_(topup|refund|gift)|subscription_(start|cancel|end)' \
  apps packages

Repository: theopenco/llmgateway

Length of output: 50379


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== candidate transaction files =="
git ls-files | rg -n 'transaction|Transaction|TransactionsClient|transactions\.mdx' || true

echo
echo "== docs snippet =="
sed -n '1,80p' apps/docs/content/learn/transactions.mdx

echo
echo "== focused source matches (top 200) =="
rg -n -C 4 \
  'getTransactionLabel|formatTransactionLabel|transaction\.type|TransactionType|devPlan|chatPlan|EndUser|credit_(topup|refund|gift)|subscription_(start|cancel|end)|DevPass|Reset Pass' \
  apps packages --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' | head -n 500

Repository: theopenco/llmgateway

Length of output: 37245


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== dashboard transaction clients =="
wc -l apps/ui/src/components/billing/transactions-client.tsx apps/ui/src/components/dashboard/transactions-client.tsx apps/ui/src/app/dashboard/[orgId]/org/transactions/page.tsx
echo

ast-grep outline apps/ui/src/components/billing/transactions-client.tsx apps/ui/src/components/dashboard/transactions-client.tsx --view expanded || true
echo

echo "== relevant transaction-client snippets =="
sed -n '1,260p' apps/ui/src/components/billing/transactions-client.ts
sed -n '1,260p' apps/ui/src/components/dashboard/transactions-client.tsx
echo

echo "== transaction schema/files =="
git ls-files | rg 'transaction' || true
rg -n -C 3 'createTable\("transaction"|transaction.*Table|enum\s*\(|dev_plan|chat_plan|end_user|credit_{topup,refund,gift}|subscription_{start,cancel,end}' packages apps/db apps/worker -g '!**/migrations/meta/**' | head -n 400

Repository: theopenco/llmgateway

Length of output: 1621


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== billing transaction-client snippets =="
sed -n '1,330p' apps/ui/src/components/billing/transactions-client.tsx
echo
sed -n '330,620p' apps/ui/src/components/billing/transactions-client.tsx
echo

echo "== dashboard transaction-client =="
sed -n '1,280p' apps/ui/src/components/dashboard/transactions-client.tsx
echo

echo "== transaction schema/files =="
git ls-files | rg 'transaction' || true
rg -n -C 3 'createTable\("transaction"|transaction.*Table|type: .*enum|credit_topup|credit_refund|credit_gift|subscription_start|subscription_cancel|subscription_end' packages apps/db apps/worker -g '!**/migrations/meta/**' | head -n 400

Repository: theopenco/llmgateway

Length of output: 37136


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== transaction schema =="
sed -n '404,455p' packages/db/src/schema.ts
echo

echo "== transaction backend handlers =="
git ls-files | rg 'transaction' | rg -v '/migrations/meta/'
rg -n -C 4 'transaction|isInvoiceableTransaction|getTransaction|dev_plan_|chat_plan_|end_user|refund|credit_gift' packages apps/worker apps/api -g '!**/migrations/meta/**' | head -n 800

echo
echo "== deterministic label shape for missing type branch =="
python3 - <<'PY'
from pathlib import Path
p = Path('apps/ui/src/components/billing/transactions-client.tsx')
s = p.read_text()
cases = [
    'credit_topup', 'credit_refund', 'credit_gift',
    'subscription_start', 'subscription_cancel', 'subscription_end',
    'dev_plan_start', 'dev_plan_upgrade', 'dev_plan_downgrade',
    'dev_plan_renewal', 'dev_plan_reset_pass',
    'dev_plan_cancel', 'dev_plan_resume', 'chat_plan_start',
    'chat_plan_upgrade', 'chat_plan_downgrade', 'chat_plan_renewal',
    'chat_plan_cancel', 'chat_plan_resume', 'chat_plan_end',
    'chat_plan_reset_pass',
    'end_user_topup', 'end_user_refund', 'end_user_bonus',
    'end_user_margin_accrual', 'end_user_margin_payout',
]
print('cases_not_defined_in_switch=', sorted([t for t in cases if f'case "{t}"' not in s]))
PY

Repository: theopenco/llmgateway

Length of output: 50376


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== schema tail for transaction enum =="
sed -n '455,485p' packages/db/src/schema.ts
echo

echo "== UI TransactionCard getTypeLabel complete block (targeted) =="
python3 - <<'PY'
from pathlib import Path
p = Path('apps/ui/src/components/billing/transactions-client.tsx')
text = p.read_text()
for line in range(1, p.stat().st_size):
    if text.find('const getTypeLabel = (type: Transaction["type"])') != -1:
        start = text.rfind('\n', 0, text.find('const getTypeLabel'))
        break
end = text.find('const getStatusColor =', start)
print(text[start:end])

cases = sorted({
    'credit_topup', 'credit_refund', 'credit_gift',
    'subscription_start', 'subscription_cancel', 'subscription_end',
    'dev_plan_start', 'dev_plan_upgrade', 'dev_plan_downgrade',
    'dev_plan_cancel', 'dev_plan_pause' or 'dev_plan_resume',
    'dev_plan_end', 'dev_plan_renewal',
    'dev_plan_reset_pass', 'dev_plan_reset_pass_reward', 'dev_plan_reset_pass_gift',
    'chat_plan_start', 'chat_plan_upgrade', 'chat_plan_downgrade',
    'chat_plan_cancel', 'chat_plan_pause' or 'chat_plan_resume',
    'chat_plan_end', 'chat_plan_renewal',
    'end_user_topup', 'end_user_refund', 'end_user_bonus',
    'end_user_margin_accrual', 'end_user_margin_payout',
})
PY
echo

echo "== all case labels in TransactionsClient GetTypeLabel =="
rg -n 'case ".*"' apps/ui/src/components/billing/transactions-client.tsx

Repository: theopenco/llmgateway

Length of output: 1963


Add UI transaction type labels for the new transaction rows.

Transaction["type"] now includes dev_plan_*, chat_plan_*, and end_user_* values, but TransactionsClient maps only the legacy credit and subscription values and falls back to default: return type. If these rows are returned from the UI transaction API, the Type column can show raw enum values in both desktop and billing UI. Add labels for each supported type, or narrow the transaction list/query so the API only returns types with UI labels.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/docs/content/learn/transactions.mdx` around lines 28 - 36, Update the
transaction type-label mapping used by TransactionsClient to cover all
dev_plan_*, chat_plan_*, and end_user_* values listed in the transaction table,
ensuring both desktop and billing UI display human-readable labels instead of
raw enum values; alternatively, narrow the transaction query to return only
types already supported by that mapping.


## Refunds and Invoices

Eligible transactions — credit top-ups and DevPass/Chat plan payments, within a short window and below a usage threshold — show a **Request Refund** action so the organization owner can self-serve a refund without contacting support. Refunds are processed through Stripe; the transaction and credit balance update once Stripe confirms the refund via webhook.

Completed payments with an associated Stripe invoice offer a **Download Invoice** action to get a PDF invoice or credit note for that transaction.
Comment on lines +40 to +42

## Status Badges

Expand Down
17 changes: 15 additions & 2 deletions apps/docs/content/resources/rate-limits.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@ For organizations that have **purchased at least some credits**:

## Paid Models

**Paid AI models are not currently rate limited.** You can make as many requests as needed to paid models, subject only to your account's credit balance and any provider-specific limits.
**Paid AI models are not rate limited by default.** You can make as many requests as needed to paid models, subject only to your account's credit balance and any provider-specific limits.

Enterprise organizations can configure their own requests-per-minute and requests-per-day caps per provider or model (or shared across the organization). When such a limit is configured, requests against it are enforced regardless of whether the model is free or paid.

## Rate Limit Headers

All API responses include rate limit information in the headers:
Free-model requests include standard rate limit headers:

```http
X-RateLimit-Limit: 20
Expand All @@ -54,6 +56,17 @@ X-RateLimit-Reset: 1640995200
- `X-RateLimit-Remaining`: Number of requests remaining in the current window
- `X-RateLimit-Reset`: Unix timestamp when the rate limit window resets

If an org-level provider/model rate limit (see above) applies to the request, additional headers report that separately, per window (`RPM`/`RPD`):

```http
X-RateLimit-Limit-Provider: 1000
X-RateLimit-Remaining-Provider: 999
X-RateLimit-Limit-Provider-RPM: 60
X-RateLimit-Remaining-Provider-RPM: 59
```

A `429` from an org-level provider/model limit also includes `Retry-After` and `X-RateLimit-Reset` headers for the blocking window.

## Rate Limit Exceeded

When you exceed your rate limit, you'll receive a `429 Too Many Requests` response:
Expand Down
Loading