Chat Completions
Minimum-discount routing: Prefix the path with a min{N} segment (e.g. /min30/v1/chat/completions) to require marketplace seller offers to meet a minimum estimated buyer discount before routing. Buyer-owned providers are not covered. See Minimum-Discount Routing.
The primary inference endpoint. OpenAI-compatible — works with any client that speaks the OpenAI format.
Request
Parameters
modelrequiredModel name (canonical, OpenRouter, Venice, or alias format)messagesrequiredArray of message objectsstreamtruefor SSE streaming (default: false)max_tokensMaximum output tokensmax_completion_tokensAlias formax_tokenstemperatureSampling temperaturetop_pNucleus samplingstopStop sequencestoolsFunction calling tools (OpenAI format — auto-translated for Anthropic sellers)tool_choiceTool selection strategy (auto,required, or specific function)response_formatStructured output formatreasoning_effortReasoning effort as a string, on models whosesupported_parameterslists it. See Reasoning ControlsreasoningReasoning options as an object (e.g.{"effort": "high"}). Not interchangeable withreasoning_effort; see Reasoning ControlsproviderOptional provider hint — a single provider or an array of providers (an allow-list). Narrows routing to offers from the matching provider(s) before cheapest-offer selection.provider_urlOptional provider URL hint (single value). Equivalent toprovider, but matched by provider host.provider_base_urlOptional provider base URL hint (single value). Equivalent toprovider_url.providers_blockedProviders to refuse for this request — a single provider or an array. The deny-list counterpart ofprovider; matched on the same spellings. See Routing Controlsprovider_orderPreferred attempt order by provider, most-preferred first. Ordering only — it never makes an offer eligible or ineligible. See Routing Controlsstream_options{"include_usage": true}(auto-injected)
curl $SURPLUS_BASE_URL/chat/completions \ -H "Authorization: Bearer inf_xxx" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-opus-4.6", "messages": [{"role": "user", "content": "Hello!"}], "stream": true, "max_tokens": 1000 }'
Minimum Discount
-H "X-Min-Discount: 30"Routes only to offers at least 30% below the go-direct reference price; otherwise the request fails with
minimum_discount_not_met, naming the best discount that was available. The same floor can be set once on the
API key (min_discount_pct) or in the URL (/min30/v1/chat/completions) — the strictest of the three applies.
Price ceilings (max_price_per_1m, X-Max-Price-Per-1M) are retired: a per-1M input rate ignored output
and cache rates, so it never described what a request actually cost.
See Parameter Compatibility for full model-specific support, and
Reasoning Controls for reasoning_effort / reasoning.
Provider Pinning, Blocking, and Ordering
By default, Surplus routes each request to the cheapest healthy seller for the requested model. If you need to restrict routing to one or more specific providers, pass a provider hint in the request body. It accepts a single provider or an array of providers (an allow-list).
With an array, the request routes to any offer whose provider is in the list — the router then applies normal selection within that set: it picks the cheapest healthy offer among the allowed providers and excludes everyone else (even a cheaper provider you didn't list). All the usual rules still apply inside the allow-list: health checks, spend caps, price thresholds, estimated-cost sorting, and failover among matching offers.
Accepted forms (each array element can be any of these):
- a provider id or name —
"zai"/"Z.ai","uncensored"/"Uncensored AI" - a provider host —
"api.z.ai" - a provider URL —
"provider_url": "https://api.z.ai/api/paas/v4"or"provider_base_url": "https://api.uncensored.com/api/v1"(these two aliases stay single-valued)
Unsupported providers. Each entry is matched against the supported provider list. Unrecognized entries are ignored, so a mix like ["zai", "not-a-provider"] simply routes on zai. If every entry is unrecognized, the request fails with 400 unsupported_provider (the error names the bad entries and lists the supported providers).
No offers vs unsupported. A supported provider that just has no active healthy offer for the requested model returns 404 no_sellers_for_model — distinct from 400 unsupported_provider. Provider hints are optional and advanced: pinning narrowly can fail even when another (unlisted) provider has liquidity for the same model.
providers_blocked — the deny-list
The counterpart of provider, and a sibling of it rather than a nested option: provider says who may serve the request, providers_blocked says who may not. Same accepted spellings (family, name, or host), single value or array.
Unlike an unrecognized provider entry, an unreadable providers_blocked is refused, not ignored — {"venice": true} fails with 400 invalid_request rather than silently applying no block. A block that quietly disappears would route the request to precisely the provider you refused, so it fails loudly instead. An empty array means "block nothing", never "block everything".
If the block leaves no eligible offer, the request fails with 503 no_healthy_sellers.
provider_order — the attempt order
Providers you name are tried first, in the order given; everything else follows in the router's own order. Ordering is not eligibility — it reorders offers that already passed every filter and can neither resurrect an excluded offer nor exclude one you did not name.
All three work on every text surface: /v1/chat/completions, /v1/completions, /v1/responses, and /anthropic/v1/messages. None of them are forwarded to the provider. See Routing Controls for the full semantics and the key-scoped blocklist they compose with.
{
"model": "glm-4.7",
"messages": [{"role": "user", "content": "Hello!"}],
"provider": "zai"
}{
"model": "glm-4.7",
"messages": [{"role": "user", "content": "Hello!"}],
"provider": ["zai", "openrouter"]
}{
"model": "glm-4.7",
"messages": [{"role": "user", "content": "Hello!"}],
"providers_blocked": ["openrouter"],
"provider_order": ["zai", "venice"]
}Tool Calling
Current behavior:
- Send tools in OpenAI
tools/tool_choiceformat. - OpenAI-compatible sellers receive OpenAI-format tool calls after normalization. This is the path used by the active production Claude sellers today, mostly Venice and Bankr LLM Gateway.
- Native Anthropic sellers (
api.anthropic.com) are supported through an OpenAI ⇄ Anthropic translation layer:toolsbecome Anthropic tools, assistanttool_callsbecometool_useblocks,role: "tool"messages becometool_resultblocks, and streamingtool_usedeltas are converted back to OpenAItool_callsdeltas. - Tool support is still model-dependent. Check
GET /v1/models: ifsupported_parametersdoes not includetools, the marketplace stripstoolsandtool_choicebefore forwarding because the upstream model rejects them. - Non-function tools such as computer-use or text-editor tool types are not executed by the marketplace; unsupported tool definitions are stripped or normalized to function tools when possible.
Streaming function-tool calling is supported for providers/models that stream tool-call deltas. Anthropic SSE tool_use events are translated to OpenAI tool_calls deltas; OpenAI-compatible providers are passed through after normalization.
{
"model": "claude-opus-4.6",
"messages": [
{"role": "user", "content": "What's the weather in Tokyo?"},
{"role": "assistant", "content": null, "tool_calls": [
{"id": "call_1", "type": "function", "function": {"name": "get_weather", "arguments": "{\"city\":\"Tokyo\"}"}}
]},
{"role": "tool", "tool_call_id": "call_1", "content": "{\"temp\": 22, \"condition\": \"sunny\"}"}
],
"tools": [{"type": "function", "function": {"name": "get_weather", "parameters": {"type": "object", "properties": {"city": {"type": "string"}}}}}]
}Multimodal Input
Images, documents, and other media travel inside messages[].content as typed parts, with no upload step:
{"type": "image_url", "image_url": {"url": "https://…"}}, or a data: URL for inline bytes.
messages is forwarded to the chosen seller whole. The marketplace does not fetch buyer URLs, re-encode
bytes, or validate the MIME type declared in a data URL. Two consequences follow. An unusual shape, such as
a video data URL in image_url, succeeds only if the provider that wins the route accepts it. An image
referenced only by file_id returns 400 unresolvable_file_reference, since no marketplace seller's key
can resolve another account's file id.
Model capability requires a lookup: check architecture.input_modalities and supported_features on
GET /v1/models. The two disagree on some rows and the router treats either as sufficient.
Full contract, including video, YouTube URLs, documents, limits, and errors: Media Inputs.
{
"model": "gemini-3.8-flash",
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "What is in this image?"},
{"type": "image_url", "image_url": {"url": "https://example.com/photo.jpg"}}
]
}]
}Message Roles
| Role | Description |
|---|---|
system | System prompt (mapped to Anthropic system parameter when needed) |
developer | Alias for system (OpenAI convention, auto-mapped) |
user | User message (text, images, or mixed content blocks) |
assistant | Model response (may include tool_calls) |
tool | Tool result (requires tool_call_id matching a previous tool_calls entry) |
systemDescription · System prompt (mapped to Anthropic system parameter when needed)developerDescription · Alias for system (OpenAI convention, auto-mapped)userDescription · User message (text, images, or mixed content blocks)assistantDescription · Model response (may include tool_calls)toolDescription · Tool result (requires tool_call_id matching a previous tool_calls entry)- providerAllow-list — keeps only these
- providers_blockedDeny-list — wins over the allow-list
- provider_orderRe-sorts the survivors
Only the first two change which sellers are eligible. provider_order re-sorts what survived — it cannot
bring back a seller the deny-list removed.
Response
Standard OpenAI format. Streaming returns SSE chunks with data: {...} lines.
Error Codes
| Status | Meaning |
|---|---|
| 400 | Invalid request (bad model name, missing messages). Media-specific: unresolvable_file_reference, and model_does_not_support_images when the image-modality gate is enforcing (it is off by default, in which case the provider's own rejection surfaces instead). See Media Inputs |
| 401 | Invalid API key |
| 402 | Insufficient balance, or payment required (x402) |
| 404 | Model has no sellers |
| 503 | All sellers unhealthy |
unresolvable_file_reference, and model_does_not_support_images when the image-modality gate is enforcing (it is off by default, in which case the provider's own rejection surfaces instead). See Media Inputs