SOLWYN
Guides

Troubleshooting

Symptom-first index for common Solwyn SDK problems in production

This page is a symptom-first index. Find the symptom you are seeing in your application or logs, then follow the resolution.

My calls are not appearing in the dashboard

The most common cause is the metadata reporter being shut down before its background batch flushes.

Resolution:

  1. Confirm client.close() is called when your application exits, or that you used a context manager (with Solwyn(...) as client: / async with AsyncSolwyn(...) as client:).
  2. Look for reporter.ingest_events_rejected warnings (SDK v0.1.7+). If you see them, your batches are reaching Solwyn Cloud but specific events were refused — usually a model Solwyn cannot price yet. See the next section.
  3. Look for the warning Cloud API budget check failed in your logs. If you see it, the SDK could not reach Solwyn Cloud — check your network and SOLWYN_API_URL value.
  4. Verify your SOLWYN_API_KEY is associated with the project ID you are reporting under. The dashboard shows usage per project.
  5. SDK v0.4.0+: look for reporter.spend_events_dropped. That line names exactly how much spend was written off and why (retry_exhausted, shutdown_deadline, overflow, and so on). It is the definitive answer to "how much is missing" — see Undeliverable spend.
  6. SDK v0.4.0+: if a short-lived process is losing spend at exit, raise reporter_shutdown_deadline (default 5.0 seconds). close() counts and drops whatever is still queued when that deadline expires.

If using AsyncSolwyn, you must await client.close() (not client.close()).

Since 0.4.0, delivery is at-least-once — a transient failure is retried rather than dropped — so persistent gaps now point at configuration or connectivity rather than a single unlucky flush. See Spend delivery.

reporter.ingest_events_rejected warnings in my logs

Solwyn Cloud accepted the metadata batch but refused to record specific events in it — this is not a connectivity failure, and the other events in the batch were recorded normally. Logged at WARNING level by the solwyn.reporter logger, one aggregated line per distinct rejection code and model per batch (SDK v0.1.7+):

reporter.ingest_events_rejected: code=unknown_model model=vendor-x-1 count=2 message=Solwyn does not have pricing for model 'vendor-x-1'. File an issue at https://github.com/solwyn-ai/solwyn-python-sdk/issues or contact support — we typically add new models within 24h.

What the codes mean:

  • unknown_model — Solwyn has no pricing entry for that model identifier. Those events were dropped, so their cost is missing from the dashboard.
  • unknown_service_tier — the event carried a service tier Solwyn Cloud does not recognize, so the cost could not be computed at the correct rate.
  • invalid_tags — the event's spend tags broke the per-event bounds (more than 10 keys, a key outside 1–64 characters, a value over 256, or a NUL). The SDK enforces the same bounds before dispatch, so this normally indicates a version skew.
  • tag_cardinality_exceeded — the event would have introduced a tag key beyond the account's 100 active keys, or a value beyond the 1,000 values allowed for one key. Retire unused keys in the dashboard, or stop putting high-cardinality data (ids, timestamps) in tag values.
  • unsupported_modality — a known model whose media configuration Solwyn cannot price (for example a resolution or quality that matches no variant).
  • Rejection codes are server-owned and the set can grow without an SDK release; an unfamiliar code is logged the same way. The message field carries the server's guidance verbatim.

Resolution:

  1. Rejected events are terminal: the SDK logs and drops them, never retries — resubmission would be rejected identically until a pricing entry lands server-side. The corresponding calls stay absent from the dashboard.
  2. For unknown_model, open an issue at github.com/solwyn-ai/solwyn-python-sdk with the model= value from the log line. Pricing lives server-side: once Solwyn adds the entry, subsequent traffic is priced automatically — no SDK upgrade or redeploy on your side. The already-rejected calls are not recovered retroactively.
  3. For unknown_service_tier, include the full log line in the issue — the server's message names the tier it could not match, and support for it is likewise a server-side fix.
  4. No action is needed for the accepted events in the same batch — they are already durable.

For the line-by-line format reference, see Logging.

ConfigurationError: Invalid API key format at construction

Your api_key did not match the required pattern sk_proj_<64 lowercase hex chars>.

Common causes:

  • Trailing whitespace or newline (often from copy-paste). Strip with os.environ["SOLWYN_API_KEY"].strip().
  • Missing sk_proj_ prefix.
  • Using the dashboard's display name instead of the raw key.
  • Quoting the value in your .env file with extra characters.

The error's field attribute is set to "api_key" and the message includes the first 12 characters of what the SDK saw.

Dashboard shows no costs and budget caps aren't enforcing

Your SOLWYN_API_KEY is a read-only key. Solwyn Cloud rejects every SDK write surface — budget checks, usage and metadata reporting, and breaker-state reports — from a read-only key with the same structured permission error. The SDK recognizes that response on its budget-check and reporting paths, and logs it once per process, at ERROR level:

solwyn.configuration_error.read_only_key: the configured API key is read-only; use a full-scope project key for SDK budget enforcement and metadata reporting

After that one log line, the SDK suppresses the repeated generic Cloud-API warnings for the same cause and drops reporting for those requests. Budget checks follow your fail_open setting: on the default fail_open=True they fail open — your provider calls keep working, but enforcement and attribution do not; with fail_open=False the usual local fallback applies and calls can be denied. Detection is exact: only the structured read-only permission response triggers this path; other 401/403 responses surface as the ordinary Cloud API budget check failed warnings below, not this one.

Resolution:

  1. Search application logs (including startup, since this fires once per process) for the solwyn.configuration_error.read_only_key line above.
  2. Swap in a full-scope project key. See API keys.
  3. Redeploy with the new key — budget enforcement and dashboard attribution resume on the next call.

Cloud API budget check failed warnings in my logs

Solwyn Cloud was unreachable for one budget check. This is logged at WARNING level by the solwyn logger.

When to ignore: Single occurrences during transient network issues. With the default fail_open=True, your LLM calls proceeded normally with local tracking.

When to investigate: Persistent failures — every call. Check:

  1. SOLWYN_API_URL is correct (default https://api.solwyn.ai).
  2. The host running your code can reach the API URL (curl -I $SOLWYN_API_URL).
  3. Your API key has not been revoked in the dashboard.

If you set fail_open=False, calls are enforced against the last budget limit Solwyn returned and denied once local spend exceeds it — or denied immediately if Solwyn was never reached.

BudgetExceededError raised when I did not expect it

You configured budget_mode="hard_deny" and a call's pre-flight check returned "denied" from Solwyn Cloud.

Diagnose:

  1. Inspect e.budget_limit and e.current_usage on the exception. If current_usage is at or near budget_limit, the budget genuinely is exhausted.
  2. Check the dashboard for the project's current period spend.
  3. Confirm you are looking at the right project — multi-agent setups often have separate projects per agent.

To allow calls through while you investigate, switch to budget_mode="alert_only" temporarily.

My calls keep raising ProviderUnavailableError

The provider's circuit breaker is open — the dashboard shows this provider as Down — and the SDK is refusing to send calls. Look for Circuit breaker [provider] opened due to failures in your logs (the [name] label is new in 0.4.0; grep for Circuit breaker to match every version). That is the usual cause, but not the only one: if the message reads failover deadline expired, the failover_total_timeout window (default 30 s) ran out before any candidate was dispatched — raise it or shorten the chain.

Resolution:

  1. The circuit closes automatically after the recovery probe succeeds. With defaults, this is roughly 60 seconds (±20% jitter, so up to 72 seconds) + 2 successful probe calls.
  2. To recover faster, restart the process — circuit-breaker state is per-process and resets on restart.
  3. Investigate why the provider was failing — Solwyn does not catch provider exceptions, so the original errors should be visible in your logs from the calls that opened the circuit.

To require more failures before opening (less aggressive), increase circuit_breaker_failure_threshold. To recover faster, decrease circuit_breaker_recovery_timeout. A same-provider model fallback that fails on both the primary and the fallback model still records only one failure against that provider's breaker — failures are deduplicated per provider within a single request — so with the default threshold of 3, the circuit opens after three failed requests.

If you configure a fallback= chain, ProviderUnavailableError is not raised just because one provider is down — a single unhealthy provider should fail over rather than raise. If it raises with a fallback configured, check e.attempted to see which candidates the router had selected when it gave up (it is empty when every breaker was already open).

My dashboard shows failover usage I didn't expect

Some calls failed on the requested provider/model and were served by a fallback entry. The dashboard tags these events: is_model_fallback=true for a same-provider model swap, is_provider_fallback=true for a cross-provider hop, with failover_reason and requested_provider / requested_model describing what changed.

Resolution:

  1. Filter the dashboard by is_model_fallback / is_provider_fallback to see how often each kind of failover fires.
  2. Inspect failover_reason (circuit_open, primary_error, model_fallback, or cost_routed) and failover_error_class to understand why the primary was bypassed. cost_routed is not a failure at all: you configured CostPolicy and a healthy, cheaper provider served the call by design — see Cost-aware routing.
  3. Check application logs for the original primary errors — the SDK surfaces them only when the whole chain is exhausted, so successful failovers won't reach your exception handlers.
  4. If failover fires more than expected, fix the underlying primary issue, or shorten the fallback= chain to surface the original errors directly.

A cross-provider call raises UntranslatableRequestError

A failover hop to a different provider could not translate the request shape (for example an OpenAI response_format that has no Anthropic equivalent). The chain aborts before any network call, so no duplicate request is sent.

Resolution:

  1. Read e.feature, e.source, and e.target — they name what could not be translated, structurally (never the value).
  2. Either keep that call's request shape within what every provider in the chain can serve, or drop the cross-provider entry for calls that rely on a provider-specific feature.
  3. UntranslatableModelError means a fallback entry has no model set for its provider — add a concrete model to that tuple.

My async tests hang on shutdown

AsyncSolwyn started a background reporter task that was never closed.

Resolution:

Always use async with or call await client.close():

# Correct
async with AsyncSolwyn(AsyncOpenAI()) as client:
    ...

# Also correct
client = AsyncSolwyn(AsyncOpenAI())
try:
    ...
finally:
    await client.close()

# Wrong — hangs on shutdown
client = AsyncSolwyn(AsyncOpenAI())
# ... no close()

SDK v0.4.0+: close() is now bounded by a single wall-clock deadline (reporter_shutdown_deadline, default 5.0 seconds) covering the worker join, the final flush, and the last breaker-report cycle. An unreachable Solwyn can no longer hold shutdown open indefinitely — undelivered work is counted and dropped at the deadline instead. If teardown is still slow, lower that deadline; if it is dropping spend you care about, raise it.

Two related 0.4.0 changes worth knowing here:

  • An AsyncSolwyn built without async with used to queue events and settlements silently until close(). Its flush loop now auto-starts on first enqueue. If you see reporter.enqueue_without_event_loop, the client was used outside a running event loop.
  • A process that exits without calling close() at all now flushes on the way out via an exit hook, bounded the same way. It is a safety net, not a substitute for close().

SDK v0.6.0+: the opt-in pytest fixtures close the client for you, in a finally, even when the test body raises. Enable them with pytest_plugins = ["solwyn.testing.pytest_plugin"] and request solwyn_test_client — see Testing budget enforcement.

Streaming tokens are not being counted

The provider's stream did not emit usage data, or your code aborted the stream before the usage chunk arrived.

Resolution:

  • For OpenAI: Solwyn injects stream_options={"include_usage": True} automatically. If you pass your own stream_options dict, Solwyn preserves your other keys and force-sets include_usage=True — even an explicit include_usage=False is overridden — so missing usage on a direct OpenAI stream is not caused by stream_options.
  • For OpenAI-compatible providers: Solwyn injects include_usage only where the provider documents support for it. It never injects for xAI, Mistral, Together, Fireworks, Perplexity, OpenRouter, or the generic openai_compatible catch-all — those endpoints reject, deprecate, or simply do not document the parameter, and most deliver usage automatically in the final chunk. If no usage arrives at all, the SDK reports length-based estimated counts flagged token_details.is_estimated=true with a one-time WARNING — counts are present but approximate, not missing. See OpenAI-compatible providers.
  • For Anthropic and Google: usage events are emitted by default — no action needed.
  • For Bedrock: usage arrives in the stream's terminal metadata event. Fully consume result["stream"], or on early abandonment call result["stream"].close() (or use with result["stream"]:) so the reservation settles with the observed usage. A stream that settles at zero tokens after real traffic logs Bedrock stream settled at zero tokens — see Streaming.
  • For early aborts on the other providers: wrap the stream in with stream: (sync) or async with stream: (async) so Solwyn settles with the tokens it observed before the break. See Streaming.

The SDK is logging a lot — how do I quiet it?

All Solwyn log messages are emitted under the solwyn logger. Set its level to ERROR to silence WARNING-level messages:

import logging
logging.getLogger("solwyn").setLevel(logging.ERROR)

For the full list of messages and what each one means, see Logging.

RunStoppedError raised inside a run

SDK v0.6.0+. The active solwyn.run(...) was stopped — not budget-denied. Read e.source:

  • "server" with reason="manual_kill" — an operator pressed Stop on the dashboard's Agents tab. The stop is permanent for that run id and survives a Solwyn outage. Start a new run; do not retry inside the old one.
  • "local_velocity" with reason="velocity:repeat_size" or "velocity:monotonic_growth" — the SDK's own detector decided the run was looping and velocity_mode="deny" is set. Inspect the loop, or lift the stop for that run id with solwyn.clear_run_termination(run_id) if it was a false positive.

RunStoppedError is not a BudgetExceededError, so an except BudgetExceededError block will not catch it. See Run control.

velocity.flagged warnings in my logs

SDK v0.6.0+. Local velocity detection flagged the active run: velocity.flagged: rule=repeat_size run=run_.... In the default velocity_mode="warn" this is advisory — the call proceeds and the run shows a Flagged badge on the Agents tab. The rules are repeat_size (the same model with the same-sized prompt, repeatedly), monotonic_growth (prompts growing steadily, as when a loop re-feeds its own output), and rate_acceleration (a call-rate spike; advisory only).

Resolution:

  1. Decide whether the pattern is a bug. A retry loop that re-sends an identical prompt, or an agent that appends every turn to its context, is exactly what the rules exist to catch.
  2. If it is expected for this workload, raise the thresholds (velocity_repeat_count, velocity_growth_streak, ...) or set velocity_mode="off". See Velocity settings.
  3. To make the flag a stop, set velocity_mode="deny" — the run then raises RunStoppedError on its next call.

"exposes untracked surface" warnings, or UntrackedSpendSurfaceError

SDK v0.6.0+. Your code reached a provider capability Solwyn does not meter — files, batches, moderations, models.list, a with_raw_response helper, or something new in the provider SDK. Under the default on_unmetered="warn" the SDK logs once per surface per process and forwards the call; under "raise" it refuses with UntrackedSpendSurfaceError before any provider I/O. Before 0.6.0 most of these surfaces passed in silence, which is why the warning is new to you after upgrading.

Resolution:

  1. If the surface carries no spend you care about, acknowledge it: acknowledge_untracked={"models.list"} (or SOLWYN_ACKNOWLEDGE_UNTRACKED=models.list). The exception's token attribute is the exact string to add. Tokens are validated at construction, so a typo fails immediately with ConfigurationError(field="acknowledge_untracked").
  2. If you want the old silence for everything, set on_unmetered="allow" — that silences the log only; add report_untracked_surfaces=False if you also want no advisory report.
  3. If the surface should be metered, tell Solwyn — the advisory report the SDK already sent is what the Providers tab's "Surfaces Solwyn does not meter yet" card lists.

See Coverage controls.

ConfigurationError from a Responses API call

SDK v0.6.0+. Native OpenAI and Azure responses.create / parse / stream are metered, and three request shapes are refused before the budget check because they cannot be metered:

  • field="background"background=True queues the response and exposes no create-time usage. Use the raw OpenAI client for background responses.
  • field="stream" — a streaming responses.parse. Use responses.create(stream=True) or the stream() helper.
  • field="extra_body"extra_body overrides model, input, instructions, max_output_tokens, or stream. Pass those as top-level arguments so preflight and dispatch see the same values.

See The Responses API.

TypeError: ... cannot be pickled

SDK v0.6.0+. Something tried to pickle the wrapper — multiprocessing with the spawn start method, a task queue serializing its arguments, or a framework snapshotting configuration. The client holds live reporter and budget state and refuses. Construct the client in the target process instead; copy.copy and copy.deepcopy return the same wrapper rather than a clone. See Copying and pickling the client.

CostPolicy selected but this budget check carried no price hints

SDK v0.6.0+. You configured selection_policy=CostPolicy() and a budget check answered with price_hints: null, so the policy used health-based order for that call. Logged once per process by solwyn._base. The usual cause is benign: lease-backed calls inside a solwyn.run(...) scope carry no price hints yet, so CostPolicy keeps configured order there. If you see it outside runs, the control plane was unreachable or answered a legacy shape. An explicit empty map ({}) — the answer for non-text modalities and for a primary provider Solwyn cannot price — is silent by design. See Cost-aware routing.

AttributeError: 'OpenAI' object has no attribute 'update_price_hints'

Solwyn.update_price_hints and the client-wide hint store were removed in 0.6.0. Price hints now arrive on every budget check and apply only to the call they were priced for; there is nothing to update. Remove the call. (The error names the provider class because unknown attributes forward to the wrapped client.)

SolwynTagsClampedWarning in my test output

The merged spend tags for a call — client defaults, run scope, and solwyn_tags together — exceeded 10 keys. The SDK kept the 10 highest-priority keys, dropped the rest from that call's attribution, and proceeded. Trim the lowest-priority layer (usually client-wide defaults). Each mapping on its own is still limited to 10 keys and raises ValueError if it exceeds them. See Bounds, validation, and clamping.

Still stuck?

  • Check the SDK version with from solwyn import __version__; print(__version__) and include it in any bug report.
  • Open an issue at github.com/solwyn-ai/solwyn-python-sdk with the version, the symptom, and any relevant log lines.

On this page