Documentation
SurgeX is an OpenAI-compatible inference gateway. Point an existing client at the base URL below, swap the key, and every model in the catalog becomes reachable through one endpoint.
Every example on this page is executed in CI
Quickstart
Create a key in the dashboard, then send a request. The key is shown once and stored only as a hash.
curl https://surgex.network/api/v1/chat/completions \
-H "Authorization: Bearer $SURGEX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "surgex/auto",
"messages": [{ "role": "user", "content": "Hello" }]
}'Base URL
https://surgex.network/api/v1Configurable per deployment. It is read from NEXT_PUBLIC_API_BASE_URL and is never hardcoded in the client.
Authentication
Pass your key as a bearer token. Anthropic-style x-api-key is also accepted, so the Anthropic SDK works without modification.
Authorization: Bearer sx-live-...Keys are stored as hashes
Chat completions
POST /v1/chat/completions, the OpenAI Chat Completions dialect, with a few additive extensions.
| Field | Type | Default | Description |
|---|---|---|---|
| model | string | required | A catalog id, or surgex/auto. May carry a routing suffix such as :rush. |
| messages | array | required | Standard OpenAI message array. System, user, assistant and tool roles. |
| stream | boolean | false | Stream the response as SSE. |
| max_tokens | integer | model max | Also accepted as max_completion_tokens. |
| temperature | number | provider default | 0 to 2. |
| tools | array | · | Function definitions. Routing will only pick tool-capable models. |
| response_format | object | · | text, json_object, or json_schema with strict mode. |
| reasoning_effort | string | · | low, medium or high, where the model supports it. |
| models | string[] | · | SurgeX extension. An ordered fallback chain tried after model. |
| routing | string | flow | SurgeX extension. rush, thrift, apex or flow. |
| provider | object | · | SurgeX extension. order, only, ignore, max_price, allow_fallbacks. |
| hedge | boolean | rush only | SurgeX extension. Race a second route if the first stalls. |
Response
Standard OpenAI shape, plus two additions: provider at the top level, and a surgex object carrying the routing receipt. Clients that ignore unknown fields are unaffected.
{
"id": "chatcmpl-req_01J...",
"object": "chat.completion",
"model": "openai/gpt-4.1",
"provider": "groq",
"choices": [ /* ... */ ],
"usage": {
"prompt_tokens": 1847,
"completion_tokens": 412,
"total_tokens": 2259,
"cost": "0.004192500"
},
"surgex": {
"routing": {
"mode": "rush",
"task_class": "code",
"complexity": "strong",
"selected_model": "llama-3.3-70b-versatile",
"selected_provider": "groq",
"candidates_considered": ["groq:llama-3.3-70b-versatile", "together:..."],
"attempts": [{ "route": "groq:llama-3.3-70b-versatile", "outcome": "ok", "ms": 91 }],
"failover_used": false,
"summary": "code → strong → rush → groq"
},
"ttft_ms": 91
}
}Cost is a string, on purpose
usage.cost is a decimal string rather than a JSON number. A cost like 0.000000001 loses precision the moment a JavaScript client parses it as a float, and small errors on millions of requests are how balances drift.Routing
Send surgex/auto and Crossbar chooses. It runs in two stages, and the order matters.
1. Filter, hard requirements
Routes that physically cannot serve the request are removed before anything is scored. A model that cannot call tools is not a cheap option for a request with tools; it is not an option. Filters cover tool support, strict structured output, reasoning budgets, image input, context-window fit, max output tokens, provider allow/deny lists, price ceilings and provider health.
2. Score, soft preferences
Survivors are weighted according to the mode you asked for.
| Field | Type | Default | Description |
|---|---|---|---|
| :rush | Rush | · | Lowest time-to-first-token. Price is a tiebreak. |
| :thrift | Thrift | · | Cheapest route that meets the request requirements. |
| :apex | Apex | · | Most capable route. Used when the task looks hard. |
| :flow | Flow | · | Balanced speed, cost and capability. The default. |
{
"model": "surgex/auto:rush",
"messages": [ /* ... */ ],
"provider": {
"order": ["groq", "together"],
"ignore": ["fireworks"],
"max_price": "2.00",
"allow_fallbacks": true
}
}The classifier is local and free
Auto-routing reads structural facts from your request, how many tools are offered, whether a response format is set, how large the context is, whether the text contains code. It does not call a model to decide which model to call, so it adds no latency, has nothing to fail, and sends your prompt nowhere.
When the signals are ambiguous, the classifier reports low confidence and the task class is dropped rather than guessed. The complexity tier, which comes from structural facts like size, still applies.
Failover and hedging
Failover stops at the first byte
Before any content has reached you, a failed route is retried on the next candidate automatically. After the first byte, SurgeX does not switch. Splicing a second provider's tokens into a half-delivered response corrupts the output, breaks tool-call framing and desynchronizes your parser.
A mid-stream failure arrives as an error frame in the stream and is not billed. A 4xx from an upstream is never retried elsewhere, because the request is the problem and three providers will reject it three times.
Hedged first token
Most gateways only react to errors. A provider that is merely slow is invisible to them, and p95 latency is what users feel. When a route has not produced a first token by its own observed median times 1.6, SurgeX starts a second route in parallel and takes whichever answers first. The loser is aborted before it emits anything, so you are not billed for it.
On by default in rush. Set "hedge": true to enable it in any mode, or false to turn it off.
Streaming
Set stream: true. The response is Server-Sent Events in the OpenAI format, terminated by data: [DONE].
Usage and cost arrive in a final chunk before the terminator. This is on by default; set stream_options.include_usage to false to suppress it.
const stream = await client.chat.completions.create({
model: 'surgex/auto:rush',
messages: [{ role: 'user', content: 'Hello' }],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? '');
}Tool call arguments arrive as fragments
tool_calls[].function.arguments delta is a fragment of a JSON string, not valid JSON on its own. Concatenate every fragment for a given index and parse once at the end. Parsing a partial fragment is the most common tool-calling bug in clients.Errors
Errors use the OpenAI envelope, extended with a remedy field saying what to actually do, and the upstream status where one exists.
{
"error": {
"message": "No such model: openai/gpt-9",
"type": "not_found_error",
"param": "model",
"code": "model_not_found",
"request_id": "req_01J...",
"remedy": "Call GET /v1/models for the live catalog. Model IDs are case-sensitive."
}
}| Status | Code | Retryable | Meaning |
|---|---|---|---|
| 400 | invalid_request | no | Malformed request. Fix it; retrying will not help. |
| 401 | invalid_api_key | no | Key missing, wrong or revoked. |
| 402 | insufficient_credit | no | Not enough balance to reserve this request. |
| 404 | model_not_found | no | Unknown model id. Check /v1/models. |
| 413 | payload_too_large | no | Body exceeds 10MB. |
| 429 | rate_limit_* | yes | Honour Retry-After. The suffix says which limit tripped. |
| 502 | upstream_error | yes | Upstream failed. SurgeX already tried failover. |
| 503 | no_route | no | No route satisfies the request. The message says why. |
| 504 | upstream_timeout | yes | No first token within the deadline. Not billed. |
Rate limits
Every response carries the current limit state.
x-ratelimit-limit-requests: 60
x-ratelimit-remaining-requests: 58
x-ratelimit-limit-tokens: 200000
x-ratelimit-remaining-tokens: 198420
x-ratelimit-reset-requests: 47sLimits are per key and configurable in the dashboard. Defaults are 60 requests and 200,000 tokens per minute.
Billing
SurgeX takes 2.00% once, at deposit, and passes provider list price through with no per-token markup. The price in the catalog is the price you are charged.
Streaming requests reserve an upper bound against your balance before the first token, because the final cost is unknown until the stream ends. The reservation is released and replaced with the real cost when it completes. A request that fails before producing a billable token is written off entirely.
Balance is derived from an append-only ledger, not a mutable column. Every credit and charge is a row with an idempotency key, so a retried deposit or a retried settlement cannot double-count.
Data handling
Prompt and completion bodies are not stored. The request log keeps metadata, model, provider, token counts, latency, cost, status, and the columns for bodies are left null unless an operator explicitly enables retention via LOG_REQUEST_BODIES, which is off by default.
The routing receipt returned with each response contains operational facts only: task class, complexity tier, which routes were considered and what was tried. It never contains prompt content or model reasoning.