Skip to main content

Provider Routing

Drop a one-liner into any existing agent and pick up:

  • Cross-provider fallback — first try Fireworks, fall through to OpenAI on rate limit
  • Per-instance secrets — each provider account uses its own env var, no secret collisions
  • Capability filtering — automatically skip non-tools instances when a call needs tools=[]
  • Group references — point at "group:cheap-llama-70b" and let the chain decide
  • Per-call telemetry — same provider + cost attribution that Waxell runtime users get

You don't need the Waxell runtime to use any of this. Configure your provider instances at /settings/llm-routing in the controlplane; your agent code calls waxell.llm.call(...) and dispatch is driven by the same data.

Quick start

pip install 'waxell-observe[all-providers]'

Configure your API keys in env (the same names you'd otherwise pass to the SDKs directly):

export OPENAI_API_KEY=sk-...
export FIREWORKS_API_KEY=fw-...
# Whatever secret_ref names you set in /settings/llm-routing

Then in code:

import waxell_observe as waxell

waxell.init() # reads WAXELL_API_KEY + WAXELL_API_URL from env

response = waxell.llm.call(
model="llama-3.1-70b-instruct",
messages=[{"role": "user", "content": "Hello!"}],
)
print(response.choices[0].message.content)

That's it. No runtime, no Django, no servers. The SDK pulls your provider config from the controlplane on first call (cached 5 minutes with ETag), resolves the model, dispatches through the right SDK, and emits the same telemetry runtime users get.

How it works

your code                                  waxell-observe              controlplane
| | |
| waxell.llm.call(model="...", ...) | |
|------------------------------------------> | |
| | GET /llm-config/manifest |
| |------------------------> |
| | <----------- 200 / 304 - |
| | resolve_chain(model) |
| | filter_chain_for_mode() |
| | os.environ[secret_ref] |
| | openai.OpenAI(base_url=) |
| | .chat.completions.create |
| <---- SDK response object (no wrapper) --- | |

The SDK's Manifest is a snapshot of:

  • instances — your registered provider accounts (kind, base_url, secret_ref name, capabilities)
  • tenant_modelsmodel_id → instance_id mappings (e.g. llama-3.1-70b-instruct lives on fireworks-prod)
  • groups — ordered fallback chains (cheap-llama-70b → fireworks first, ollama fallback)
  • capability_overrides — per-(instance, model) tri-state flag pins

Resolved at dispatch time; cached by ETag.

Resolving a model

waxell.llm.call(model=...) accepts three shapes:

ShapeExampleWhat happens
Plain"gpt-4o"Look up TenantModel; if missing, use the default instance for the prefix-inferred kind.
Qualified"fireworks-prod/llama-3.1-70b-instruct"Use that exact instance with that exact model name.
Group"group:cheap-llama-70b"Walk the group's entries in declared order.

Mode-specific helpers

# Plain chat (default)
waxell.llm.text(model="gpt-4o", messages=[...])

# JSON mode — adds response_format={"type": "json_object"}
waxell.llm.json(model="gpt-4o", messages=[...])

# JSON with schema
waxell.llm.json(model="gpt-4o", messages=[...], schema={...})

# Tool calls
waxell.llm.tool(
model="gpt-4o",
messages=[...],
tools=[{"type": "function", "function": {...}}],
)

The capability filter drops chain entries whose instances don't advertise the required capability. If you ask for tool mode against a chain whose only candidate is Ollama (no native tools), you'll get NoCandidateForMode rather than a confusing OpenAI error from the provider.

Secrets — the contract

Provider instances reference an env var name, not a secret value. The controlplane stores the name (secret_ref); your process reads the value from os.environ[secret_ref] at dispatch time.

When the env var is unset:

SecretNotInEnvironment: Provider instance 'fireworks-prod' references
env var FIREWORKS_API_KEY which is not set in this process. Either set
the env var, or change the instance's secret_ref in the controlplane
at /settings/llm-routing.

This intentionally mirrors how you'd already provide keys to the direct SDK — the dispatcher is opt-in, not magical. Keys never leave your process.

Groups for cross-provider fallback

Define a group at /settings/llm-routing (or via the API), then reference it as model="group:...":

# Group "cheap-llama-70b" defined as:
# 1. fireworks-prod / accounts/fireworks/models/llama-v3p1-70b-instruct
# 2. groq-prod / llama-3.1-70b-versatile
# 3. ollama-local / llama3.1:70b

response = waxell.llm.call(
model="group:cheap-llama-70b",
messages=[...],
)
# If Fireworks rate-limits, dispatch retries against Groq.
# If Groq is down, falls through to local Ollama.
# Capability filter drops Ollama if you passed tools=[].

Fallback walks on retryable errors only (rate limits, 5xx, connection errors, NotFound). Auth errors, BadRequest, and unknown errors raise immediately — they're caller bugs, not provider hiccups.

Capability overrides

Sometimes a model that "should" support tools doesn't on a specific provider. Override per-(instance, model) at /settings/llm-routing:

override:
instance: together-prod
model: llama-3.1-70b-instruct
native_tools: false # tri-state: true / false / null

The dispatcher's capability filter respects overrides as veto: an explicit false skips this candidate even if its instance baseline says true. null means "no override; defer to baseline."

Inspect what would happen

wax llm call --model gpt-4o --show-config

Prints the resolved chain, which entries pass the capability filter, whether the env var is set, and the candidate's base_url — without actually dispatching.

Resolved chain for 'gpt-4o' (mode=chat)
┌──────────────────────────────────────────────────────────────────┐
│ # │ instance_id │ kind │ base_url │ env set? │ passes? │
├──────────────────────────────────────────────────────────────────┤
│ 1 │ oai-prod │ openai │ (SDK default) │ ✓ │ ✓ │
└──────────────────────────────────────────────────────────────────┘

Useful for:

  • Debugging "why is this routing to provider X?"
  • Verifying secret_ref env vars are set before running batch jobs
  • Confirming capability filter behavior under tool / JSON modes

What gets recorded

Every waxell.llm.call(...) produces one LlmCallRecord in your controlplane with:

  • provider (e.g. "openai_compat")
  • provider_instance_id (e.g. "fireworks-prod")
  • model (the resolved provider model id)
  • tokens_in, tokens_out, cost
  • dispatch_source: "observe-sdk" (for analytics distinguishing observe-side dispatch from runtime dispatch)
  • Plus any fallback_chain metadata if the call walked past a retryable error before succeeding

The same LlmCallRecord ingest path stamps last_success_at on the provider instance — your "is this Fireworks instance healthy?" analytics work the same whether the call went through observe SDK dispatch or the Waxell runtime.

Coexistence with raw SDK calls

Your existing direct SDK calls — openai.chat.completions.create(...), anthropic.messages.create(...) — keep working unchanged. The auto-instrumentor records them as before. The dispatcher sets a context-var around its own SDK call so the instrumentor doesn't double-record when both code paths run.

You can mix freely: dispatch when you want fallback / groups, direct SDK calls everywhere else. The tracing and cost attribution are unified.

Provider extras

Install only the SDKs you need:

pip install 'waxell-observe[openai]'      # OpenAI + all OpenAI-compat
pip install 'waxell-observe[anthropic]' # Anthropic
pip install 'waxell-observe[fireworks]' # alias for [openai]
pip install 'waxell-observe[together]' # alias for [openai]
pip install 'waxell-observe[xai]' # alias for [openai]
pip install 'waxell-observe[bedrock]' # boto3
pip install 'waxell-observe[vertex]' # google-cloud-aiplatform
pip install 'waxell-observe[gemini]' # google-generativeai
pip install 'waxell-observe[cohere]' # cohere
pip install 'waxell-observe[mistral]' # mistralai
pip install 'waxell-observe[groq]' # groq

# Or install the whole thing
pip install 'waxell-observe[all-providers]'

Most "OpenAI-compatible" providers (Fireworks, Together, Groq, xAI, NVIDIA, Mistral, AI21, Replicate, Ollama, vLLM, HF TGI) work with just the openai SDK because they speak the OpenAI HTTP wire format on a different base_url. The dispatcher handles the base_url override transparently.

When to use this vs the runtime

Use observe SDK dispatch (this) when…Use the Waxell runtime when…
You have an existing agent (LangGraph, LangChain, ad-hoc)You're building a new agent from scratch
You want fallback + groups but not durable executionYou need durable execution + replay
You want to add Waxell to a serverless functionYou want supervised + governed agent fleets
You want minimal migration costYou want the full governance surface

The data layer is shared — the same provider instances, groups, and capability overrides feed both adoption modes. You can start with observe SDK dispatch and graduate to the runtime later without re-configuring providers.

Reference