SOLWYN

Changelog

Solwyn Python SDK release history

All notable changes to the Solwyn Python SDK are documented here. This project follows Semantic Versioning.

0.9.0 — 2026-10-25

The SDK now meters Anthropic beta.messages.create and OpenAI decisions.create, and counts Anthropic server-side compaction in settled token usage. A new optional OpenTelemetry layer correlates metadata events with the caller's active trace and can emit content-free solwyn.call and solwyn.run spans. Runs stepped by fresh-context generators such as Starlette StreamingResponse keep their attribution, wrapped Anthropic Bedrock clients keep the caller's transport and AWS profile, and strict mode no longer refuses client.timeout reads. The SDK is tested against the new anthropic 2.x and together 3.x majors, and the surface ledger admits openai 3.33 through 3.37, anthropic 1.15 through 2.3, together 2.41 and 3.0, and google-genai 2.31. No existing export or signature changed; three SolwynConfig fields, the solwyn[otel] extra, and one contract-pack function were added. The wire-contract change is API-first: Solwyn Cloud accepts the compaction_* token_details keys and the optional trace_id / span_id / trace_sampled metadata fields before this SDK release.

Added

  • Optional OpenTelemetry trace correlation — metadata events carry the caller's active trace ID, span ID, and sampled flag, including streaming, failover, and denied calls. These are random identifiers, never content, and are omitted when no valid span is active. On by default whenever opentelemetry-api is importable (solwyn[otel] installs it as a convenience); disable with trace_correlation=False or SOLWYN_TRACE_CORRELATION=false. See Trace correlation.
  • Opt-in solwyn.call and solwyn.run spans — otel_spans=True or SOLWYN_OTEL_SPANS=true emits a solwyn.call span around budget preflight, provider dispatch, failover, and stream lifetime, and solwyn.run spans for run scopes and lifecycle handles. Attributes come from a closed solwyn.* allowlist of call, routing, budget, and run-ID metadata, never cost, token usage, content, tags, or exception messages. Budget checks and native provider spans nest beneath the call span, which is never current while your code iterates a stream. Run-span opt-in is process-wide and sticky after the first opted-in wrapper or truthy SOLWYN_OTEL_SPANS at run entry. See Call spans and Run spans.
  • Opt-in Anthropic conversation IDs — anthropic_conversation_id=True or SOLWYN_ANTHROPIC_CONVERSATION_ID=true extends an Anthropic client's existing open_telemetry mapping with the active run ID, so native Anthropic spans carry gen_ai.conversation.id. Customer settings and conversation IDs are preserved, and the ID is never sent to Anthropic. Requires anthropic>=1.14; off by default. See Anthropic conversation IDs.
  • assert_trace_ingest_contract in solwyn.testing.contract posts sampled and unsampled trace-correlated events. See The contract pack.

Changed

  • Anthropic beta Messages are metered — client.beta.messages.create(...) is budget-checked and settled on sync and async clients, streaming and non-streaming, including the Bedrock, Vertex, and Foundry platform clients. Beta request fields (betas, context_management, compaction) pass through unchanged and are never recorded. A beta call fails over only to other Anthropic models in your fallback= chain; cross-provider entries are skipped. Migration: remove a beta.messages.create acknowledgment token — a stale token raises ConfigurationError. beta.messages.stream, .parse, .tool_runner, .count_tokens, batches, and the raw/streaming response mirrors stay unmetered. See Beta Messages.
  • Anthropic server-side compaction is counted — compaction usage, which Anthropic reports only in usage.iterations, is added to the settled input, output, and cache counts for streaming and non-streaming calls. token_details carries the compaction share as compaction_input_tokens, compaction_cached_input_tokens, compaction_cache_creation_5m_tokens, compaction_cache_creation_1h_tokens, and compaction_output_tokens, each omitted when zero, so calls without compaction send the same payload as before. Anthropic streams also settle the cumulative input and cache counts reported on the final message_delta. See Server-side compaction.
  • OpenAI Decisions is metered — native decisions.create(...) (openai>=3.26) is budget-checked and settled with text pricing on sync and async clients, using the requested model even when the response echoes a dated snapshot. Migration: remove a decisions.create acknowledgment token on native OpenAI clients. Azure OpenAI, compatible providers, the raw/streaming response mirrors, and the read-only admin.organization.usage.decisions report stay unmetered. See Decisions.
  • Background tasks no longer inherit the caller's context — async reporter and budget-lease tasks start in an empty context, so application instrumentation no longer attributes their work to the first caller's trace. See Async usage.
  • Newer provider SDKs are classified — openai 3.33–3.37 (audio.voices, audio.voice_consents, realtime.translations.connect and client_secrets, safety.cases, and more beta.agents and admin leaves), anthropic 1.15–2.3 (the organization and user_profiles administration trees, newer beta.organization.* operations, beta.messages.thread, and the open_telemetry option as metadata), together 2.41 and 3.0 (clusters, post_training, projects, beta.jig rollback, and the promoted top-level endpoints / models / organization with organization.usage.costs), and google-genai 2.25's voices / aio.voices. They are unmetered, follow on_unmetered, and need exact acknowledgment tokens in strict mode. Anthropic 2.x needs no wrapper change, and native Together chat stays metered on 3.0's new inference host. Together with the two leaves promoted to metered, these admissions move pinned CoverageFingerprint values. See Coverage controls and Surface coverage.

Fixed

  • Wrapped AnthropicBedrock and AnthropicBedrockMantle clients keep the caller's http_client on every provider call (sync and async), and AnthropicBedrock also keeps its aws_profile. A client configured with a named AWS profile could fail to resolve credentials, and a custom transport or proxy was bypassed. See Anthropic.
  • Runs in sync generators stepped in fresh contexts keep their attribution — with Starlette/FastAPI StreamingResponse, every call is attributed to the correct run and parent, including runs and detached activations wrapped in context managers or ExitStack. Scope exit releases the run's lease without raising when an outer run is active, run stops follow the generator's run, activations can exit across yields, and start_run() handles can finish from their owning generator in a later step. Recovery follows up to 256 stack frames; threads started inside a recovered step need an explicit detached activation, and scopes inside async generators remain unsupported. See Sync generators stepped in fresh contexts.
  • Strict mode no longer refuses client.timeout reads — on wrapped OpenAI, Anthropic, Azure OpenAI, Together, and OpenAI-compatible clients, numeric, None, and timeout-object values are preserved without raising UntrackedSpendSurfaceError, including raw clients copied with with_options(timeout=...) before wrapping. See Structural pass-through.

0.8.0 — 2026-09-21

The SDK no longer estimates cost during a control-plane outage, and every outcome it cannot settle now keeps its token bound instead of leaving a reservation for the sweep to refund. The outage estimate used a flat per-token rate that meant nothing once calls became multimodal and broke the rule that Solwyn Cloud owns pricing: the legacy per-call path now keeps a token-only tally of the calls it admits while Solwyn is unreachable and reports it on the next successful budget check. The reporter and the lease-release path gain fixed bounds, run-control lookup is constant time in a run's active streams, and the surface ledger catches up with openai 3.16.2, together 2.35, and google-genai 2.24. One public signature changed (BudgetExceededError) and one name was removed (solwyn.budget.DEFAULT_COST_PER_TOKEN). The wire-contract change is API-first: Solwyn Cloud has accepted uncounted_calls / uncounted_tokens on /budgets/check since 2026-09-17.

Changed

  • Behaviour change: fail_open=False fails closed on the per-call check path while Solwyn is unreachable — it used to admit calls against the last known dollar limit using a local per-token cost estimate, a ledger that started at zero on every outage and never included the spend the server had already counted, so it could not enforce the real limit. Now every call that no live budget lease covers raises BudgetExceededError until Solwyn is reachable again, including the first call of a run that holds no lease yet. The error carries budget_limit and current_usage of 0.0, budget_period="unknown", and your configured budget_mode (an alert_only client still raises); its receipt reports deny_source="local_enforcement" and deny_reason="control_plane_unreachable", and the SDK logs budget.fail_closed_unreachable at most every 30 seconds. Migration: to keep run-scoped calls metered through an outage, run them inside solwyn.run(...) with lease_enabled=True (the default), or set fail_open=True to admit and tally. A prior hard deny, a retained run stop, and a cached allow behave exactly as before. See When Solwyn Cloud is unreachable.
  • fail_open=True tallies the calls it admits during an outage — one call plus its input-token estimate per admission, replaced by the provider-reported total when the call settles (a media call with no token usage, or a stream abandoned before its usage chunk, keeps its estimate). The tally rides the next /budgets/check that reaches Solwyn as uncounted_calls / uncounted_tokens, both omitted when zero so an ordinary check is byte-identical to 0.7.0; Solwyn Cloud records it as an advisory gap, never as priced spend. It is kept for the next check on a 408, 409 or 429, any 5xx, a transport error or timeout, a breaker hold, or a cancellation, and cleared by any 2xx or any other 4xx (logged as budget.uncounted_report_dropped, at most every 30 seconds). It only rides a budget check: all-lease or allow-cache traffic never sends one, and a tally still unsent at process exit is lost — there is no exit flush. Lease-path tallies are unchanged and still ride renewals. See When Solwyn Cloud is unreachable.
  • Breaking for code that constructs BudgetExceededError directly: estimated_input_tokens: int is a required keyword and estimated_cost is now float | None, defaulting to None — it is set only from a server-supplied figure, which Solwyn Cloud does not supply today, so it is None on every SDK-raised error. Code that formats e.estimated_cost as a number must switch to e.estimated_input_tokens. See BudgetExceededError.
  • Unknown provider usage retains the authorized token bound — cancelling an async chat or Responses call after the provider SDK was invoked (including while it still waits for a connection-pool slot), interrupting a sync call mid-dispatch with KeyboardInterrupt, SystemExit, or another non-Exception exit, a failure while wrapping an already-open provider stream (including at a Responses stream() helper's entry), a stream whose streamed usage cannot be finalized, and a read timeout, 5xx, or protocol drop with ambiguous failover disabled all retire the reservation at its bound behind a possibly_succeeded error receipt, record no provider-breaker verdict, and re-raise the original exception. Under failover_idempotency="always" an ambiguous hop pins the reservation at its bound for the rest of the call. Proven pre-send failures, request-shaped 4xx refusals, and an interrupt that provably landed before the request (or during a Retry-After wait) still return unused authority. The conservative tally can exhaust a lease sooner under aggressive caller timeouts; it never invents output usage or cost, and each terminal's charge leaves on the run's next renewal or surrender. See When a call does not finish cleanly.
  • Lease releases are bounded and share one HTTP pool — each budget enforcer runs at most 4 release workers with at most 64 releases parked unsent, identical lease/holder/generation/spend payloads coalesce, a full queue abandons the new unsent courtesy release (release_counts["queue_full"]), parked work still unsent after six seconds expires (expired, shortened to close()'s 3-second drain), and worker-start failures count dispatch_failed. Releases reuse a dedicated pool separate from provider clients and the reporter; local pool/TLS failures count setup_failed and other local errors local_error without touching the shared control-plane breaker, and cancelled async workers count cancelled. On AsyncSolwyn the pool belongs to the loop that built it: close() must run there while it is alive, and a later loop rebuilds it. A burst of 128 finished runs used to start a thread and a TLS client per release. No configuration or wire change. See When a lease is handed back.
  • reporter_batch_size must be at least one — the constructor, SOLWYN_REPORTER_BATCH_SIZE, and direct reporter construction reject zero and negative values instead of accepting a batch that could never make progress. See Reporter settings.
  • Run-control lookup is constant time in the number of active streams — admission, the post-check, stream-handle acquisition, stop publication, clear, and release no longer scan a run's live handles; termination authority is one shared epoch per clear generation, so a stop is a single write every current watcher reads and the per-chunk abort check is a lock-free read. A stream that latched a stop keeps aborting and one that never latched is never latched by a later stop, by construction. No public API, configuration, or wire change. See Run control.
  • 816 new provider-SDK rules are classified — audited against openai 3.16.2 (openai 3.14's beta.agents.* tree and live.*, openai 3.16's webhooks.* management with the webhooks.event_types namespace), together 2.35 (beta.endpoints.rollouts.*), and google-genai 2.24 (environments.files.upload, and the inert credentials properties as metadata). Namespaces are guarded, leaves are unmetered spend acknowledgeable by their exact dotted path, raw- and streaming-response mirrors are raw_response-scoped escapes; no existing rule moved and nothing was promoted to metered. Before this release these paths resolved to unknown and followed on_unmetered, as they still do. A CoverageFingerprint pinned on openai 3.8 moves in guarded_namespaces, untracked, and scoped_escapes once you are on openai 3.16.2 — re-capture it then. See Surface coverage.

Fixed

  • A provider exception with an unusual class name can no longer cost a metadata batch or hide the provider's error. failover_error_class is normalized before the event is built — leading characters up to the first ASCII letter stripped, every other character outside [A-Za-z0-9_.] replaced by _, cut to 64 characters, omitted when no letter remains — so grpc's _InactiveRpcError is reported as InactiveRpcError instead of making Solwyn reject the whole ingest batch with a 422, and a class name over 64 characters no longer raises a ValidationError in place of the provider's exception. The error receipt is now best-effort: if one cannot be built or enqueued the SDK logs call.error_receipt_failed, still ends the reservation, and re-raises the provider's original exception. See Privacy.
  • A cancelled recovery attempt no longer strands the provider's half-open probe. Cancelling a recovery attempt, abandoning a Responses stream() manager before entry, or a request-shaped refusal at manager entry used to leave the breaker's single probe held, so no later attempt could prove the provider healthy. Each admitted attempt now owns its probe until a health verdict or an explicit hand-off to a manager or live stream; stale cleanup cannot clear a successor's probe, a cancelled Responses helper entry no longer records a false breaker failure, and malformed streamed usage retires ownership without a false verdict. See Circuit breaker.
  • Cancelling a lease grant behind a predecessor release no longer forces later same-run calls onto legacy budget checks. Grant-slot cleanup now runs even when the fence wait is cancelled or probe cleanup raises, so the run's next call re-grants instead of paying the per-call /budgets/check for the rest of the process. If the bounded wait for an old release expires while that request is still active, the call uses the per-call check once and the old request keeps its slot until it ends.
  • A steady stream of successful confirms can no longer starve metadata ingestion. Each reporter round now handles at most min(reporter_batch_size, reporter_max_queue_size) standalone confirms and the same number of settlement confirms (50 each by default), then a bounded snapshot of ready metadata batches; productive rounds continue immediately, held and retrying heads keep their FIFO parking, and a settlement's confirm still precedes its metadata transfer. reporter_max_in_flight remains an event-send guard, not a throughput setting. See Fairness between confirms and metadata.

Removed

  • solwyn.budget.DEFAULT_COST_PER_TOKEN and the enforcer's local dollar ledger. Nothing in the SDK multiplies tokens by a price. The top-level solwyn package exported neither, so from solwyn import ... statements are unaffected.

0.7.0 — 2026-09-05

A run's budget lease is handed back the moment the SDK stops using it — at scope exit, on a revoked renewal, and on an operator stop — instead of waiting for close() or the server deadline, so the float returns to the project while the process keeps running. Every event of a lease-funded call now names its lease, letting Solwyn Cloud count that call once even when its confirmation is lost. solwyn.testing.FakeControlPlane mirrors both, and the surface ledger catches up with openai 3.8, anthropic 1.4, and google-genai 2.22. No existing name was removed or renamed and no signature changed; the only additions are the two names below. Wire-contract changes are API-first: Solwyn Cloud accepts every field below before this SDK releases.

Added

  • MetadataEvent.lease_id — every event of a lease-funded call — the success event, a failed-over attempt's error event, an ambiguous abort, an aborted stream's partial settlement — carries the server-minted id of the lease whose grant admitted it, the same id its confirmation carries. It is omitted from the wire when the call was funded by a per-call reservation (outside a run, in a tagged run, with per-call solwyn_tags, on a media or embedding surface), admitted from the allow cache, admitted uncounted during an outage, or denied, so payloads for those calls are byte-identical to 0.6.0. A renewal or a fresh grant landing while a call is at the provider never changes the id that call reports. See Privacy.
  • FakeControlPlane.released_leases — the lease ids a surrender actually released, alongside the existing lease_surrenders record of what the SDK sent, so a test can tell a release that landed from one the fence refused. See Inspecting what the SDK sent.

Changed

  • Leases are handed back as soon as the SDK stops using them — exiting a with solwyn.run(...) block (normally or on an exception), RunHandle.finish(), a renewal answered ineligible or denied, a lease block that cannot be read, and a server run-stop directive each surrender the run's lease immediately, off the calling thread (a solwyn-lease-release daemon thread, or a task on AsyncSolwyn); a release that cannot be dispatched — no running event loop, thread exhaustion, interpreter shutdown — is parked and rides close(). A handle.activate() exit on a create_run(...) handle deliberately keeps the lease, because a detached identity is re-entered; a renewal 404 / 409 and a local expiry have nothing to release. Work that outlives its scope with the run id still bound pays one blocking grant on its next call, fenced behind the release of the lease it replaces (at most 6 seconds) so the server sees the replacement after the release. An inner run releases only its own lease. Previously a lease the SDK had stopped using kept its float reserved until close(), the exit hook, or the server deadline. See When a lease is handed back.
  • Surrender is bounded at 3 seconds and retried once on timeout — up from 1 second with no retry. A refusal or transport failure is still not retried, an open control-plane breaker skips the release, and close() bounds the whole lease drain — releases still owed plus leases still held — at the same 3 seconds; the interpreter-exit budget is unchanged at 2 seconds. Because a scope exit now releases before the reporter has necessarily flushed, a surrender can reach Solwyn before the call's confirmation; the confirmation still settles, and the recorded spend is the same either way. See Shutdown.
  • Breaking for test suites that pin the double's wire shapes — FakeControlPlane answers /api/v1/metadata/ingest with a third lane, duplicates (index, reason of call_id or legacy_key, call_id), that partitions the batch with ingested and rejected; reject_ingest raises RuntimeError when a scenario names a dedup-skipped index, and its count= form draws from fresh events only. The surrender fence accepts the predecessor of a terminal successor and any generation of a stopped run's lease, so a test that pinned a 409 there now meets 200, and a stopped run does surrender — a test asserting plane.lease_surrenders == [] after stop_run no longer holds. assert_receipt_ingest_contract posts a third, lease-tagged event, so it fails against a server that predates lease_id. The SDK itself still reads only rejected from the ingest body. See Testing budget enforcement.
  • 795 new provider-SDK surfaces are classified — audited against openai 3.8 (safety.alerts.*), anthropic 1.4 (the beta.organization.* admin tree, beta.webhooks.parse_unverified, raw and streaming messages.batches.results), and google-genai 2.22 (environments.files.*). Namespaces are guarded silently and their leaves are unmetered spend under your on_unmetered posture, acknowledgeable by exact path; no existing rule moved, so nothing that was allowed or refused before changes. A CoverageFingerprint pinned under 0.6.0 differs in guarded_namespaces, untracked, and scoped_escapes once you are on openai 3.8, anthropic 1.4, or google-genai 2.22 — re-capture it then. See Surface coverage.

Fixed

  • A renewal or release thread that fails to start no longer breaks close() — the thread is registered only after a successful start, closing a race where a concurrent close() could join a thread that never ran; a release whose thread cannot start is re-parked and sent at close().

0.6.0 — 2026-08-24

The SDK grows from a budget gate into a control plane for agent runs. A run can now be stopped from the dashboard — or by the SDK's own content-free velocity detector — and the stop survives control-plane outages, aborts open streams at the next chunk, and leaves a denial receipt that is never lost. Failover becomes cost-aware, native OpenAI and Azure OpenAI Responses calls are metered, wrappers pass isinstance so agent frameworks admit them, every provider capability your code reaches is classified under a posture you choose, Anthropic 1.x is supported on httpx2, and solwyn.testing.FakeControlPlane exercises all of it with zero network. Wire-contract changes are API-first: Solwyn Cloud accepts every field below before this SDK releases.

Added

  • Agent-run stops raise RunStoppedError — an operator pressing Stop on the dashboard's Agents tab makes Solwyn Cloud deny the run's next budget check or lease renewal with a version 1 run_control terminate directive, and the SDK raises RunStoppedError on the next intercepted call. The exception inherits from SolwynError, not BudgetExceededError, so an agent loop's budget-denial handler cannot swallow and retry an explicit stop; it carries only the structural agent_run_id, reason, and source (server or local_velocity). A stop is retained per run through outages regardless of fail_open, is never softened by alert_only, refuses lease authority, and reaches a leased run through its renewal channel. It governs future dispatch — a non-streaming request already in flight completes — while streams stop at the next raw provider-chunk boundary: that chunk is pulled and discarded, prior usage settles exactly once as a partial success, and the provider stream is closed. Read and clear the retained state with current_run_terminated(), run_termination(run_id), and clear_run_termination(run_id); the registry is a 256-entry LRU that never guesses from fingerprints. A directive echoed for the wrong run is treated as contract drift on one call, credits the control-plane breaker, and logs a distinct ERROR. See Run control.
  • Content-free local velocity detection — seven velocity_* / SOLWYN_VELOCITY_* settings control the mode (warn by default, deny, or off) and the thresholds of three per-run rules: repeat_size (the same model with the same-sized prompt, repeatedly), monotonic_growth (prompts growing steadily), and the advisory-only rate_acceleration. Detection runs in-process on token counts and timing, never on content; under deny the first two rules stop the run locally, and a server allow never lifts a local stop. See Local velocity detection and Velocity settings.
  • Denial receipts that are never lost — every refused call reports a budget_denied event carrying deny_source, deny_reason, denied_by_period, estimated_output_bound, velocity_flags, and, on aggregates, receipt_aggregate_count and receipt_pricing_input_tokens. A receipt that would otherwise be dropped — overflow, ingest rejection, retry exhaustion, shutdown — folds by pricing-compatible identity (including the reason and period, so a run's run_stopped and monthly evidence never merge) and replays as one aggregate_replay event after delivery recovers. Folded receipts are not counted as drops; only a full aggregate table loses one, counted as receipt_fold_overflow. See Denial receipts and aggregate replay and Privacy.
  • A zero-network control-plane test double — solwyn.testing.FakeControlPlane plugs into the new caller-owned control_plane_transport seam, shared by normal operation, fork recovery, interpreter-exit delivery, and lease surrender, and speaks the production wire models. It scripts transport outages, endpoint refusals, magic-model verdicts (solwyn-test/deny, deny-alert, deny-tag, deny-stopped, runaway, kill, lease-ineligible), operator kills (stop_run, clear_stop, misroute_stops), ingest rejections, reservations, and the complete lease lifecycle, and records every request the SDK sent, including denial_receipts and aggregate_replays. It never prices anything. A reusable solwyn.testing.contract pack is dogfooded against both the double and the live API, and solwyn.testing.pytest_plugin provides explicitly opt-in, function-scoped fixtures — never automatic registration. See Testing budget enforcement.
  • Type-transparent wrappers — Solwyn(client) and AsyncSolwyn(client) report the wrapped provider class through __class__, so isinstance(wrapper, OpenAI) is True and frameworks admit the wrapper, while type(wrapper) stays truthful. Public attribute writes and deletes forward to the provider client; copy.copy and copy.deepcopy return the same wrapper; pickling raises with guidance to construct a fresh wrapper in the target process. Passthrough reads still cross the coverage guard. See Type transparency.
  • Detached run identities for framework callbacks — start_run(...) opens a scope and returns a RunHandle whose finish() closes it; create_run(...) snapshots a stable run id, parent, and inherited tags without changing the current context, and handle.activate() binds that identity around provider work in any task or thread, as often as needed. finish() fails loud while an activation is live. See Detached run handles.
  • Framework integrations — tested, offline-smoked recipes for the OpenAI Agents SDK (a wrapped AsyncSolwyn default client plus recipe-local model and provider adapters; no module ships), LangChain and LangGraph (solwyn[langchain] adds the content-free SolwynRunScopeHandler; the exact raw-response shim admits basic non-streaming invoke / ainvoke and two-node graphs), and CrewAI (solwyn[crewai] adds the structural SolwynEventListener; native LiteLLM stays attribution-only, and a narrow custom-BaseLLM recipe crosses a wrapped client). Every integration module is privacy-firewall-enforced, and scheduled smokes re-resolve current framework releases. See Framework integrations.
  • Native OpenAI and Azure OpenAI Responses calls are metered — sync and async responses.create(...), responses.parse(...), and the responses.stream(...) helper use one primary-only path: chat-only defaults are stripped, caller arguments win, and preflight and dispatch see the same mapping. Usage settles from the response or the stream's terminal event; an entered stream abandoned early, or a create/parse with missing or zeroed usage, settles a marked request-length estimate and keeps its lease reservation. background=True, a streaming parse, and extra_body overrides of model, input, instructions, max_output_tokens, or stream are refused with ConfigurationError before any I/O; the existing-response retrieval overload stays raw. Azure is the one compatible profile admitted. See The Responses API.
  • Explicit provider identity pins bypass auto-detection — provider="openai" (or a fallback tuple's fourth element) selects the named adapter without inspecting base_url, keeping native metering — including Responses — behind corporate gateways and local proxies. A pin never translates dialects or replaces the client: construction validates the client family and sync/async mode, rejecting mismatches with ConfigurationError(field="client") and unknown names with field="provider". See Explicit provider identity.
  • CostPolicy consumes server price hints — every budget check opts into price_hints_version: "1", and the request-scoped hints apply only to the call they were priced for. When the policy serves a cheaper healthy provider ahead of a healthy primary, the metadata event reports failover_reason: "cost_routed" — only when the served hint is strictly below the primary's (or the primary carries no hint), so a custom policy that fronts a pricier provider is still labelled circuit_open. See Cost-aware routing.
  • Strict pre-call coverage controls — clients classify and guard the whole reachable public provider-client graph. on_unmetered="raise" / SOLWYN_ON_UNMETERED=raise refuses untracked or unknown capabilities before provider I/O with the exported UntrackedSpendSurfaceError, which names the exact refused capability and its acknowledgment token; warn remains the default and allow the explicit unrestricted posture. acknowledge_untracked / SOLWYN_ACKNOWLEDGE_UNTRACKED accept exact terminal tokens only — namespaces, wildcards, tracked, blocked, and unsupported leaves are rejected at construction — with audio.speech.create:gpt-4o-mini-tts for the conditional TTS exception. coverage(client) returns a deterministic, content-free, sans-I/O CoverageReport you can pin with expect(...) so a provider-SDK upgrade fails a test instead of a budget. See Coverage controls.
  • Privacy-safe advisory reporting of untracked surfaces — on by default. Unacknowledged warn / allow observations are reported off the hot path, at most once per surface every fifteen minutes, as a structural payload: the dotted surface path, provider, client shape, mode, rule kind, scope, posture, approximate counts, timestamps, and random identifiers — never model names, arguments, prompts, or responses. It adds no budget check and no cost event, and report_untracked_surfaces=False / SOLWYN_REPORT_UNTRACKED_SURFACES=false turns the channel off without changing local behavior. See Advisory reports and Privacy.
  • Anthropic 1.x on httpx2 — Solwyn never imports httpx2; it recognizes the stack by identity. Per-hop timeouts are delivered as a native httpx2.Timeout when the client's own timeout class is identity-proven (so Anthropic's x-stainless-read-timeout header carries the numeric bound), and as a granular four-tuple otherwise. Anthropic 1.0's promoted-stable files and skills surfaces are classified as unmetered spend under the ordinary on_unmetered posture. See Anthropic.
  • New configuration — on_unmetered, acknowledge_untracked, report_untracked_surfaces, and the seven velocity_* fields, each with a SOLWYN_* environment variable, plus the constructor-only control_plane_transport. See SolwynConfig.

Changed

  • The budget allow cache is bounded and hint-aware — a 16-entry LRU keyed by provider, model, fallback chain, and modality, with the existing budget_check_cache_ttl window. A hit replays only its own entry's hints; run-scoped and tagged calls never read it. CostPolicy warns once only when a check carries no hints (null), not when the server explicitly clears them with {}.
  • Breaking only for consumers of private wrapper attributes — Solwyn-owned state now lives under _solwyn_*. The former _client, _budget, and _reporter no longer expose Solwyn internals; every non-prefixed private name belongs to the wrapped provider client. Public APIs are unchanged.
  • Breaking (pre-launch): the metered Responses leaves are no longer acknowledgeable — responses.create, parse, and stream are tracked and are rejected as acknowledgment tokens; the still-unmetered responses.retrieve remains the example.
  • Untracked capabilities warn once by default instead of passing silently — files, batches, moderations, models.list, and everything else Solwyn does not meter now log one warning per surface per process. Guarded namespaces keep descendants inside the same posture decision. Strict mode is a cooperative pre-call guard, not a sandbox: retained raw clients, private wrapper state, acknowledged scoped raw escapes, and native behavior on returned provider objects stay outside its boundary. See Surface coverage.
  • Over-limit provider paths keep the local posture — a public path too long or deep for the advisory wire is counted and warned about locally and never sent externally; malformed paths still fail closed.
  • run_stopped denials stay sticky only for their run — a control-plane outage can no longer replay a dashboard stop against unrelated run ids, and BudgetExceededError.budget_period now carries the actual Cloud or lease label (agent_run, tag, model, provider, or a period), falling back to unknown only when none was supplied.
  • Breaking for anyone passing inf: failover timeouts must be finite — failover_total_timeout and failover_hop_read_timeout reject booleans, NaN, and ±inf at construction; float("inf") was previously accepted and produced an unbounded window or hop read. The hop read bound must still be positive; a zero total timeout remains accepted. See Failover timeouts.

Fixed

  • A Responses stream-helper entry failure is classified instead of always blaming the provider. The manager sends its request at __enter__, so that failure now runs the same dispositions the candidate walk uses: a request-shaped 4xx records no breaker failure, while failover and post-send-ambiguous errors keep the breaker verdict. Previously a repeated 400 on responses.stream(...) could open the breaker and block all traffic to a healthy provider.
  • Compatible-provider Responses streams without readable usage hold their lease floor — a settlement that falls back to a length estimate is treated as unmeasured, so a run lease trues up at its reserved bound instead of re-lending output allowance the response already spent.
  • Closing a Responses stream helper before entering it releases its reservation — no provider request was dispatched, so no phantom confirm, breaker success, or latency sample is recorded.
  • extra_body={"stream": ...} is refused on metered Responses calls — the OpenAI SDK merges extra_body after named arguments, which would desync Solwyn's streaming mode from the dispatched request.
  • Anthropic 1.x transport failures are classified instead of failing fast — an httpx2 transport error is dispositioned exactly like its httpx twin and at the same position, ahead of any attached status: connect and pool failures fail over; read, write, and protocol failures stay post-send ambiguous.
  • The solwyn.testing contract probe opts into price hints and validates the served map — every key must be a known ProviderName (an unknown key makes the whole check response unparseable, so the SDK would fail open with no reservation) and every value a finite number; {} and null remain valid.

Removed

  • Solwyn.update_price_hints / AsyncSolwyn.update_price_hints and the client-wide hint store — price hints are request-scoped and supplied by the server's budget response.

Known limitation

  • Lease-backed solwyn.run() calls carry no price hints, so CostPolicy keeps configured provider order for those calls — and fires its once-per-process no-hints warning — until lease grants carry hints.

0.5.0 — 2026-08-07

Spend attribution becomes first-class: clients carry default tags, nested run scopes inherit and refine them key by key, one immutable per-call snapshot feeds both budget admission and spend events, and nested runs report their immediate parent run. Wire-contract changes are API-first: Solwyn Cloud accepts every field below before this SDK releases.

Added

  • Client default spend tags — Solwyn(..., tags={...}) / AsyncSolwyn(..., tags={...}) or SOLWYN_TAGS apply to every intercepted call from the client. The environment value is comma-separated key=value entries, split at the first =; values containing commas use the constructor mapping. Run-scope tags override client defaults key by key, and per-call solwyn_tags= has the highest precedence. See Tags.
  • Budget admission observes the call's tags, and tag-cap denials stay selector-scoped — a tagged call's /budgets/check carries the same immutable tag snapshot its spend event reports, which is what a tag-scoped budget decides on. A denial with denied_by_period == "tag" denies only traffic matching that selector and never creates project-wide or run-wide sticky authority. Tagged calls always take a fresh budget check, bypassing the allow cache and the run lease path. See Tags and budget admission.
  • Nested runs report their immediate parent — spend events from a nested solwyn.run(...) scope carry parent_agent_run_id; root runs omit the field. Parent context restores after normal exits and exceptions, including across concurrent asyncio tasks. See Nesting.
  • solwyn.current_run_context() returns the active RunContext(id, name, tags) with a defensive copy of the scope's tags. RunContext and the attribution bounds TAGS_MAX_KEYS, TAG_KEY_MAX_LENGTH, and TAG_VALUE_MAX_LENGTH are package exports.

Changed

  • Nested solwyn.run(...) scopes inherit tags additively by default — a child keeps all non-conflicting outer tags and overwrites only keys it supplies. inherit_tags=False starts a fresh tag scope; exiting either form restores the exact outer context.
  • A combined tag map over 10 keys clamps without aborting the live call — per-call keys are kept first, then scope keys, then client defaults, preserving insertion order within each layer; each overflowing capture emits one SolwynTagsClampedWarning, exported from the package root, and drops the lower-priority excess from that event while the provider request continues. A single mapping over 10 keys still raises. See Bounds, validation, and clamping.
  • Tag keys and values containing NUL are rejected eagerly, mirroring the control plane's storage and selector constraints, so an invalid tag cannot turn a budget check into an outage failure.

0.4.0 — 2026-07-28

The control plane comes off the hot path. A run now takes one token lease instead of asking permission before every call, settlement stops holding your provider response hostage, and spend telemetry is delivered at least once — across outages, forks, and interpreter exit. Failover timeouts split into a window and a per-hop read bound, so a slow generation is no longer cut into ambiguous spend.

Added

  • Run-scoped budget leases — a token-billed call inside a solwyn.run(...) scope used to pay a blocking budget round-trip to Solwyn before every provider request. The run now takes one server-granted lease, draws it down in memory, renews it in the background ahead of need, and hands it back when the run is done. The shape is DHCP's, for the same reason: authority is delegated for a bounded window so the client keeps serving without asking permission per call. A lease is denominated in tokens, never dollars — the server folds price into the grant, and the SDK still performs no pricing math. Each call reserves its input estimate plus the largest effective output cap across every configured hop (honoring per-call over per-entry over global precedence, and each provider's own cap spelling), then trues that reservation up to actual usage at settlement. Renewal is driven by demand rather than a timer — at 75% depletion or the refresh deadline — so an idle run costs nothing. Non-run calls, non-text modalities, media calls, and models outside the lease's declared set take the per-call check path automatically. Disable entirely with lease_enabled=False. See Run-scoped leases and Privacy.
  • A defined outage ladder, so no deny ever traces to unreachability — with the grant spent and Solwyn reachable, a call falls back to a per-call check (an empty wallet is not a refusal). With Solwyn unreachable, it draws on the lease holder's apportioned slice of real remaining headroom, admitted with a warning and still metered. Only when that is genuinely exhausted does your budget_mode decide. Past the lease deadline with Solwyn still down, your fail_open posture picks the floor: fail_open=True admits calls uncounted and tallies every one for the next successful renewal to report, while fail_open=False meters against the freshest known remainder and denies at your configured mode. Uncounted episodes announce themselves — lease.uncounted_entry on entry, then lease.uncounted_continuing at most every 30 seconds. See When Solwyn Cloud is unreachable.
  • failover_hop_read_timeout (default 600.0) — each hop's read/write bound, now decoupled from the failover window. 600.0 matches the openai and anthropic SDKs' own read/write default, so a wrapped call's read bound never fires earlier than the unwrapped SDK's would. Constructor-only (deliberately no environment variable) and governed by the same plan entitlement as the other failover knobs, which is now a closed set of eight fields rather than seven. See Failover timeouts.
  • A control-plane circuit breaker — a Solwyn outage is now discovered once per client rather than re-paid as a timeout on every call. One breaker guards both the budget check and the settlement path, applying your configured posture instantly instead of waiting. It is a separate health domain from your provider breakers, never appears in breaker state reports, and treats a read-only-key refusal as a success (Solwyn responded). Tuned by control_plane_failure_threshold and control_plane_recovery_timeout. Breaker log lines now name their domain: Circuit breaker [control-plane] opened due to failures.
  • A build-time warning for unbounded Bedrock reads — Solwyn cannot bound a Bedrock hop per call, so a botocore Config(read_timeout=None) is the one shape neither Solwyn nor botocore will ever bound. Building such a client now warns. Detection is defensive: an unreadable or absent config reads as bounded. See Bedrock.
  • New configuration — lease_enabled, lease_output_bound_default, budget_check_timeout, control_plane_failure_threshold, control_plane_recovery_timeout, breaker_report_heartbeat, reporter_max_send_attempts, reporter_retry_backoff_base, reporter_retry_backoff_cap, and reporter_shutdown_deadline, each with a SOLWYN_* environment variable, plus the constructor-only failover_hop_read_timeout. See SolwynConfig.

Changed

  • Settlement moved off the caller's thread — non-streaming chat completions and the entire media lifecycle (embeddings, images, audio, video) used to settle with a blocking confirm request on your thread after the provider had already answered, withholding the response until Solwyn's round-trip returned. Settlement is now built without I/O and enqueued with its metadata event as one ordered item — the path streaming completions already used. You get the provider response without waiting on Solwyn. See Spend delivery.
  • Spend telemetry is delivered at least once — confirms, settlements, and metadata batches were previously dropped on their first failed send. They are now retried with bounded exponential backoff. A transport error or an HTTP 408/429/5xx is transient and retried; every other status is terminal, so a poison item can never wedge the queue head. The server deduplicates on an idempotency ledger, so the SDK retries freely and never deduplicates client-side. Queues drain strictly FIFO, so later spend can never be confirmed ahead of earlier acknowledged spend. During a Solwyn outage a settlement is held for a later cycle rather than dropped, and if a settlement is ultimately rejected its paired metadata event is still delivered — ingest is the durable record of spend.
  • Undeliverable spend is counted and loudly logged, never silently dropped — queue overflow, retry exhaustion, terminal statuses, per-event ingest rejections, shutdown-deadline expiry, and enqueue-after-close all increment a counter surfaced by reporter.spend_events_dropped. The first drop logs immediately; after that at most one aggregated line per 60 seconds, so a sustained outage reports its loss without flooding logs. See Undeliverable spend.
  • Queued spend survives interpreter exit, and reporters survive fork() — a process exiting without close() used to discard everything queued; an exit hook now flushes each live reporter and surrenders held leases on a wall-clock deadline, so an unreachable Solwyn can never hold up process exit. Separately, threads, locks, and HTTP clients do not survive fork(), so a forked child inherited a dead flush thread and never delivered its settlements — the SDK now rebuilds them on the child's next enqueue, abandoning (never closing) the parent's sockets and deliberately keeping fork-duplicated items, since the server deduplicates.
  • close() is bounded by one shutdown deadline — reporter_shutdown_deadline (default 5.0) is now shared across the worker join, the final flush, and the breaker-report cycle, so shutdown against a black-holed control plane no longer pays a serial per-request timeout chain. It is a true wall-clock bound: the final flush runs off the closing thread, so a slow-drip response cannot hold close() open. Work still queued at the deadline is counted and dropped rather than requeued into a queue nothing drains.
  • The async reporter auto-starts on first enqueue — AsyncSolwyn previously flushed only after an explicit start(), which async with performs for you. Constructed without async with, it queued events and settlements silently until close(), drifting server-side spend tracking. The flush loop now starts on the first enqueue when a running event loop is present; with no running loop the item stays queued and one warning is logged per reporter. Enqueueing never raises — it sits on the LLM call path. See Async usage.
  • The failover window no longer caps an in-flight hop's read — failover_total_timeout (default 30.0) is now purely the failover window: the budget pre-flight, each hop's connect/pool slice, Retry-After sleeps, and advancement between hops. Reads are bounded separately by failover_hop_read_timeout. The old behavior cut a slow-but-connected provider at 30 seconds and re-raised it as APITimeoutError — but a read timeout is post-send ambiguous, and under the default failover_idempotency="safe" it re-raises without failing over. That cut therefore bought no failover; it only converted legitimately slow generations (reasoning models, large max_tokens) into ambiguous spend. Because window expiry still gates advancement between hops, at most one hop per call can consume the full read bound.
  • Two providers cannot be bounded that way — google-genai supports only a single whole-request timeout, so a google hop receives the read bound as its whole-request timeout and a google pre-send hang can exhaust the failover window without failing over (Google Gemini). boto3 has no per-call timeout override at all, so a Bedrock hop is governed entirely by your botocore Config (Bedrock).
  • The budget pre-flight timeout dropped to 1.0s (budget_check_timeout, was an effective 5s). That request gates the caller's hot path, and the control-plane breaker now caps repeated discovery of an outage.
  • The openai extra no longer installs tiktoken — pre-call token estimation is now always the length-based heuristic, which is what a conservative budget pre-flight needs. Exact tokenization was never used for billing, and post-call counts remain exact because they are read from the provider's response. See OpenAI.
  • Breaker reports are sent when a provider's snapshot changes — state, failure count, or success count — with a periodic full refresh governed by breaker_report_heartbeat (default 60.0 seconds), rather than on every cycle. A failed send leaves that provider due for the next cycle, and close() forces a final snapshot.
  • call_id pins the canonical UUID text form on the wire, matching what the API's idempotency ledger has required since it landed. The wrapper has only ever emitted that form, so nothing it sends changes. See Privacy.

Fixed

  • A connect or pool timeout wrapped by the provider SDK now fails over. Both openai and anthropic wrap the entire httpx.TimeoutException family in APITimeoutError, so a provably pre-send ConnectTimeout reached classification wearing the same class name as a post-send read timeout — and was treated as ambiguous, taking the whole call down instead of failing over to a healthy provider. APITimeoutError is now classified by its chained cause, the way APIConnectionError already was.
  • Server-pushed failover tuning is snapshotted once per call. A directive landing mid-call could leave that call running on a torn mix of old and new tuning — for example the new total timeout paired with the old idempotency mode. Each call now captures one immutable snapshot and consumes only that. See Plan-scoped tuning entitlement.
  • A media call whose failover window expired during the budget pre-flight no longer starts provider I/O, and releases its reservation — matching the chat path.
  • Output-cap alias precedence now matches dispatch on every provider hop, so a per-call cap beats provider-entry and global defaults even when one layer spells it max_tokens and another max_completion_tokens. Lease reservations therefore cannot silently use a lower-priority, larger cap.
  • Breaker-report writes now recognize read-only keys. The breaker-report request was the only Cloud write not routed through the one-time read-only-key diagnostic, so a read-only key produced a warning per provider on every reporter cycle — the exact noise that diagnostic exists to collapse. See Troubleshooting.
  • Post-success bookkeeping is fail-soft. Usage, region, and service-tier extraction now degrade on an adapter raise — usage to a flagged estimate, region and tier to omitted — instead of destroying a paid provider response. A call whose usage is entirely unmeasurable settles its lease at the reserved bound rather than crediting the untouched-looking output allowance back, which would re-lend authority a paid response already consumed.
  • Budget checks distinguish unparseable success bodies from transport outages. A 2xx with an unreadable body now logs budget.check_response_unreadable at ERROR and records breaker success (server contract drift), distinct from a transport failure.

Eleven new configuration fields — ten with SOLWYN_* environment variables plus the constructor-only failover_hop_read_timeout — all with defaults that preserve existing behavior for non-run traffic. Leases are on by default for eligible run-scoped calls; set lease_enabled=False to keep the per-call check path everywhere.

0.3.0 — 2026-07-16

Attribution and enforcement get finer-grained: agent runs carry labels you author and can be capped individually, circuit-breaker health reaches the dashboard, and custom failover tuning becomes a plan entitlement — one new configuration field, no breaking changes.

Added

  • Per-run budget caps — every pre-flight budget check made inside a solwyn.run(...) scope now carries the run's stable agent_run_id, so a budget can cap an individual run rather than only the project. Caps are server-enforced and configured in Cloud — there is no cap argument on run(); the SDK stamps the run id on the check and enforces the answer, raising the same BudgetExceededError in hard_deny mode. Run-scoped checks bypass the allow cache, so every call inside a scope performs a real check, and an authoritative hard deny is remembered per run id — preserved rather than failing open if Cloud then goes unreachable. A preserved deny is never silent: each call it applies to logs a WARNING on the solwyn.budget logger naming the usage and limit (Cloud API unreachable; preserving prior hard deny: $99.50/$100.00 used), so a sustained outage under a hard deny stays visible for its whole duration rather than announcing itself once. The same holds for the older global project-period deny. See Agent Runs and Logging.
  • Customer tags on runs and calls — attach your own key/value labels for cost attribution and dashboard filtering, per scope via solwyn.run(name, tags={...}) or per call via the reserved solwyn_tags={...} keyword, which the SDK strips before the request reaches the provider. The merge is shallow and per-call keys win; nested scopes replace rather than merge. Bounds are 10 keys across the merged set, keys 1–64 characters, values 0–256 — and violations are rejected, never truncated, raising before the budget check and before provider dispatch. Tags are the single deliberate exception to the zero-content guarantee: free-form text you author, transmitted as provided, never derived from your prompts — so never put prompt text, PII, or secrets in one. See Agent Runs and Privacy.
  • Circuit-breaker state reporting — the SDK periodically snapshots each provider's breaker and reports it — provider, state, failure and success counts, snapshot time, and a per-instance SDK id — giving the dashboard fleet-wide provider-health visibility. Advisory and one-way: Cloud snapshots never drive admission, state is never shared between instances, and every failover decision stays in-process. Reports ride the background reporter off the request path, never blocking a call, and begin only once a successful budget check has established the project. Disable with breaker_reporting_enabled=False or SOLWYN_BREAKER_REPORTING_ENABLED=false — the only new configuration field in this release. See Provider Failover, Privacy, and SolwynConfig.
  • Plan-scoped failover tuning entitlement — each budget check opts into a versioned failover directive, and Solwyn Cloud may answer with one governing a closed set of seven fields: the four failover knobs and the three circuit-breaker parameters. An absent directive is a no-op that retains your tuning, true (re)applies it, and false applies SDK defaults in its place — reversible in both directions, never a one-way ratchet. Strictly advisory: provider order never changes, breakers keep their identity and accumulated health state, a failover_total_timeout change preserves the original call start rather than restarting the clock, and calls never fail for lack of a directive. Suppressed tuning logs one WARNING per client instance, and only when your tuning actually differs from the defaults. See Provider Failover.
  • OpenAI prompt-cache write extraction — usage.prompt_tokens_details.cache_write_tokens (Chat Completions) and usage.input_tokens_details.cache_write_tokens (Responses API) are now extracted and reported as cache_creation_5m_tokens. OpenAI's cache writes carry a 30-minute default/minimum TTL, not 5 minutes — the field name is a wire-contract artifact mapping OpenAI's writes onto the existing bucket, not a claim about its TTL; cache_creation_1h_tokens stays 0 for OpenAI. Independent of cache reads, no opt-in or configuration, and unusable values degrade to 0 rather than raising. See OpenAI.
  • Read-only key diagnostics — when Solwyn Cloud refuses the SDK's writes because the configured key is read-only, the SDK now recognizes that structured response and logs one ERROR per process naming the cause, instead of a stream of generic Cloud-API warnings. Detection is exact — other 401/403 responses still surface as the ordinary warnings. Reporting is dropped, and budget checks fail open on the default fail_open=True, so your provider calls keep working while enforcement and attribution do not. See Troubleshooting.
  • UnsupportedSurfaceError in the package root — from solwyn import UnsupportedSurfaceError now works, and the class joins every other exception in the package's __all__. It shipped in 0.2.0 as a deep import only; the addition is additive, so from solwyn.exceptions import UnsupportedSurfaceError keeps working. See Exceptions.

One new configuration option, breaker_reporting_enabled. No breaking changes: tags is omitted from the wire entirely when you set none, so payloads for untagged calls are unchanged.

0.2.0 — 2026-07-11

The SDK goes multimodal: embeddings, images, audio, and video are now intercepted, budget-checked, and priced with the same wrapper-not-a-proxy privacy boundary as chat — quantities and selectors leave your process, never content.

Added

  • Embeddings interception — embeddings.create (OpenAI dialect, native Together) and models.embed_content (Google) are budget-checked and recorded as cost events with modality embedding, priced from input tokens; Google exposes no usage on embeddings, so its counts are length-estimated and flagged is_estimated. See Surface coverage.
  • Image interception — images.generate and images.edit (OpenAI, compat, native Together) and models.generate_images (Google Imagen). Token-billed image models price from usage token buckets; per-image endpoints return no usage, so billing is request-derived (image count × per-image rate) with the count, resolution, and quality selectors measured locally. See OpenAI and Google Gemini.
  • Audio interception — audio.transcriptions.create and audio.speech.create (OpenAI, compat incl. Groq, native Together). Transcriptions price from usage token buckets or whole-second durations on a JSON response_format — a non-JSON format is recorded unpriced, never guessed; speech prices from the input character count measured inside the privacy firewall. audio.translations and usage-less TTS models (gpt-4o-mini-tts) warn once and pass through. See Surface coverage.
  • Video interception — videos.create (OpenAI Sora) and models.generate_videos (Google Veo), priced per second on resolution-gridded cards. Video jobs carry no usage, so billing settles at initiation as a flagged, conservative over-count — while the pre-flight check is exact, denying an over-budget generation before the provider is called. On providers with no video seam, videos.create fails loud instead of passing through untracked.
  • MediaUsage on the wire — non-token billable quantities (image_count, generation_count, video_seconds, audio_seconds, input_characters) plus the resolution / quality selectors, and a modality field on every cost event. Pre-flight budget checks carry estimated_media, the same quantities derived from the request. Unobservable quantities are omitted, routing the call to the unpriced lane — never a fabricated $0. See Privacy.
  • UnsupportedSurfaceError — raised when a wrapped client's adapter serves no seam for a requested media surface, instead of letting a spend surface run untracked. A deep import (from solwyn.exceptions import UnsupportedSurfaceError) until 0.3.0, which also exported it from the package root. See Exceptions.

No new configuration options. Pre-media wire payloads are byte-identical; the new fields are omitted from the wire for text calls.

0.1.12 — 2026-07-10

Added

  • First-class Together AI provider — wrap a native Together or AsyncTogether client directly; detection is duck-typed by module and class, and Together-hosted OpenAI clients (base_url="https://api.together.xyz/v1") are still admitted via the compat profile. Billable surfaces Solwyn does not yet meter on the native client — completions, rerank, code_interpreter, evals — warn once and pass through. The solwyn[together] extra installs together>=2.0 as a convenience; the SDK never imports it. See Together AI.

0.1.11 — 2026-07-09

Added

  • Z.AI compatibility profile — an OpenAI client pointed at api.z.ai (or calling glm-* models) is detected and attributed as provider="zai" for budgets, attribution, and failover. See OpenAI-compatible providers.

Fixed

  • Z.AI streaming usage injection — Z.AI documents stream_options.include_usage, so Solwyn now injects it on streamed calls and reads exact usage from the final chunk instead of falling back to estimation.
  • Type-checking — OpenAI usage values now narrow correctly under mypy.

0.1.10 — 2026-07-06

Added

  • CostPolicy no-op warning — CostPolicy is selectable but inert until the server sends relative price hints; when no candidate carries a hint it degrades to health-based ordering. That fallback now logs a one-time, per-process warning naming the condition — visible, and carrying no request content.

0.1.9 — 2026-07-06

Fixed

  • Privacy: no tracebacks on the budget-denied report path — two logger.warning calls on the budget-denied report-failure path logged full tracebacks (exc_info=True), breaching the firewall convention that only exception class names are ever logged. Both now log the class name only, and a firewall gate asserts the pattern cannot regress. See Privacy.

0.1.8 — 2026-07-02

Fixed

  • Hard deny survives Cloud outages — After an authoritative Cloud hard_deny, if a subsequent live budget check fails because Solwyn Cloud is unreachable, the SDK keeps denying instead of falling through to its outage path — even when fail_open=True. This is sticky enforcement state, not a cache: live Cloud checks still run on every call, and the first Cloud response that allows clears the sticky deny, so recovery is automatic with no client restart. Ordinary pre-deny outages continue to honor fail_open / local enforcement as before. Applies to both sync and async budget enforcement, at the provider-call boundary — a blocked call raises before reaching the underlying LLM SDK.
  • Cloud response mode is authoritative for denials — Denial behavior now keys off the mode in the Cloud budget response rather than the local budget_mode config, fixing local/Cloud mode drift (e.g. local alert_only vs. a Cloud hard_deny, or the reverse). The served decision now matches what Cloud actually decided.

No breaking changes, no wire-contract changes.

0.1.7 — 2026-06-12

Added

  • Amazon Bedrock support (Converse API) — Wrap a boto3 or aioboto3 bedrock-runtime client: Solwyn(boto3.client("bedrock-runtime")). Intercepts converse / converse_stream, which work uniformly across every chat model Bedrock hosts, with exact cache accounting (additive input formula, 5-minute/1-hour cache-write TTL split via usage.cacheDetails), service/latency tier capture, and per-region cost attribution. Bedrock participates in cross-provider failover in both directions. Zero new runtime dependencies — boto3 is never imported (detection is duck-typed); the solwyn[bedrock] extra installs boto3>=1.34 as a convenience. invoke_model / invoke_model_with_response_stream raise ConfigurationError instead of silently bypassing budget tracking. See Amazon Bedrock.
  • OpenAI-compatible providers — Point an openai.OpenAI client at xAI, DeepSeek, Mistral, Qwen, Groq, Together, Fireworks, Perplexity, Azure OpenAI, OpenRouter, Ollama, vLLM, LM Studio, or any compatible endpoint via base_url; Solwyn detects the real provider for attribution, budgets, and failover instead of mislabeling it openai. Streaming usage (stream_options.include_usage) is requested only where documented-safe per provider. See OpenAI-compatible providers.
  • provider= constructor argument — Explicit provider identity for the primary client when auto-detection cannot name the endpoint (e.g. provider="vllm" on a non-default port). Fallback specs accept a matching 4th element: (client, model, default_params, provider). An override relabels within the same API dialect only; unknown or dialect-mismatched values raise ConfigurationError at construction.
  • Flagged estimated usage — When a provider returns no usage data, the SDK reports length-based estimates explicitly marked token_details.is_estimated=true (serialized only when true) and logs a one-time WARNING. Budgets still enforce; degraded accounting is loud, never silently zero.
  • Same-dialect failover passthrough — Hops between OpenAI-dialect providers (e.g. Groq → OpenRouter) are native passthrough: tools, JSON mode, and streaming survive. On cross-provider hops, endpoint-scoped extra_headers / extra_query / extra_body are stripped and max_completion_tokens ⇄ max_tokens is rewritten per target. See Provider Failover.
  • Per-event ingest rejection logging — Solwyn Cloud now accepts well-formed metadata batches with a 202 and reports per-event rejections in the response body instead of failing whole batches. The SDK surfaces these as one aggregated WARNING per distinct (code, model) per batch on the solwyn.reporter logger (reporter.ingest_events_rejected), with the server's guidance message logged verbatim. Rejected events are terminal (never re-queued); accepted events in the same batch remain recorded. Malformed response bodies fail open to the previous count-only acknowledgment, escalating to ERROR after 10 consecutive occurrences. See Logging.
  • New wire fields — Fifteen new provider values (bedrock plus fourteen OpenAI-compatible identifiers), optional provider_region (omitted when unset), service_tier on budget confirms, token_details.is_estimated, and model identifiers up to 2048 characters (Bedrock ARNs).

0.1.6 — 2026-06-06

Added

  • True cross-provider failover — Configure a failover chain with fallback=[(client, model), (client, model, default_params)] spanning OpenAI, Anthropic, and Google. When a provider fails or its circuit is open, Solwyn translates the request and retries it on the next provider in-process. See Provider Failover.
  • Selection policies — selection_policy= accepts HealthBasedPolicy (default) or LatencyPolicy to control the order in which healthy candidates are attempted.
  • Request translation — Cross-provider hops translate the request shape through a canonical subset (structural only — never content). Untranslatable shapes fail loud before any network call.
  • New constructor arguments — model= (primary entry model), fallback=, default_params= (global fill-absent params), and selection_policy=.
  • Failover tuning — failover_total_timeout, failover_idempotency ("safe" / "never" / "always"), same_provider_retries, and circuit_breaker_recovery_timeout_jitter. Per-call override with the solwyn_idempotent keyword.
  • New exceptions — UntranslatableRequestError and UntranslatableModelError, both carrying structural labels only.
  • New metadata fields — is_provider_fallback, requested_provider, requested_model, failover_reason, failover_error_class, attempt_index, possibly_succeeded, and call_id on the reported event.

Changed (BREAKING)

  • fallback_model and primary_provider removed. Same-provider model fallback is now expressed as a same-provider entry in the fallback= chain (e.g. fallback=[(OpenAI(), "gpt-4o-mini")]); the primary provider is always auto-detected from the wrapped client. The SOLWYN_FALLBACK_MODEL and SOLWYN_PRIMARY_PROVIDER environment variables are no longer read. Migration: replace fallback_model="x" with fallback=[(SameClient(), "x")], and delete any primary_provider= argument.

0.1.5 — 2026-05-16

Added

  • Agent runs — solwyn.run(name) (sync and async context manager) attributes every LLM call inside the scope to a single agent run, so the dashboard groups cost and latency by run. Calls outside a scope are grouped into a synthetic per-day run. See Agent Runs.
  • solwyn.run_in_executor(...) — Submit threaded work to a ThreadPoolExecutor with the active run tag preserved (contextvars do not propagate into worker threads automatically).
  • solwyn.current_run() — Read the active (agent_run_id, agent_run_name) for correlating your own logs.
  • Wire fields — agent_run_id and agent_run_name added to the reported metadata event.

0.1.4 — 2026-05-13

Added

  • Full token-extraction parity — Complete normalized token details across providers.
  • OpenAI Responses API — Token details extracted from both the Chat Completions and Responses API response shapes.
  • Anthropic cache TTL split — Cache-creation tokens are captured separately as cache_creation_5m_tokens (5-minute TTL, 1.25× rate) and cache_creation_1h_tokens (1-hour TTL, 2× rate), with an aggregate fallback for older responses.
  • OpenAI service tier — The response service_tier is captured and reported on the metadata event when present.

0.1.3 — 2026-05-01

Changed (BREAKING)

  • Single-credential SDK contract. project_id removed from Solwyn() / AsyncSolwyn() constructors and the SOLWYN_PROJECT_ID environment variable is no longer read. The project is resolved server-side from the API key. Migration: delete the project_id= kwarg and unset SOLWYN_PROJECT_ID.
  • Project key format tightened. Project keys now match ^sk_proj_[a-f0-9]{64}$ (was ^sk_solwyn_[a-zA-Z0-9]{32,64}$). Existing keys are not migrated; create a new key from the dashboard.

0.1.2 — 2026-04-17

Added

  • Same-provider model fallback (fallback_model). Retry a failed call once with a different model on the same provider client. (Superseded in 0.1.6 by the fallback= chain.)

0.1.1 — 2026-04-15

Changed

  • Tag-derived versioning — Migrated to hatch-vcs so the package version is derived from the git tag.
  • Expanded test coverage for environment-variable construction and credential validation. No user-facing API changes.

0.1.0 — 2026-04-09

Initial release of the Solwyn Python SDK.

Added

  • Zero-config construction — Solwyn(OpenAI()) and AsyncSolwyn(AsyncOpenAI()) work with no kwargs when SOLWYN_API_KEY is set in the environment.
  • Solwyn and AsyncSolwyn client wrappers — Drop-in wrappers for OpenAI, Anthropic, and Google Gemini clients with budget enforcement, circuit breaking, and metadata reporting.
  • Provider auto-detection — Automatically detects the LLM provider from the client instance. No provider= argument needed.
  • Normalized token extraction — Extracts and normalizes token usage across all three providers into a consistent TokenDetails format.
  • Budget enforcement — Pre-call budget checks via Solwyn Cloud API with local fallback. Supports alert_only (default) and hard_deny modes.
  • Circuit breaker — Process-local circuit breaker with CLOSED/OPEN/HALF_OPEN states for provider health tracking.
  • Metadata reporting — Non-blocking batch reporting of usage events to Solwyn Cloud. Background thread (sync) or asyncio.create_task (async).
  • SolwynConfig — Pydantic-validated configuration with environment variable loading (SOLWYN_* prefix).
  • Exception hierarchy — BudgetExceededError, ProviderUnavailableError, ConfigurationError with structured attributes.
  • OpenAI support — Chat Completions and Responses API response shapes. Optional tiktoken extra for accurate token estimation.
  • Anthropic support — Cache normalization (additive input_tokens). Reasoning tokens documented as always 0.
  • Google Gemini support — Thinking token normalization (candidates + thoughts). usage_metadata handling.
  • Fail-open default — LLM calls proceed when Solwyn Cloud is unreachable. Local tracking until connectivity is restored.
  • Context manager support — with Solwyn(...) and async with AsyncSolwyn(...) for automatic resource cleanup.

On this page