SOLWYN

Changelog

Solwyn Python SDK release history

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

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 doublesolwyn.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 wrappersSolwyn(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 callbacksstart_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-detectionprovider="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 configurationon_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 acknowledgeableresponses.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 silentlyfiles, 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 finitefailover_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 tagsSolwyn(..., 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 configurationlease_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 deadlinereporter_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 enqueueAsyncSolwyn 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 readfailover_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 extractionusage.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 rootfrom 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 interceptionembeddings.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 interceptionimages.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 interceptionaudio.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 interceptionvideos.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 warningCostPolicy 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_tokensmax_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 policiesselection_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 argumentsmodel= (primary entry model), fallback=, default_params= (global fill-absent params), and selection_policy=.
  • Failover tuningfailover_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 exceptionsUntranslatableRequestError and UntranslatableModelError, both carrying structural labels only.
  • New metadata fieldsis_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 runssolwyn.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 fieldsagent_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 constructionSolwyn(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 hierarchyBudgetExceededError, 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 supportwith Solwyn(...) and async with AsyncSolwyn(...) for automatic resource cleanup.

On this page