OpenAI-compatible providers
Use Solwyn with any OpenAI-compatible endpoint — xAI, DeepSeek, Mistral, Groq, Z.AI, Together, Azure OpenAI, OpenRouter, Ollama, vLLM, and more
Any provider that speaks the OpenAI Chat Completions dialect works with Solwyn: point an openai.OpenAI client at the endpoint via base_url and wrap it as usual. Solwyn detects the real provider from the URL, so budgets, per-agent attribution, failover, and the cost dashboard all see the actual provider (e.g. groq) -- not "openai":
from openai import OpenAI
from solwyn import Solwyn
client = Solwyn(
OpenAI(base_url="https://api.groq.com/openai/v1", api_key="gsk_..."),
api_key="sk_proj_...",
)
response = client.chat.completions.create(
model="llama-3.3-70b-versatile",
messages=[{"role": "user", "content": "Hello!"}],
)Available from SDK v0.1.7. No new install extras -- compat providers use the same openai client package as the OpenAI provider, installed via pip install "solwyn[openai]".
Using Together's native Together or AsyncTogether client instead? See the dedicated Together AI provider guide. The OpenAI-client route below remains supported when you intentionally use Together's compatible endpoint.
Auto-detected providers
Detection runs at construction, from the client's base_url (or, for Azure, the client class). Each provider also carries its own streaming-usage policy -- Solwyn injects stream_options={"include_usage": True} only where that is documented-safe:
| Provider | Detected from | Streaming usage |
|---|---|---|
| xAI (Grok) | api.x.ai | automatic (final chunk); stream_options is never sent -- xAI rejects it |
| DeepSeek | api.deepseek.com | include_usage injected |
| Mistral | api.mistral.ai | stream_options never sent (strict validation); final-chunk usage or estimate |
| Qwen (DashScope compatible mode) | dashscope.aliyuncs.com / dashscope-intl / dashscope-us | include_usage injected |
| Z.AI | api.z.ai | include_usage injected |
| Groq | api.groq.com | include_usage injected; legacy x_groq.usage final-chunk fallback also handled |
| Together AI (native client guide) | api.together.xyz / api.together.ai | automatic terminal usage when present; stream_options is never injected; otherwise estimate |
| Fireworks | api.fireworks.ai | automatic (final chunk) |
| Perplexity (Sonar) | api.perplexity.ai | usage rides streamed chunks; stream_options never sent |
| Azure OpenAI | *.openai.azure.com / *.cognitiveservices.azure.com, or AzureOpenAI / AsyncAzureOpenAI client class | include_usage injected -- skipped for "on your data" data_sources requests, which reject it |
| OpenRouter | openrouter.ai | automatic (final chunk); stream_options is deprecated there |
| Ollama | localhost:11434 | include_usage injected (older versions ignore it → estimate) |
| vLLM | localhost:8000 | include_usage injected |
| LM Studio | localhost:1234 | include_usage injected (older versions omit usage → estimate) |
| Anything else | any non-OpenAI base_url | generic openai_compatible; stream_options never sent |
Notes on detection:
- Local servers (Ollama, vLLM, LM Studio) are recognized by their conventional default ports on a local host (
localhost,127.0.0.1,0.0.0.0,::1). When identity is guessed from a port, the SDK logs a one-time INFO message naming the detected provider (never the URL) and suggestingprovider=if the guess is wrong. A non-default port still works via the generic catch-all -- use the explicit override to name it. - The catch-all claims any
http(s)base URL that is not OpenAI's own (api.openai.com, including regionaleu./us.subdomains) and not a named provider above. A client with no custombase_urlremains plainopenai. - Model prefixes supplement URL detection for providers with distinctive naming (
grok-,deepseek-, the Mistral family,qwen/qwq-/qvq-,glm-,accounts/fireworks/,sonar). Providers that serve shared open-model catalogs (Groq, Together, OpenRouter, local servers) deliberately have no model prefixes -- a barellama-3-8bis never silently claimed for any of them.
Explicit provider identity
For endpoints auto-detection cannot name -- e.g. vLLM on a non-default port, or a private gateway -- pass the provider explicitly. On the constructor for the primary client, or as the 4th element of a fallback spec:
client = Solwyn(
OpenAI(base_url="http://gpu-box:8080/v1", api_key="-"),
api_key="sk_proj_...",
provider="vllm",
fallback=[
(OpenAI(base_url="https://openrouter.ai/api/v1", api_key="sk-or-..."), "openrouter/auto"),
(other_client, "my-model", {}, "ollama"),
],
)Fallback specs accept (client, model), (client, model, default_params), or (client, model, default_params, provider).
An override relabels attribution within the client's API dialect -- it cannot make an Anthropic client speak the OpenAI wire shape. The SDK fails loud at construction with solwyn.exceptions.ConfigurationError for:
- an unknown provider name (the message lists the known values),
- a cross-dialect override (e.g.
provider="anthropic"on anopenai.OpenAIclient), - an override on a client object that is not a recognized provider SDK client,
- a malformed fallback spec.
See the exceptions reference for details.
Z.AI
Z.AI's OpenAI-compatible endpoint serves the GLM model family. Point the client at it and wrap as usual — detection keys off the api.z.ai host, with glm- model prefixes as a supplement:
client = Solwyn(
OpenAI(base_url="https://api.z.ai/api/paas/v4", api_key="..."),
api_key="sk_proj_...",
)
response = client.chat.completions.create(
model="glm-4.6",
messages=[{"role": "user", "content": "Hello!"}],
)Tracked calls are attributed as provider="zai". Z.AI is priced for text only — the provider × modality matrix has the full picture. Streamed calls settle on exact usage, not estimates: Z.AI supports stream_options.include_usage, so Solwyn injects it and reads usage from the final chunk.
Z.AI detection and streaming-usage injection require Solwyn 0.1.11 or newer.
Azure OpenAI
Azure clients are detected by class (AzureOpenAI / AsyncAzureOpenAI) or by host suffix, and attributed as provider="azure_openai".
Azure pricing follows the one thing an Azure request actually carries: your deployment name, an arbitrary string you chose — not a model catalog id. A deployment named after the OpenAI catalog model it serves (the common convention — a deployment called gpt-4o serving gpt-4o) prices at OpenAI catalog rates. Any other deployment name is accepted and recorded unpriced — real spend the platform declined to guess at, visible on the dashboard's unpriced lane, never rejected and never a silent $0. Name deployments after their catalog models if you want priced events.
Streaming usage and estimation
Sync and async streaming are fully supported (AsyncSolwyn mirrors Solwyn). Token accounting for a stream resolves in three tiers:
- Standard usage block -- the last chunk whose usage parses to non-zero counts wins; zeroed placeholder blocks never latch.
- Groq legacy
x_groq.usage-- the older final-chunk shape is read when the standard block is absent. - Flagged estimation -- if the provider reports no usage at all, the SDK falls back to a length-based estimate: input tokens from the pre-call length estimate, output tokens from accumulated delta character lengths at 4.0 characters per token.
Estimation is explicitly marked: the wire payload carries token_details.is_estimated = true, and the SDK logs a one-time WARNING per provider. Budgets still enforce, but per-call costs are approximate. Degraded accounting is loud and flagged, never silently zero.
Non-streaming responses behave the same way: missing usage, a zeroed usage block alongside real content, or garbage counts (negative, non-integer) fall back to flagged estimation. Provider-reported non-zero usage is never overridden, and an all-zero block on a genuinely empty response is taken as provider truth, not estimated. Usage extraction never raises -- a deliverable response is never settled as a failure over broken usage metadata.
is_estimated is serialized only when true, so payloads from providers that always report usage are unchanged.
stream_options drop-in contract
The injection policy in the table above is Solwyn's own behavior. A stream_options you pass explicitly always reaches your configured provider untouched. It is stripped only when a cross-provider failover hop lands on a provider known to reject it (and the data_sources exception applies on hops onto Azure).
Token detail fields
Compat adapters reuse the full OpenAI Chat Completions extractor: input_tokens, output_tokens, cached_input_tokens (from prompt_tokens_details.cached_tokens), reasoning_tokens, audio input/output tokens, and accepted/rejected prediction tokens -- see the OpenAI token detail table for the exact source fields. A returned service_tier is captured (bounded and string-only), as on OpenAI.
Compat providers carry no per-region pricing contract, so no provider region is reported.
Pricing
The SDK never computes cost. It reports the served (provider, model) pair verbatim -- for OpenRouter that is the full model slug (e.g. anthropic/claude-sonnet-4.5) -- and the Solwyn Cloud API prices it server-side. Models unknown to the pricing catalog surface as unpriced on the dashboard, never silently costed at $0.
Failover
Compat providers participate fully in provider failover:
- Same-dialect hops (e.g. Groq → OpenRouter) are native passthrough -- tools, JSON mode, and streaming all survive, with no canonical-subset restriction.
max_completion_tokensis rewritten tomax_tokensfor targets that need the legacy key (and inverselymax_tokens→max_completion_tokensfor OpenAI/Azureo1/o3/o4/gpt-5targets). - Endpoint-scoped params (
extra_headers/extra_query/extra_body) are stripped on cross-provider hops -- they carry gateway credentials authored for the original endpoint -- and the target entry's owndefault_paramsversions re-apply. They are untouched on the primary and on same-provider model swaps. - Cross-dialect hops (e.g. Groq → Anthropic) use the standard canonical translation subset. Tool-using streams across dialects fail loud with
UntranslatableRequestErrorbefore any network call.
Known limitations
Circuit-breaker health, latency signals, and failover labeling key off the provider name. Two chain entries that resolve to the same name -- two Azure resources, or two unnamed gateways both detected as openai_compatible -- share one health domain, are reported as model fallbacks of each other, and skip cross-provider request sanitization (stream_options stripping, the max_completion_tokens rewrite, endpoint-scoped param stripping). A header authored for the first endpoint then reaches the second untouched and can fail there. Give distinct endpoints distinct provider= identities -- on the constructor, or as the 4th element of a fallback spec.
chat.completions.create, embeddings.create, images.generate, audio.transcriptions.create, and audio.speech.create are intercepted on the OpenAI dialect. Embedding calls are budget-checked before the request and priced server-side from the response's usage.prompt_tokens, recorded as a cost event with modality embedding; when a compat endpoint returns no usable usage, the SDK falls back to a length-based estimate flagged is_estimated = true -- never a silent $0. Image generation is budget-checked before the request and recorded as a cost event with modality image; because compat image endpoints return no usage, its billing quantities are request-derived (image count × the model's per-image rate) -- measured inside the privacy firewall from config values (the n, size, and quality you pass), never the prompt and never image bytes. Audio calls are budget-checked and recorded as a cost event with modality audio: transcriptions price from their usage token buckets (token-billed models such as gpt-4o-transcribe) or from the whole-second audio duration on the provider's usage block (Whisper-style models -- present only on a JSON response_format; a text/srt/vtt call is recorded but unpriced and warns once), and text-to-speech prices from the input character count measured inside the privacy firewall, never the text itself. Groq is fully covered here -- its Whisper transcription prices per hour of audio and its Orpheus TTS per input character. Token-billed TTS models that report no usage (gpt-4o-mini-tts) and audio.translations warn once per process and then pass through untracked. Video generation (videos.create, Sora) is OpenAI-native only, so on an OpenAI-compatible endpoint it is unsupported — videos.create fails loud with UnsupportedSurfaceError rather than passing through. Other client surfaces (the Responses API as a call surface, etc.) pass through to the underlying client untracked.
Privacy
The same privacy posture applies as for every provider: Solwyn is a wrapper, not a proxy -- calls go direct to the provider, and prompts and responses are never captured, logged, or transmitted. Compat-specific guarantees:
- Length-based estimation sums string lengths of message text, reasoning content, and tool-call arguments without concatenating, storing, or logging any content -- only an irreversible character count is used.
- The streaming accumulator extracts usage and
service_tieras chunks pass through and never retains chunk objects (deltas carry content). - The local-port detection log names the detected provider only, never the
base_url, which may carry credentials. - Provider credentials live only on your client objects -- Solwyn's provider configuration cannot carry an
api_keyorbase_url.