steady-proxy is a local reverse proxy for AI agents and API clients. It sits between your client and API gateway, buffering streaming responses so dropped connections, truncated streams, rate limits, and gateway 5xx errors can be retried before the client sees a failed turn.
Agents / SDKs / applications → steady-proxy (127.0.0.1:8789) → your gateway
- Transactional buffering: Holds SSE responses until completion (or a configurable time window) so broken streams can be discarded and retried cleanly.
- Retry normalization: Converts recoverable HTTP and pre-commit stream failures into standard
503responses withRetry-After(plus optional in-proxy retries). Final upstream answers (auth, billing, validation, and other4xx) are relayed unchanged. - Stream validation and repair: Validates Anthropic event order and tool JSON, coalesces fragmented
input_json_deltachunks, and merges bursty Responses text deltas on replay. - Refusal fallback: Re-issues Claude refusals against a fallback model (default
claude-opus-5, can be disabled) before committing the response, delivering the original refusal if the fallback cannot answer. - Workflow watchdog support: Uses a short buffering window for Claude Code Workflow agents so their content-stall timer does not trip.
- Diagnostics: Logs one-line request outcomes and optionally archives full request/response payloads for debugging.
| API | Route | Default behavior |
|---|---|---|
| Anthropic Messages | POST /v1/messages (stream: true) |
Buffers up to 10 minutes (600000 ms) until message_stop, then replays. Workflow agents switch to live streaming after 10 seconds once content is ready. |
| OpenAI Responses | POST /v1/responses (stream: true) |
Catches start-of-stream failures before commit, then streams live on first output. Set PROXY_RESPONSES_EARLY_COMMIT=0 for 10-minute full buffering. |
| Gemini GenerateContent | POST /{version}/models/{model}:streamGenerateContent?alt=sse (including Vertex AI publisher and express paths) |
Buffers up to 10 minutes through clean EOF after all requested candidates finish. |
| Other routes | /v1/chat/completions, generateContent, countTokens, embedContent, non-SSE streamGenerateContent, /v1/models, non-streaming calls |
Forwarded single-shot; recoverable HTTP and connection errors still normalize for client retry. |
See Architecture for client compatibility details.
Requires Docker with Compose v2 (or Go 1.23+ to build from source).
git clone https://github.com/Xllent-AI/steady-proxy
cd steady-proxy
cp .env.example .env
cp docker-compose.yml.example docker-compose.ymlSet your gateway URL in .env:
PROXY_UPSTREAM_URL=https://your-gateway.example.comUse the gateway root (or required prefix) without a trailing /v1—the proxy appends the incoming request path (/v1/messages, /v1/responses, etc.) verbatim.
Start the proxy and verify it responds:
docker compose up -d --build
curl -fsS http://127.0.0.1:8789/__version/__version checks local proxy health only. The proxy forwards your client's existing API keys and auth headers to the gateway.
Linux amd64 and arm64 images are published to GitHub Container Registry:
docker pull ghcr.io/xllent-ai/steady-proxy:latestlatest tracks main; version tags (such as v0.1.1) pin a release. To run the prebuilt image with Compose, set image: ghcr.io/xllent-ai/steady-proxy:latest in docker-compose.yml, remove the build: block, and run docker compose pull && docker compose up -d. See Releasing.
Merge this env block into ~/.claude/settings.json alongside your existing settings and credentials:
{
"env": {
"ANTHROPIC_BASE_URL": "http://127.0.0.1:8789",
"CLAUDE_CODE_CONNECT_TIMEOUT_MS": "660000",
"API_TIMEOUT_MS": "720000",
"API_FORCE_IDLE_TIMEOUT": "0"
}
}Because the proxy sends no HTTP headers while buffering, client timeouts must exceed the 600,000 ms buffering window and the idle watchdog must be disabled. If you change PROXY_KEEPALIVE_MS, update these client timeouts to match; see Timeouts.
Update your active provider section in ~/.codex/config.toml (keep /v1 in base_url and keep your auth settings):
[model_providers.your_provider]
base_url = "http://127.0.0.1:8789/v1"
wire_api = "responses"
stream_idle_timeout_ms = 660000
request_max_retries = 10
stream_max_retries = 10To enable full 10-minute buffering for Responses and up to 3 in-proxy retries per client attempt, add to .env and run docker compose up -d:
PROXY_RESPONSES_EARLY_COMMIT=0
PROXY_TRANSACTIONAL_LOCAL_RETRIES=3Full buffering delays output until the turn completes (or the window expires). Local retries apply across Messages, Responses, and Gemini SSE within the active buffering window; see Retry budgets.
Point your Gemini SDK or client base URL at http://127.0.0.1:8789 and set PROXY_UPSTREAM_URL to your Gemini gateway (or https://generativelanguage.googleapis.com). The proxy forwards x-goog-api-key, Bearer tokens, and query parameters unchanged.
Example streaming call:
curl -N --max-time 720 \
"http://127.0.0.1:8789/v1beta/models/${GEMINI_MODEL}:streamGenerateContent?alt=sse" \
-H "x-goog-api-key: ${GEMINI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"contents":[{"parts":[{"text":"Say hello."}]}]}'Set client timeouts above PROXY_GEMINI_BUFFER_MS (default 600,000 ms) and configure bounded retries on HTTP 503 honoring Retry-After. Failed upstream attempts and local-retry waits count toward that silent window; for a client whose first-byte timeout is shorter, lower PROXY_GEMINI_BUFFER_MS below it (see Match the client to the window). If a Gemini stream fails after committing to live output, the proxy aborts the HTTP transport so the client does not mistake a truncated stream for clean EOF.
Point Messages clients at http://127.0.0.1:8789/v1/messages and Responses clients at http://127.0.0.1:8789/v1/responses (watch whether your SDK appends /v1 automatically). Enable stream: true, set client timeouts above the buffering window, and retry HTTP 503 with Retry-After. See Client compatibility.
Send a test prompt and tail the proxy logs:
docker compose logs -f proxyEach request logs a single line starting with OK (delivered), RETRY (client asked to retry), WARN (model fallback), FAIL (terminal error), or DROP (stream failed after headers were already sent). See Reading the logs.
- Bind to loopback only: The proxy has no authentication of its own; anyone who can reach its port can relay requests through your gateway. Compose binds
127.0.0.1:8789by default. - Payload archives are unencrypted:
PROXY_REQUEST_LOG_DIRis off by default. When enabled, it writes full prompts and responses to disk without encryption or automatic retention; see Data handling. - Retries repeat upstream work: Buffering hides failed attempts from the client, but cannot undo side effects already executed by server-side or remote tools. Ensure remote tools are idempotent; see Retry limits and side effects.
| Guide | Contents |
|---|---|
| Architecture | Buffering lifecycle, commit points, retry budgets, refusal fallback, and Workflow agents. |
| Configuration | Environment variables, client timeout matching, and upstream gateway headers. |
| Operations | Compose workflow, log format, payload archiving, and systemd service setup. |
| Troubleshooting | Symptom lookup, error classification rules, and timeout debugging. |
| Development | Building from source, running unit and live tests, and code layout. |
| Releasing | Local release packaging, tag publishing, and GHCR container images. |
MIT © Xllent AI.