Quick start

SlashSpend is an OpenAI-compatible gateway — it implements the Chat Completions API that nearly every client and framework already targets (OpenAI SDK, LangChain, LiteLLM, the Vercel AI SDK, plain fetch). Point your base URL at it, pass your provider keys as headers, and keep calling gpt-*, claude-*, gemini-*, or openrouter/* models — same code. One-line diff, not a rewrite; your main branch won't even flinch.

1. Send a request

import openai

client = openai.OpenAI(
    base_url="https://api.slashspend.ai/v1",
    api_key="ss_live_your-slashspend-key",
    default_headers={
        "X-OpenAI-API-Key": "sk-your-openai-key",
        "X-Anthropic-API-Key": "sk-ant-your-anthropic-key",
    },
)

resp = client.chat.completions.create(
    model="gpt-5",
    messages=[{"role": "user", "content": "Hello"}],
)
print(resp.choices[0].message.content)

2. Supported providers

ProviderHeaderExample models
OpenAIX-OpenAI-API-Keygpt-5, gpt-5-mini, gpt-4.1
AnthropicX-Anthropic-API-Keyclaude-opus-4-8, claude-sonnet-4-6, claude-haiku-4-5
Google GeminiX-Google-API-Keygemini-3.1-pro, gemini-3.1-flash, gemini-2.5-pro
OpenRouterX-OpenRouter-API-Keyopenrouter/zhipu/glm-5.2, openrouter/qwen/qwen-3.7

3. Key modes

Resolution priority per request: matching header → Key Vault → a clear configuration error.

4. Response headers

X-SlashSpend-Cachehit | semantic | miss — exact, then semantic match
X-SlashSpend-Model-UsedThe model actually called
X-SlashSpend-Model-Routedtrue when routed to a cheaper model
X-SlashSpend-Routing-Decisionrouted | off | above_threshold | no_target | pinned | declined_* — why routing did or didn't substitute (§8)
X-SlashSpend-Shadowsampled — this request was shadow-tested against the cheaper model in the background (§8)
X-SlashSpend-Session-Lockedtrue when locked to a warm provider/model
X-SlashSpend-Session-Lock-Bypassedno_provider_key — a lock was skipped because its provider key wasn't resolvable this request (fail-open)
X-SlashSpend-Session-Lock-Droppedterminal_error | all_failed | no_candidate_fits — the lock's model went bad, so we dropped the lock; the session re-pins next request
X-SlashSpend-Failovertrue when a fallback provider was used
X-SlashSpend-Failover-Reasonstatus_5xx | ttfb_timeout | context_window_too_small — why we moved off an earlier candidate
X-SlashSpend-Compressedtrue when the newest message was compressed
X-SlashSpend-Tokens-Savedestimated input tokens removed by compression
X-SlashSpend-Output-Modecaveman | standard — dense-output mode applied
X-SlashSpend-Caveman-Suppressedstructured_output — caveman skipped to protect JSON/tool output
X-SlashSpend-Latency-Msmeasured end-to-end time at our edge
X-SlashSpend-Upstream-Mstime spent in the provider call (0 on a cache hit)
X-SlashSpend-Overhead-Msour processing — routing, cache, compression
X-SlashSpend-Request-Idtrace id; look it up at GET /v1/trace?id=
X-SlashSpend-Routed-Confidence0–1 confidence that a downgrade was safe
X-SlashSpend-PromptCachepreserved | stripped — Anthropic cache_control handling
X-SlashSpend-PromptCache-Reasonwhy the discount was stripped, when it was
X-SlashSpend-Translatedtools,images,structured_output — features translated to Anthropic-native on the cache_control path
X-SlashSpend-JSON-Modeprompted — response_format json_object honored via a system-prompt nudge
X-SlashSpend-Passthroughtrue when an unknown model was forwarded untouched
X-SlashSpend-Degradedtrue when an optional optimization was skipped after a fault
X-SlashSpend-Semantic-Scorecosine similarity of a semantic cache match
X-SlashSpend-Semantic-Match-Idwhich stored entry a semantic hit matched
X-SlashSpend-Semantic-Matched-Requestrequest id whose response the semantic hit served — look it up via /api/v1/trace
X-SlashSpend-Debugcaptured | skipped — opt-in content debug (skipped if it can't be encrypted at rest)
X-SlashSpend-Cache-Tier1h | 5m — Anthropic prompt-cache tier applied
X-SlashSpend-Stabilizedcount of volatile fragments normalized in the prompt
X-RateLimit-Limit / -Remainingyour account's per-minute budget and what's left (all keys share one bucket; configurable in Settings)
X-SlashSpend-RateLimit-Warningapproaching-limit — traffic entered the top 20% of your rate window (§5)

5. Errors & rate limits

Errors are returned in OpenAI format with standard status codes: 401 invalid SlashSpend key, 402 inactive subscription, 429 rate limited, 400 missing provider key, 503 all providers failed.

Rate limiting is per account — every API key you mint shares one sliding-window bucket, sized by your own setting (Settings → Account, 100–5000 requests/minute self-serve, effective on the next request) or the default. Every response advertises X-RateLimit-Limit and X-RateLimit-Remaining; a 429 includes a real Retry-After computed from the window (usually a few seconds, not a blanket minute). When traffic enters the top 20% of the window we add X-SlashSpend-RateLimit-Warning: approaching-limit and email you once a day — so a launch-day ceiling is a heads-up, not a surprise. And if our limiter infrastructure ever falters, we fail open: your requests are served (marked X-SlashSpend-Degraded: true) rather than refused.

Reliability controls. A slow provider fails over on time-to-first-byte: X-SlashSpend-Timeout-Ms: 20000 sets that budget per request (clamped 1 000–120 000 ms; default 30 000), and a timeout moves to the next candidate tagged X-SlashSpend-Failover-Reason: ttfb_timeout. A conversation is briefly pinned to whichever model served it (so a multi-turn thread stays consistent); the pin yields to failover and re-points at the healthy model, and if the pinned model goes bad (a terminal 4xx) we drop the pin so the next request starts fresh — send X-SlashSpend-Session-Lock: off to disable pinning for a request entirely.

Cross-provider failover, with an open-weight backstop. On a transient upstream fault (429, 5xx, or a TTFB timeout) we walk a curated chain: an OpenAI model degrades to Anthropic, then Gemini; an Anthropic model to OpenAI, then Gemini; and so on. If you've stored an OpenRouter key, every chain ends with one more shot at a strong open-weight model (DeepSeek by default) — so even a simultaneous wobble across all three frontier providers degrades to a served answer instead of a 503. That last hop is only attempted when an OpenRouter key is present; without one it's invisible, and a total outage returns an honest 503. A candidate is skipped (never silently retried) if it has no key for you or physically can't hold the request; terminal client errors (400/401/403/404/422) are relayed as-is and never retried across the chain, so you're never double-billed.

6. Streaming

Set "stream": true for server-sent events in the standard OpenAI chat.completion.chunk shape — for every provider, including Anthropic. We normalize to that one shape on purpose (we don't relay a provider's native SSE), so your stream parser never branches on who answered. Even Anthropic cache_control requests — which we route to the native Messages API to preserve your prompt-cache discount — are translated to OpenAI chunks on the fly, including streamed tool_calls (we open the call, then stream its arguments exactly like OpenAI does). Streaming responses are not cached.

Token usage on a stream: we auto-request a final usage chunk on OpenAI, OpenRouter, Gemini and Anthropic's compat endpoint (unless you set your own stream_options), and synthesize one on the native Anthropic cache_control path — so streamed token counts are the provider's own, not estimates. If a provider ever omits usage despite the request, we fall back to estimating output tokens from the streamed text; reconcile against your provider invoice for exact billing.

7. Dense output — “caveman” mode (optional)

Send X-SlashSpend-Output: caveman and we prepend a single system instruction asking the model to answer tersely — short sentences, no filler, no pleasantries. It's a prompt-level nudge, opt-in per request and off by default: we ask for a shorter answer, we don't truncate or rewrite what comes back. Output tokens are the expensive half of most bills, so for agents, coding tools, and batch/programmatic jobs — where a dense answer is fine — this is a real saver. It's the wrong tool for user-facing chat, where tone and complete prose matter.

We won't let it corrupt structured responses: if a request uses response_format JSON, tools, or functions, we automatically suppress caveman (terse nudging makes models drop JSON fields) and report X-SlashSpend-Caveman-Suppressed: structured_output. It's also skipped for unknown models we forward untouched (X-SlashSpend-Passthrough: true). The response header X-SlashSpend-Output-Mode always tells you which mode actually applied — caveman or standard. Dense output is independent of history compression (§10): caveman shortens the answer; it never edits your conversation history.

8. Model routing (optional)

Routing substitutes a cheaper model for small, clearly-simple requests — think claude-opus-4-8claude-sonnet-4-6 on a two-line classification prompt. It is off by default, and turning it on is not a blank check: a substitution happens only when every one of these checks passes, in order:

  1. Routing is enabled for your account (Settings → Routing).
  2. The request is under your token threshold (default 800) — a cost-worthiness gate, not the difficulty check.
  3. A substitution row matches, and the model isn't pinned “never downgrade”.
  4. The request carries no cache_control breakpoints — we won't trade your prompt-cache discount for a cheaper model.
  5. Difficulty scoring judges the prompt clearly simple, with a confidence margin — and only when real embeddings are active. Short-but-hard prompts stay on the model you asked for, and we refuse to downgrade at all if we can't score difficulty properly.
  6. Your key for the cheaper model's provider is available. If not, the request stays on the original model — a savings setting never turns a servable request into an error.

Every response tells you what happened in X-SlashSpend-Routing-Decision: routed, off, above_threshold, no_target, pinned, or a declined_* reason (floor, cache_control, no_embeddings, complexity, no_target_key) — so a declined downgrade is never confusable with “routing isn't configured”. When a substitution does happen, X-SlashSpend-Model-Routed: true and X-SlashSpend-Routed-Confidence accompany it, and X-SlashSpend-Model-Used always names the model that actually ran. Default substitutions only ever step down one capability tier within the same provider (Opus → Sonnet, Sonnet → Haiku, Pro → Flash); anything more aggressive — including open-model targets like DeepSeek or Qwen via OpenRouter — is available but requires your explicit say-so in the substitution table.

Shadow mode — prove it before you trust it. Not sure the cheap model is good enough? Enable shadow mode (Settings → Routing) and we re-run a sampled percentage of requests that would have been routed against the cheaper model in the background — your callers always get the primary answer, and sampled requests carry X-SlashSpend-Shadow: sampled. We embed both answers and store only their similarity score (never content), building a per-route divergence report — “the cheap model agrees N% of the time” — so you can turn on auto-routing with evidence instead of hope. Shadow calls run on your provider key and are billed by the provider; streaming and structured-output requests are never shadowed.

9. Open models & OpenRouter

Anything with the openrouter/ prefix routes through your OpenRouter key — so the cheap room can be an open model like openrouter/zhipu/glm-5.2 or openrouter/qwen/qwen-3.7. You can even point your substitution table at them in Settings → Routing.

Adding an OpenRouter key does two things at once. Cost: the routing screen surfaces one-click recommendations (DeepSeek, GLM, Qwen) so simple, sub-threshold requests can run on an open model for a fraction of frontier pricing — off until you add the row. Resilience: the same key turns those open models into the automatic last-resort failover backstop described in §5 — you get the cheaper bill and a stronger safety net from one key, and neither is applied silently.

New models are never gated behind us: an unrecognized model is forwarded untouched (X-SlashSpend-Passthrough: true), and our model registry auto-syncs from the OpenRouter catalog every few hours — so a newly launched model gains full pricing and context-window awareness within 24 hours, automatically, with no deploy on your side.

And yes, SlashSpend stacks happily on top of OpenRouter. OpenRouter picks the best host for a model you already chose; we decide whether that pricey model should run at all — cross-tier routing, semantic cache, and compression that provider-selection doesn't do. Different layer, no conflict. More in the FAQ.

10. Caching controls

Cached responses are encrypted at rest with a per-account key when an encryption secret is set. Anthropic cache_control prompt caching is preserved — we route those calls to the native Messages API so your provider-side cache discount survives, and we translate your tools, tool calls/results, images, and response_format to the native schema so nothing is lost on that path (the X-SlashSpend-Translated header tells you what was translated). The one edge is loose {"type":"json_object"} mode, which has no native equivalent — we honor it with a JSON-only system nudge and flag it via X-SlashSpend-JSON-Mode: prompted.

11. Observability

Every response carries X-SlashSpend-Request-Id. Fetch the metadata-only decision trace (cache type, routing decision + confidence, failover reason, latency — never prompt or response content) at GET /v1/trace?id=<request id>.

Need the actual content for one request? Send X-SlashSpend-Debug: true and we capture that request + response for 1 hour — owner-scoped, and encrypted at rest with your account key when an encryption secret is set — returned in the same trace lookup. Off by default: with no debug header, no content is ever stored.

Semantic cache hits return X-SlashSpend-Semantic-Score, -Match-Id, and -Matched-Request — the id of the prior request whose response you were served, resolvable through this same trace endpoint — so a match is never a black box. The threshold resolves in order: per-request X-SlashSpend-Semantic-Threshold header (0–1), then your account setting (Settings → Account, 0.8–1), then the conservative default. The dashboard's semantic-match audit shows every hit's score with two one-click fixes: Exclude (that stored entry never matches again) and Raise (your account threshold moves just above the bad match's score).

12. No lock-in — leaving SlashSpend

SlashSpend is BYOK: your provider accounts, keys, billing tier, and previews always stay yours — we never intermediate that relationship. Leaving is a one-line revert: point your base URL back at the provider and your code keeps working (we speak the same OpenAI wire format you were already using; Anthropic-native paths revert cleanly too).

Take your data with you anytime via GET /v1/export — your config, routing rules, provider-key inventory (previews only, never secrets), and full usage logs as open JSON.

Step-by-step in the five-minute leaving guide. We test the base-URL revert in CI on every release — leaving stays a one-liner by construction.