Exceptions
Reference for Solwyn SDK exceptions — BudgetExceededError, RunStoppedError, ProviderUnavailableError, ConfigurationError, the translation and surface errors, and the tag-clamp warning
import os
from openai import OpenAI
from solwyn import (
BudgetExceededError,
ConfigurationError,
ProviderUnavailableError,
RunStoppedError,
Solwyn,
)
try:
client = Solwyn(
OpenAI(),
api_key=os.environ["SOLWYN_API_KEY"],
budget_mode="hard_deny",
)
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello!"}],
)
except ConfigurationError as e:
print(f"Bad config: {e.message} (field: {e.field})")
except RunStoppedError as e:
print(f"Run stopped: {e.agent_run_id} ({e.source}: {e.reason})")
except BudgetExceededError as e:
print(f"Over budget: ${e.current_usage:.2f} / ${e.budget_limit:.2f}")
except ProviderUnavailableError as e:
print(f"No provider could serve the call; candidates: {e.attempted}")Every exception below is importable from the top-level solwyn package. UnsupportedSurfaceError joined them in SDK v0.3.0; RunStoppedError, UntrackedSpendSurfaceError, CoverageMismatchError, and SurfaceInspectionError in v0.6.0; the SolwynTagsClampedWarning warning class in v0.5.0.
BudgetExceededError
Raised when a request would exceed a configured budget limit. Only raised in budget_mode="hard_deny" mode — or when a scoped rule in hard-deny mode denies the call on an alert_only project. In "alert_only" mode, the SDK logs a warning instead of raising.
When raised
- Before an LLM call, when the pre-flight budget check returns "denied" and the effective mode is
hard_deny. - New in 0.3.0: the same check also enforces per-run budget caps — a call denied against the cap on its agent run raises this same exception, with
budget_period="agent_run". - New in 0.5.0: scoped budgets — a rule keyed on a model, a provider, a run, or a spend tag denies with
budget_periodset to the rule's scope. A tag denial is selector-scoped and never sticky. - The dates above are when each denial shipped. The label itself is only exposed on the exception from 0.6.0: SDK 0.3.0 through 0.5.x report
budget_period="unknown"for every denial, so abudget_periodcomparison written against an older SDK never fires.
A stopped run does not raise this exception; it raises RunStoppedError, which is deliberately not a subclass.
Attributes
| Attribute | Type | Description |
|---|---|---|
project_id | str | None | Project identifier resolved by the API, if available. |
budget_limit | float | Configured spending cap in dollars. |
current_usage | float | Amount already consumed in the current period. |
estimated_cost | float | Estimated cost of the blocked request. |
budget_period | str | What denied the call: a project period ("daily", "weekly", "monthly"), a per-run cap ("agent_run"), a scoped rule ("model", "provider", "agent_run", "tag"), or "unknown" if not provided by the API. |
mode | str | Active budget mode when the error was raised — "hard_deny" for a scoped hard-deny rule even on an alert_only project. |
Example
import os
from openai import OpenAI
from solwyn import BudgetExceededError, Solwyn
client = Solwyn(
OpenAI(),
api_key=os.environ["SOLWYN_API_KEY"],
budget_mode="hard_deny",
)
try:
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Summarize this document."}],
)
except BudgetExceededError as e:
print(f"Budget exhausted: ${e.current_usage:.4f} used of ${e.budget_limit:.2f} cap")
print(f"Blocked request estimated cost: ${e.estimated_cost:.4f}")
print(f"Period: {e.budget_period}")
print(f"Mode: {e.mode}")
client.close()RunStoppedError
SDK v0.6.0+. Raised when an intercepted call is made inside a solwyn.run(...) scope whose run has been stopped — by an operator on the dashboard, or by the SDK's own velocity detector under velocity_mode="deny". It is a SolwynError but not a BudgetExceededError, so a budget-denial handler cannot swallow or retry through it. It is raised regardless of budget_mode and fail_open, and a retained stop keeps raising through a Solwyn outage.
When raised
- Before the budget check when the SDK's velocity detector stopped the run; after the check for an operator stop — a live check is still issued.
- From the iteration of a stream that was open when the stop arrived: the SDK discards the next chunk, closes the provider stream, settles once with the usage observed so far, and raises.
- Never for a non-streaming call already in flight — that call completes and settles normally.
Attributes
| Attribute | Type | Description |
|---|---|---|
agent_run_id | str | The stopped run. |
source | str | "server" for an operator stop delivered by Solwyn Cloud, "local_velocity" for a stop the SDK's detector issued. |
reason | str | "manual_kill" for a server stop; "velocity:repeat_size" or "velocity:monotonic_growth" for a local one. |
The message reads Agent run <id> was stopped (<source>: <reason>). Read the retained state with solwyn.current_run_terminated() or solwyn.run_termination(run_id), which returns an immutable RunTermination(reason, source, at_monotonic) value, and lift a local stop for future calls with solwyn.clear_run_termination(run_id). See Run control.
Example
import solwyn
from solwyn import BudgetExceededError, RunStoppedError
with solwyn.run("nightly-report"):
for chunk in work:
try:
client.chat.completions.create(model="gpt-4o", messages=[...])
except RunStoppedError as e:
log.error("run %s stopped by %s (%s)", e.agent_run_id, e.source, e.reason)
break # do not retry inside a stopped run
except BudgetExceededError:
... # a budget denial is a different decisionProviderUnavailableError
Raised when no candidate could be dispatched — every circuit open, or the failover window expired. If every candidate was attempted and failed, the last provider's exception propagates instead.
When raised
- When a provider's circuit breaker is
OPENand not yet eligible for a recovery probe, the router drops it. If that leaves no candidate to serve the call, the SDK raises this error rather than sending a request that is bound to fail. - When the
failover_total_timeoutwindow runs out before any candidate could be dispatched — the message isfailover deadline expired. - For a Responses call whose primary breaker is open: Responses never fail over, so the candidate list is empty and
attemptedis[].
Attributes
| Attribute | Type | Description |
|---|---|---|
provider | str | None | Retained for compatibility; never set by the current SDK — always None. |
circuit_state | str | None | Retained for compatibility; never set by the current SDK — always None. |
attempted | list[str] | None | The candidate providers selected when the SDK gave up — some or none may have been dispatched (empty when every breaker was already open). Never contains prompt content. |
Example
import os
from openai import OpenAI
from solwyn import ProviderUnavailableError, Solwyn
client = Solwyn(
OpenAI(),
api_key=os.environ["SOLWYN_API_KEY"],
)
try:
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello!"}],
)
except ProviderUnavailableError as e:
print(f"No provider could serve the call: {e}")
print(f"Candidates when the SDK gave up: {e.attempted}")
client.close()UntranslatableRequestError
Raised when a cross-provider failover hop cannot translate a request from the source provider's dialect into the target's. Raised before any network call, aborting the whole candidate chain. Carries only structural labels describing what could not be translated — never the offending value and never prompt content.
When raised
- During a cross-provider hop, when a request feature has no equivalent on the target provider (e.g. an OpenAI
response_formatAnthropic cannot express, a dangling tool call, or a parameter outside the target's accepted range). - On hops involving a Bedrock entry, with fixed structural labels such as
bedrock.guardrail_config(a safety feature a hop would otherwise silently strip),cache_control(BedrockcachePointblocks),missing_max_tokens(inferenceConfig.maxTokensis required for a cross-provider hop — the SDK never invents an output bound),image.opaque_handle(s3Locationimage sources), andunsupported_kwarg.<key>for any unrecognized keyword. - For cross-dialect tool-using streams (e.g. an OpenAI-dialect tool stream failing over to Anthropic) — raised pre-dispatch, before any network call. Same-dialect hops between OpenAI-compatible providers are native passthrough and never raise this.
Attributes
| Attribute | Type | Description |
|---|---|---|
source | str | Provider the request was authored against (e.g. "openai"). |
target | str | Provider the request was being translated toward (e.g. "anthropic"). |
feature | str | Structural token naming the untranslatable shape (e.g. "response_format", "dangling_tool_call"). Never the offending value or prompt content. |
Example
import os
from anthropic import Anthropic
from openai import OpenAI
from solwyn import Solwyn, UntranslatableRequestError
client = Solwyn(
OpenAI(),
model="gpt-4o",
api_key=os.environ["SOLWYN_API_KEY"],
fallback=[(Anthropic(), "claude-sonnet-4-5", {"max_tokens": 1024})],
)
try:
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello!"}],
)
except UntranslatableRequestError as e:
print(f"Cannot translate {e.feature} from {e.source} to {e.target}")
client.close()UntranslatableModelError
Raised when a fallback entry has no concrete model configured for its target provider. A model id is a configuration value (not prompt content), so it is safe to surface in the message.
When raised
- During a cross-provider hop, when the target entry's
modelis empty or missing — caught structurally before any network call so the chain aborts like every other translation error.
Attributes
| Attribute | Type | Description |
|---|---|---|
model | str | The model identifier that has no mapping. |
provider | str | The target provider the model was not configured for. |
UnsupportedSurfaceError
Raised when a provider adapter does not serve a requested wrapper surface. New in 0.2.0, alongside the non-text modality surfaces.
Non-chat media surfaces (embeddings, images, audio, video) dispatch through a per-adapter seam; an adapter with no branch for the requested surface raises this instead of letting a spend surface run untracked. Both attributes are configuration values — a surface name and a provider name — never prompt content.
When raised
videos.createon an OpenAI-compatible or native Together client — Solwyn's video interception is wired for OpenAI's Sora only. See Surface coverage.- Any media surface on a client whose adapter serves no seam for it —
embeddings.createon a wrapped Anthropic client, for instance.
The Responses API is not in this set. It is metered on native OpenAI and Azure OpenAI; on every other compatible endpoint the Responses leaves — including create / parse / stream — are untracked and follow your on_unmetered posture (warn once and forward by default); they are not budget-checked and produce no cost event. Under on_unmetered="raise" the error is UntrackedSpendSurfaceError, never this one. On an Anthropic, Google, or Bedrock client client.responses does not exist at all and raises a plain AttributeError.
Attributes
| Attribute | Type | Description |
|---|---|---|
surface | str | The surface that was requested (e.g. "embeddings", "video"). |
provider | str | The provider whose adapter does not serve it. |
Import
UnsupportedSurfaceError is exported from the top-level package (SDK v0.3.0+):
from solwyn import UnsupportedSurfaceErrorOn SDKs before 0.3.0, use the deep import — it still works on current versions:
from solwyn.exceptions import UnsupportedSurfaceErrorUntrackedSpendSurfaceError
SDK v0.6.0+. Raised under on_unmetered="raise" when your code reaches a provider capability Solwyn does not meter — a reviewed-but-unmetered leaf such as moderations.create, or a leaf the SDK has never classified. Raised before any provider I/O: at attribute-access time for a guarded read, or as the first statement of an intercepted method. Nothing is recorded and no advisory report is sent.
It subclasses both SolwynError and AttributeError, so hasattr(client.moderations, "create") and getattr(client.moderations, "create", None) keep their feature-probe semantics under strict mode. Probe the leaf, not the namespace: client.moderations is a namespace and comes back as a guarded resource in every posture, so it is never refused.
Attributes
| Attribute | Type | Description |
|---|---|---|
surface | str | Full dotted capability path, e.g. "responses.input_items.list". |
token | str | The exact acknowledge_untracked token that would admit this surface. |
provider | str | Provider name. |
client_shape | str | The wrapped client's shape, e.g. "openai_sdk", "anthropic_sdk", "google_genai". |
kind | str | "unmetered_spend" (reviewed, not metered) or "unknown" (never classified). |
capability_scope | str | None | "operation", "client", "resource", "raw_response", "arbitrary_endpoint", or None. |
drifted_from_rule_id | str | None | Set when a reviewed rule no longer matches the observed shape. |
The message names the surface, provider, scope, and the fix: Solwyn refused untracked surface 'moderations.create' for provider openai (scope: operation); acknowledge exact token 'moderations.create' or choose on_unmetered='warn'/'allow'. Every attribute is a structural label. See Coverage controls.
CoverageMismatchError
SDK v0.6.0+. Raised by CoverageReport.expect(...) when the coverage classification of a wrapped client differs from a pinned CoverageExpectation or CoverageFingerprint. Only raised where you call solwyn.coverage(client).expect(...) — usually a test.
Attributes
| Attribute | Type | Description |
|---|---|---|
differences | tuple[str, ...] | One entry per drifted category: "<category>: added <rule_id>", "<category>: removed <rule_id>", "<category>: changed <rule_id> (<fields>)", or, for a fingerprint pin, "<category>: fingerprint changed (...)". |
The message is coverage expectation mismatch: followed by the differences joined with ; . See Pinning coverage in CI.
SurfaceInspectionError
SDK v0.6.0+. Raised by solwyn.coverage(client) when the structural inspection of a provider client cannot observe part of its public graph. A RuntimeError, not a SolwynError. Attributes: path (the attribute path being observed), stage (e.g. "public_enumeration", "static_inspection", "depth_exhaustion", "cycle"), and cause_type (the class name of the underlying exception, if any). The message is Unable to observe provider surface '<path>' during <stage>.
ConfigurationError
Raised when SDK configuration is invalid or incomplete — mostly at construction time, with a small set of deliberate call-time cases that each prevent a call from bypassing metering.
When raised
At construction:
- An invalid
api_keyformat, a malformedfallbackspec (field="fallback_specs"), or an unknown config field. - A
provider=pin (constructor argument or 4th fallback-tuple element) naming an unknown provider — the message lists the known values (field="provider"). - A pin whose client family or sync/async mode does not match the actual client object, a pinned client that is not a recognized provider SDK client, or an already-wrapped client passed as
clientor infallback(field="client"). Without aprovider=pin, an unrecognized client raises a bareValueErrorfrom adapter detection (No provider adapter found for client type '...') rather thanConfigurationError. - Invalid client default
tagsor a malformedSOLWYN_TAGSentry (field="tags", SDK v0.5.0+). - An
acknowledge_untrackedtoken that is not an exact untracked leaf on this client — a namespace, a metered or blocked leaf, a wildcard, or a path that does not exist (field="acknowledge_untracked", SDK v0.6.0+). See Acknowledging surfaces. - A non-finite failover timeout — booleans,
NaN,inf— or afailover_hop_read_timeoutthat is not greater than zero (fieldnames the timeout, SDK v0.6.0+).
At call time:
invoke_model,invoke_model_with_response_stream, orstart_async_invokeon a Bedrock-wrapped client (fieldis the method name). This is a budget-bypass guard: those surfaces fail loud instead of letting calls run untracked. Useconverse()/converse_stream(), or the unwrapped boto3 client for deliberately untracked calls. See Amazon Bedrock.- A metered Responses call with
background=True(field="background"), a streamingresponses.parse(field="stream"), or anextra_bodythat overridesmodel,input,instructions,max_output_tokens, orstream(field="extra_body") — SDK v0.6.0+, raised before the budget check. See The Responses API.
Attributes
| Attribute | Type | Description |
|---|---|---|
field | str | None | Configuration field that failed validation. May be None for general errors. |
message | str | Human-readable description of the problem. |
Example
from openai import OpenAI
from solwyn import ConfigurationError, Solwyn
try:
client = Solwyn(
OpenAI(),
api_key="bad_key_format",
)
except ConfigurationError as e:
print(f"Configuration error: {e.message}")
if e.field:
print(f"Invalid field: {e.field}")SolwynTagsClampedWarning
SDK v0.5.0+. A UserWarning subclass — issued through the warnings module, not raised — emitted once per call when the merged spend tags (client defaults, run scope, and per-call solwyn_tags) exceed 10 keys. The SDK keeps the 10 highest-priority keys, drops the rest from that call's attribution, and proceeds. The message is merged tags exceed 10 keys; lower-priority tags were dropped. It cannot abort the call even under warnings.simplefilter("error"). See Bounds, validation, and clamping.
Exception hierarchy
SolwynError
+-- BudgetExceededError
+-- RunStoppedError
+-- ProviderUnavailableError
+-- ConfigurationError
+-- UntranslatableRequestError
+-- UntranslatableModelError
+-- UnsupportedSurfaceError
+-- UntrackedSpendSurfaceError (also subclasses AttributeError)
+-- CoverageMismatchError
RuntimeError
+-- SurfaceInspectionError
UserWarning
+-- SolwynTagsClampedWarningSolwynError is the base class for all Solwyn SDK exceptions. Catch it to handle any error originating from the SDK without distinguishing the specific cause:
from solwyn import SolwynError
try:
response = client.chat.completions.create(...)
except SolwynError as e:
# catches every Solwyn-raised error
...Import
Every class is exported from the top-level package:
from solwyn import (
BudgetExceededError,
ConfigurationError,
CoverageMismatchError,
ProviderUnavailableError,
RunStoppedError,
SolwynError,
SolwynTagsClampedWarning,
SurfaceInspectionError,
UnsupportedSurfaceError,
UntrackedSpendSurfaceError,
UntranslatableModelError,
UntranslatableRequestError,
)UnsupportedSurfaceError was added to the root export in SDK v0.3.0. On earlier versions, import it from solwyn.exceptions instead.