Solwyn
Reference for the Solwyn synchronous client wrapper
import os
from openai import OpenAI
from solwyn import Solwyn
with Solwyn(
OpenAI(),
api_key=os.environ["SOLWYN_API_KEY"],
) as client:
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello!"}],
)
print(response.choices[0].message.content)Synchronous client wrapper. Wraps an OpenAI, Anthropic, Google Gemini, or Amazon Bedrock (bedrock-runtime) client — or an openai.OpenAI client pointed at any OpenAI-compatible endpoint via base_url — with budget enforcement, circuit breaking, and metadata reporting.
Constructor
Solwyn(
client,
*,
api_key: str | None = None,
model: str | None = None,
provider: str | None = None,
fallback: list[tuple] | None = None,
default_params: dict | None = None,
selection_policy: SelectionPolicy | None = None,
**config_kwargs,
)Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
client | openai.OpenAI | anthropic.Anthropic | genai.Client | boto3 bedrock-runtime client | Yes | The LLM provider client to wrap. Provider is auto-detected — including OpenAI-compatible endpoints via the client's base_url. Becomes the primary entry in the failover chain. |
api_key | str | None | Yes (kwarg or env) | Solwyn API key. Must match sk_proj_<64 lowercase hex chars>. Falls back to SOLWYN_API_KEY if omitted. |
model | str | None | No | Model for the primary entry. Optional for single-provider use (the per-call model wins); set it when configuring a fallback chain. |
provider | str | None | No | Explicit provider identity for the primary client, for endpoints auto-detection cannot name (e.g. provider="vllm" on a non-default port). An override relabels attribution within the client's API dialect only; an unknown or dialect-mismatched value raises ConfigurationError at construction. |
fallback | list[tuple] | None | No | Failover chain entries: (client, model), (client, model, default_params), or (client, model, default_params, provider) tuples, in attempt order. See Provider Failover. |
default_params | dict | None | No | Global fill-absent request params applied to every entry (per-entry default_params wins). |
selection_policy | SelectionPolicy | None | No | Candidate ordering policy. Defaults to HealthBasedPolicy; LatencyPolicy is also available. |
**config_kwargs | No | Additional configuration options. See SolwynConfig. |
api_key, model, provider, fallback, default_params, and selection_policy are all keyword-only arguments. If SOLWYN_API_KEY is present in the environment, you can omit api_key:
# With SOLWYN_API_KEY set in the environment:
client = Solwyn(OpenAI())ConfigurationError is raised if neither the kwarg nor the corresponding environment variable is set, or if the format is invalid.
Raises
ConfigurationError-- Invalid API key format, malformedfallbackspec, unknown config field, or an invalidprovider=override (unknown name, unrecognized client, or a dialect mismatch between the override and the detected client). Also raised at call time wheninvoke_model/invoke_model_with_response_streamis called on a Bedrock client (field="invoke_model").UntranslatableRequestError-- (at call time) a cross-provider failover hop cannot translate the request shape.UntranslatableModelError-- (at call time) a fallback entry has no model configured for its provider.
Example
import os
from anthropic import Anthropic
from openai import OpenAI
from solwyn import Solwyn
client = Solwyn(
OpenAI(),
model="gpt-4o",
api_key=os.environ["SOLWYN_API_KEY"],
budget_mode="hard_deny",
fallback=[(Anthropic(), "claude-sonnet-4-5", {"max_tokens": 1024})],
)Methods
close()
client.close() -> NoneShuts down the metadata reporter and closes HTTP connections. Always call close() when you are done with the client, or use a context manager.
Calling close() flushes any pending metadata events to Solwyn Cloud before shutting down.
Context manager
Solwyn supports use as a context manager via with:
import os
from openai import OpenAI
from solwyn import Solwyn
with Solwyn(
OpenAI(),
api_key=os.environ["SOLWYN_API_KEY"],
) as client:
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello!"}],
)
# close() is called automatically on exitCall surfaces
The call surface depends on the wrapped provider client:
OpenAI
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello!"}],
)Anthropic
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello!"}],
)Google Gemini
response = client.models.generate_content(
model="gemini-2.0-flash",
contents="Hello!",
)Amazon Bedrock
response = client.converse(
modelId="us.anthropic.claude-3-5-sonnet-20241022-v2:0",
messages=[{"role": "user", "content": [{"text": "Hello!"}]}],
inferenceConfig={"maxTokens": 1024},
)converse() requires the modelId keyword argument (TypeError otherwise) and returns the raw boto3 Converse dict untouched. converse_stream(...) returns the boto3 contract dict whose result["stream"] is Solwyn's wrapped stream — see Streaming for the close obligation on early abandonment.
invoke_model and invoke_model_with_response_stream raise ConfigurationError (field="invoke_model") on a Bedrock client instead of silently bypassing budget tracking. Use the Converse API (it covers every chat model Bedrock hosts), or call the unwrapped boto3 client directly for deliberately untracked calls. See Amazon Bedrock.
OpenAI-compatible providers
OpenAI-compatible providers (xAI, Groq, Azure OpenAI, Ollama, ...) use the OpenAI call surface. chat.completions.create, embeddings.create, images.generate / images.edit, audio.transcriptions.create, and audio.speech.create are intercepted on the OpenAI dialect: embedding calls are budget-checked and priced server-side from the response's usage.prompt_tokens (or a flagged length estimate when the endpoint omits usage), recorded as a cost event with modality embedding; image generation and edits are budget-checked and recorded with modality image, priced from the request's image count (these endpoints return no usage) at the model's per-image rate; audio is budget-checked and recorded with modality audio, transcriptions priced from usage token buckets or the whole-second duration a JSON response_format reports, text-to-speech priced from the input character count measured inside the firewall. Token-billed TTS models with no usage (gpt-4o-mini-tts) and audio.translations warn once per process, then pass through untracked; video generation (videos.create, Sora) is OpenAI-native only, so on a compat endpoint it fails loud with UnsupportedSurfaceError. Other client surfaces (the Responses API as a call surface, etc.) pass through to the underlying client untracked.
All keyword arguments are passed through to the underlying provider client. The response object is the same type returned by the provider SDK.
Attribute passthrough
Any attribute not intercepted by Solwyn is passed through to the underlying provider client:
# Access OpenAI's models API directly
models = client.models.list()Interception behavior
For each intercepted call, Solwyn:
- Estimates input tokens from message text
- Checks budget with Solwyn Cloud (or local fallback)
- Orders the failover chain via the selection policy, dropping providers whose circuit is open
- Walks the chain: for each candidate, translates the request if the dialects differ (same-dialect cross-provider hops pass through natively, with request sanitization), then calls the provider client
- Extracts token details from the response
- Confirms actual usage with Solwyn Cloud
- Reports metadata (non-blocking)
If the budget check fails in hard_deny mode, BudgetExceededError is raised before step 4. If every candidate is unavailable or the chain is exhausted, ProviderUnavailableError is raised. A cross-provider hop that cannot translate the request raises UntranslatableRequestError (or UntranslatableModelError) and aborts the chain before any network call.
Provider exceptions from the underlying SDK are not caught -- when the chain cannot recover, the originating provider exception propagates directly to your code.