Logging
Every WARNING and ERROR message the Solwyn SDK emits — what it means, when to act
The Solwyn SDK emits diagnostic messages via Python's standard logging module under the solwyn logger. Configure verbosity like any other logger:
import logging
# See SDK warnings and errors (recommended)
logging.getLogger("solwyn").setLevel(logging.WARNING)
# See state transitions like circuit breaker recovery
logging.getLogger("solwyn").setLevel(logging.INFO)
# Silence the SDK entirely
logging.getLogger("solwyn").setLevel(logging.CRITICAL)The SDK never logs prompt content, response content, or any customer text. See Privacy for the full guarantee.
Message reference
Every WARNING-, ERROR-, and INFO-level message the SDK emits is listed below with its meaning and recommended action. Messages with %s placeholders show the actual format string the SDK uses.
WARNING level
| Message | Meaning | Action |
|---|---|---|
Budget limit reached (alert_only mode): limit=$X, usage=$Y | A budget check returned "denied" but budget_mode="alert_only", so the call proceeded. | Either accept the overspend or switch to budget_mode="hard_deny". |
Cloud API budget check failed: <exception class> | The pre-flight budget check could not reach Solwyn Cloud. Behavior depends on fail_open. | Ignore single occurrences. Investigate persistent failures. |
Cloud API unreachable; preserving prior hard deny: $X/$Y used (SDK v0.3.0+) | A budget check failed while a prior authoritative hard deny was on record — the project-period deny, or the sticky deny for the call's run scope — so the SDK preserved the denial instead of failing open. X is the usage, Y the limit. Logged on the solwyn.budget logger, immediately after the generic Cloud API budget check failed warning above, on every affected call rather than once per process. | Restore connectivity so a live check can clear the deny, or raise the budget. See Budget Enforcement. |
budget.confirm_cost_failed: exc_type=<type> | Post-call cost confirmation failed. The metadata event was still queued. | None — single events. |
Failed to send metadata batch (N events): <exception class> | The reporter could not deliver a batch. The events in that batch are dropped, not re-queued — reporting is best-effort. | Investigate connectivity if persistent. |
reporter.ingest_events_rejected: code=<code> model=<model> count=N message=<message> | Solwyn Cloud accepted the batch but refused to record N events for this (code, model) pair — for example a model without a pricing entry. The rejected events are dropped, never retried. | See Per-event ingest rejections. |
reporter.ingest_response_unparseable: exc_type=<type> | An ingest response body could not be parsed for per-event dispositions. The batch itself was delivered; only the rejection detail was lost. The SDK fell back to the count-only acknowledgment. | None — single occurrences. |
reporter.confirm_send_failed: exc_type=<type> | Stream-completion confirm-cost request failed inside the reporter thread. | None — best-effort. |
reporter.confirm_enqueue_failed: exc_type=<type> | Could not enqueue a confirm-cost request. | Rare — investigate if recurring. |
reporter.settlement_enqueue_failed: exc_type=<type> | Could not enqueue a stream settlement (confirm + metadata event as one ordered item). | Rare — investigate if recurring. |
Circuit breaker opened due to failures | A provider's circuit breaker opened after consecutive failures. | Investigate the provider's status if calls keep failing. |
Custom failover tuning is unavailable for this plan; SDK defaults applied (SDK v0.3.0+) | Your plan lacks the failover-tuning entitlement, so your custom failover and circuit-breaker tuning was suppressed and the SDK defaults applied in its place. Logged on the solwyn._base logger, at most once per client instance, and only when your requested tuning actually differs from the defaults. | Upgrade to a plan with the entitlement, or drop the custom tuning. See Plan-scoped tuning entitlement. |
reporter.breaker_send_failed: provider=<name> exc_type=<type> (SDK v0.3.0+) | A breaker state report could not be delivered. The report is dropped, never re-queued — reporting never blocks calls. | Investigate connectivity if persistent. See Breaker state reporting. |
reporter.breaker_snapshot_failed: exc_type=<type> (SDK v0.3.0+) | Collecting the breaker snapshots failed before any provider was read; that reporting cycle is dropped. | None — single occurrences. Investigate if persistent. |
reporter.breaker_snapshot_invalid: provider=<name> exc_type=<type> (SDK v0.3.0+) | One provider's breaker snapshot was unusable; that provider's report is dropped, others still send. | None — single occurrences. Investigate if persistent. |
Failed to report budget_denied metadata event | Reporting a budget_denied metadata event failed. | None — the BudgetExceededError was still raised correctly. |
Failed to use tiktoken for <model>: <exception class> | tiktoken raised when estimating tokens. The SDK fell back to the heuristic estimator. | Pin a known-good tiktoken version if persistent. |
Failed to get exact Anthropic tokens: exc_type=<type> model=<model> | An optional count_tokens server call failed. The SDK fell back to a heuristic. | None — estimation only. |
Anthropic stream finalized without message_start; input token counts may be incomplete (also: without message_delta, for output) | An Anthropic stream ended without the event that carries that token category. | Possible undercount — investigate truncated streams. |
OpenAI service_tier exceeds N characters; truncating / Bedrock service tier exceeds N characters; truncating | The response echoed a service-tier value longer than 32 characters; the bounded value is still reported. | None — investigate unusual tier values if recurring. |
Bedrock stream settled at zero tokens: no usage in the terminal metadata event (stream abandoned, or the metadata event carried no usage) | A Bedrock stream produced events but settled without usage — possible undercount. | Fully consume result["stream"], or close it on early abandonment so the terminal metadata event is observed. See Streaming. |
Provider '<name>' returned no usage data; Solwyn is reporting length-based ESTIMATED token counts (marked is_estimated=true). Budgets still enforce, but per-call costs are approximate. | An OpenAI-compatible provider emitted no usage; the SDK fell back to flagged estimation. Logged once per adapter. | Per-call costs are approximate until the endpoint emits usage — for local servers, upgrade to a version that reports usage. See OpenAI-compatible providers. |
on_complete raised during __exit__; suppressing (also: on_complete / on_error raised during stream settlement; suppressing, and the async variants) | A stream callback raised while settling or exiting a with block. The original application exception (if any) is preserved. | Investigate the inner exception. |
ERROR level
| Message | Meaning | Action |
|---|---|---|
budget.confirm_cost_persistent_failure: exc_type=<type> consecutive_failures=N | 10 or more consecutive cost-confirm failures. Operators should see this. | Investigate immediately — usage may be undercounted in the dashboard. |
reporter.confirm_send_persistent_failure: exc_type=<type> consecutive_failures=N | 10 or more consecutive stream-completion confirm-cost failures inside the reporter. | Investigate immediately — streamed usage may be undercounted in the dashboard. |
reporter.ingest_response_unparseable_persistent: exc_type=<type> consecutive_failures=N | 10 or more consecutive ingest response bodies could not be parsed — likely contract drift between SDK and API. Per-event rejection detail (if any) is not being surfaced. The counter resets on the first parseable body. | Upgrade to the latest SDK; open an issue if it persists. |
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 (SDK v0.3.0+) | The configured key is read-only, so Solwyn Cloud refuses the SDK's writes. The diagnostic is raised from the budget-check and reporting (usage confirm, metadata) paths, where the SDK recognizes the structured refusal. Reporting is dropped, and budget checks fail open on the default fail_open=True — provider calls keep working, but enforcement and attribution do not (with fail_open=False, the usual local fallback applies). Logged once per process on the solwyn._read_only_key logger. | Swap in a full-scope project key. See API keys. |
INFO level
| Message | Meaning |
|---|---|
Circuit breaker closed, provider recovered | A circuit breaker transitioned from HALF_OPEN to CLOSED after successful probes. |
Circuit breaker half-open, testing recovery | A circuit breaker transitioned from OPEN to HALF_OPEN after the recovery timeout elapsed. |
Detected a local OpenAI-compatible server on its conventional port as provider '<name>'; pass provider=... to Solwyn to override. | The provider identity was guessed from a conventional local port (Ollama 11434, vLLM 8000, LM Studio 1234). Logged once per adapter, naming the provider only — never the URL, which may carry credentials. |
Per-event ingest rejections
Available from SDK v0.1.7. When the reporter delivers a metadata batch, Solwyn Cloud responds with a per-event disposition list: events it cannot price — a model with no pricing entry, an unrecognized service tier — are rejected individually while the rest of the batch is recorded. The SDK reads those dispositions and surfaces them as WARNINGs on the solwyn.reporter logger (a child of solwyn, so the configuration at the top of this page covers it).
There is nothing to configure — no constructor params, kwargs, or environment variables. The feature is observed entirely through logging:
import logging
from openai import OpenAI
from solwyn import Solwyn
# Rejection warnings arrive on the standard `solwyn` logger
# (the reporter logs under `solwyn.reporter`).
logging.getLogger("solwyn").setLevel(logging.WARNING)
client = Solwyn(OpenAI(), api_key="sk_proj_...")
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello!"}],
)
client.close()
# If the Solwyn API cannot price an event, the next flush logs one
# aggregated WARNING per distinct (code, model) — for example:
#
# 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.Each line carries four fields:
code— a server-owned rejection code. Today the codes areunknown_modelandunknown_service_tier; the set can grow without an SDK release, and an unfamiliar code is logged the same way.model— the model identifier as submitted (truncated server-side to 50 characters).count— how many events in the batch were rejected for this (code, model) pair.message— the server's human-readable guidance, logged verbatim and never parsed by the SDK.
Behavior notes:
- One line per distinct (code, model) per batch. A fleet stuck on a single unpriced model emits one WARNING per batch flush, not one per call.
- Rejected events are terminal. They are logged and dropped, never re-queued — resubmission would be rejected identically until a pricing entry lands server-side. Pricing fixes are an API-side deploy: once the entry exists, subsequent traffic prices automatically with no SDK upgrade or redeploy on your side. The rejected calls are not recovered retroactively.
- Accepted events in the same batch are unaffected. They are already durable server-side when the rejections are logged.
- Sync and async are identical.
SolwynandAsyncSolwynreporters share the same rejection-logging code path. - Fail-open. A malformed response body never interrupts reporting: the SDK falls back to the count-only acknowledgment and logs one
reporter.ingest_response_unparseableWARNING. Ten consecutive unparseable bodies escalate to thereporter.ingest_response_unparseable_persistentERROR; a single parseable body resets the counter. - Logs only. There is no callback, hook, return value, or SDK-side metric for rejections. This is the post-call telemetry path — it plays no part in failover or circuit-breaker decisions.
These lines carry only the rejection code, model identifier, count, and the server's guidance message — never prompts, responses, or any customer text; the batch itself is content-free metadata. As defense in depth, the SDK escapes raw control characters in the server-echoed fields before logging, so the lines are safe to ship to log aggregators.
Filtering at the logger level
If you only want to act on a subset of messages, attach a filter:
import logging
class OnlyCircuitBreaker(logging.Filter):
def filter(self, record):
return "Circuit breaker" in record.getMessage()
logger = logging.getLogger("solwyn")
logger.setLevel(logging.INFO)
logger.addFilter(OnlyCircuitBreaker())What is not logged
The SDK never logs:
- Prompt text or message content
- Response text or model output
- API keys (Solwyn or provider)
- Project IDs in error contexts where they could leak via aggregation
Exception messages from provider SDKs are not re-raised through the SDK's logger. They propagate to your code unchanged.