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 1run_controlterminate directive, and the SDK raisesRunStoppedErroron the next intercepted call. The exception inherits fromSolwynError, notBudgetExceededError, so an agent loop's budget-denial handler cannot swallow and retry an explicit stop; it carries only the structuralagent_run_id,reason, andsource(serverorlocal_velocity). A stop is retained per run through outages regardless offail_open, is never softened byalert_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 withcurrent_run_terminated(),run_termination(run_id), andclear_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 (warnby default,deny, oroff) 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-onlyrate_acceleration. Detection runs in-process on token counts and timing, never on content; underdenythe 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_deniedevent carryingdeny_source,deny_reason,denied_by_period,estimated_output_bound,velocity_flags, and, on aggregates,receipt_aggregate_countandreceipt_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'srun_stoppedandmonthlyevidence never merge) and replays as oneaggregate_replayevent after delivery recovers. Folded receipts are not counted as drops; only a full aggregate table loses one, counted asreceipt_fold_overflow. See Denial receipts and aggregate replay and Privacy. - A zero-network control-plane test double —
solwyn.testing.FakeControlPlaneplugs into the new caller-ownedcontrol_plane_transportseam, 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, includingdenial_receiptsandaggregate_replays. It never prices anything. A reusablesolwyn.testing.contractpack is dogfooded against both the double and the live API, andsolwyn.testing.pytest_pluginprovides explicitly opt-in, function-scoped fixtures — never automatic registration. See Testing budget enforcement. - Type-transparent wrappers —
Solwyn(client)andAsyncSolwyn(client)report the wrapped provider class through__class__, soisinstance(wrapper, OpenAI)isTrueand frameworks admit the wrapper, whiletype(wrapper)stays truthful. Public attribute writes and deletes forward to the provider client;copy.copyandcopy.deepcopyreturn 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 aRunHandlewhosefinish()closes it;create_run(...)snapshots a stable run id, parent, and inherited tags without changing the current context, andhandle.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
AsyncSolwyndefault client plus recipe-local model and provider adapters; no module ships), LangChain and LangGraph (solwyn[langchain]adds the content-freeSolwynRunScopeHandler; the exact raw-response shim admits basic non-streaminginvoke/ainvokeand two-node graphs), and CrewAI (solwyn[crewai]adds the structuralSolwynEventListener; native LiteLLM stays attribution-only, and a narrow custom-BaseLLMrecipe 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 theresponses.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 streamingparse, andextra_bodyoverrides ofmodel,input,instructions,max_output_tokens, orstreamare refused withConfigurationErrorbefore 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 inspectingbase_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 withConfigurationError(field="client")and unknown names withfield="provider". See Explicit provider identity. CostPolicyconsumes server price hints — every budget check opts intoprice_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 reportsfailover_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 labelledcircuit_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=raiserefuses untracked or unknown capabilities before provider I/O with the exportedUntrackedSpendSurfaceError, which names the exact refused capability and its acknowledgment token;warnremains the default andallowthe explicit unrestricted posture.acknowledge_untracked/SOLWYN_ACKNOWLEDGE_UNTRACKEDaccept exact terminal tokens only — namespaces, wildcards, tracked, blocked, and unsupported leaves are rejected at construction — withaudio.speech.create:gpt-4o-mini-ttsfor the conditional TTS exception.coverage(client)returns a deterministic, content-free, sans-I/OCoverageReportyou can pin withexpect(...)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/allowobservations 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, andreport_untracked_surfaces=False/SOLWYN_REPORT_UNTRACKED_SURFACES=falseturns the channel off without changing local behavior. See Advisory reports and Privacy. - Anthropic 1.x on
httpx2— Solwyn never importshttpx2; it recognizes the stack by identity. Per-hop timeouts are delivered as a nativehttpx2.Timeoutwhen the client's own timeout class is identity-proven (so Anthropic'sx-stainless-read-timeoutheader carries the numeric bound), and as a granular four-tuple otherwise. Anthropic 1.0's promoted-stablefilesandskillssurfaces are classified as unmetered spend under the ordinaryon_unmeteredposture. See Anthropic. - New configuration —
on_unmetered,acknowledge_untracked,report_untracked_surfaces, and the sevenvelocity_*fields, each with aSOLWYN_*environment variable, plus the constructor-onlycontrol_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_ttlwindow. A hit replays only its own entry's hints; run-scoped and tagged calls never read it.CostPolicywarns 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_reporterno 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, andstreamare tracked and are rejected as acknowledgment tokens; the still-unmeteredresponses.retrieveremains 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_stoppeddenials stay sticky only for their run — a control-plane outage can no longer replay a dashboard stop against unrelated run ids, andBudgetExceededError.budget_periodnow carries the actual Cloud or lease label (agent_run,tag,model,provider, or a period), falling back tounknownonly when none was supplied.- Breaking for anyone passing
inf: failover timeouts must be finite —failover_total_timeoutandfailover_hop_read_timeoutreject booleans,NaN, and±infat 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-shaped4xxrecords no breaker failure, while failover and post-send-ambiguous errors keep the breaker verdict. Previously a repeated400onresponses.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 mergesextra_bodyafter 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
httpx2transport error is dispositioned exactly like itshttpxtwin 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.testingcontract probe opts into price hints and validates the served map — every key must be a knownProviderName(an unknown key makes the whole check response unparseable, so the SDK would fail open with no reservation) and every value a finite number;{}andnullremain valid.
Removed
Solwyn.update_price_hints/AsyncSolwyn.update_price_hintsand 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, soCostPolicykeeps 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={...})orSOLWYN_TAGSapply to every intercepted call from the client. The environment value is comma-separatedkey=valueentries, split at the first=; values containing commas use the constructor mapping. Run-scope tags override client defaults key by key, and per-callsolwyn_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/checkcarries the same immutable tag snapshot its spend event reports, which is what a tag-scoped budget decides on. A denial withdenied_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 carryparent_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 activeRunContext(id, name, tags)with a defensive copy of the scope's tags.RunContextand the attribution boundsTAGS_MAX_KEYS,TAG_KEY_MAX_LENGTH, andTAG_VALUE_MAX_LENGTHare 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=Falsestarts 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 withlease_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_modedecide. Past the lease deadline with Solwyn still down, yourfail_openposture picks the floor:fail_open=Trueadmits calls uncounted and tallies every one for the next successful renewal to report, whilefail_open=Falsemeters against the freshest known remainder and denies at your configured mode. Uncounted episodes announce themselves —lease.uncounted_entryon entry, thenlease.uncounted_continuingat most every 30 seconds. See When Solwyn Cloud is unreachable. failover_hop_read_timeout(default600.0) — each hop's read/write bound, now decoupled from the failover window.600.0matches 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_thresholdandcontrol_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, andreporter_shutdown_deadline, each with aSOLWYN_*environment variable, plus the constructor-onlyfailover_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 withoutclose()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 survivefork(), 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(default5.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 holdclose()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 —
AsyncSolwynpreviously flushed only after an explicitstart(), whichasync withperforms for you. Constructed withoutasync with, it queued events and settlements silently untilclose(), 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(default30.0) is now purely the failover window: the budget pre-flight, each hop's connect/pool slice,Retry-Aftersleeps, and advancement between hops. Reads are bounded separately byfailover_hop_read_timeout. The old behavior cut a slow-but-connected provider at 30 seconds and re-raised it asAPITimeoutError— but a read timeout is post-send ambiguous, and under the defaultfailover_idempotency="safe"it re-raises without failing over. That cut therefore bought no failover; it only converted legitimately slow generations (reasoning models, largemax_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
openaiextra no longer installstiktoken— 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(default60.0seconds), rather than on every cycle. A failed send leaves that provider due for the next cycle, andclose()forces a final snapshot. call_idpins 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.TimeoutExceptionfamily inAPITimeoutError, so a provably pre-sendConnectTimeoutreached 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.APITimeoutErroris now classified by its chained cause, the wayAPIConnectionErroralready 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_tokensand anothermax_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_unreadableat 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 stableagent_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 onrun(); the SDK stamps the run id on the check and enforces the answer, raising the sameBudgetExceededErrorinhard_denymode. 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 thesolwyn.budgetlogger 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 reservedsolwyn_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=FalseorSOLWYN_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, andfalseapplies 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, afailover_total_timeoutchange 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) andusage.input_tokens_details.cache_write_tokens(Responses API) are now extracted and reported ascache_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_tokensstays 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. UnsupportedSurfaceErrorin the package root —from solwyn import UnsupportedSurfaceErrornow 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, sofrom solwyn.exceptions import UnsupportedSurfaceErrorkeeps 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) andmodels.embed_content(Google) are budget-checked and recorded as cost events with modalityembedding, priced from input tokens; Google exposes no usage on embeddings, so its counts are length-estimated and flaggedis_estimated. See Surface coverage. - Image interception —
images.generateandimages.edit(OpenAI, compat, native Together) andmodels.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.createandaudio.speech.create(OpenAI, compat incl. Groq, native Together). Transcriptions price from usage token buckets or whole-second durations on a JSONresponse_format— a non-JSON format is recorded unpriced, never guessed; speech prices from the input character count measured inside the privacy firewall.audio.translationsand usage-less TTS models (gpt-4o-mini-tts) warn once and pass through. See Surface coverage. - Video interception —
videos.create(OpenAI Sora) andmodels.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.createfails loud instead of passing through untracked. MediaUsageon the wire — non-token billable quantities (image_count,generation_count,video_seconds,audio_seconds,input_characters) plus theresolution/qualityselectors, and amodalityfield on every cost event. Pre-flight budget checks carryestimated_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
TogetherorAsyncTogetherclient 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. Thesolwyn[together]extra installstogether>=2.0as 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 callingglm-*models) is detected and attributed asprovider="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
CostPolicyno-op warning —CostPolicyis 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.warningcalls 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 whenfail_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 honorfail_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
modein the Cloud budget response rather than the localbudget_modeconfig, fixing local/Cloud mode drift (e.g. localalert_onlyvs. a Cloudhard_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-runtimeclient:Solwyn(boto3.client("bedrock-runtime")). Interceptsconverse/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 viausage.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); thesolwyn[bedrock]extra installsboto3>=1.34as a convenience.invoke_model/invoke_model_with_response_streamraiseConfigurationErrorinstead of silently bypassing budget tracking. See Amazon Bedrock. - OpenAI-compatible providers — Point an
openai.OpenAIclient at xAI, DeepSeek, Mistral, Qwen, Groq, Together, Fireworks, Perplexity, Azure OpenAI, OpenRouter, Ollama, vLLM, LM Studio, or any compatible endpoint viabase_url; Solwyn detects the real provider for attribution, budgets, and failover instead of mislabeling itopenai. 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 raiseConfigurationErrorat 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_bodyare stripped andmax_completion_tokens⇄max_tokensis 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.reporterlogger (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
providervalues (bedrockplus fourteen OpenAI-compatible identifiers), optionalprovider_region(omitted when unset),service_tieron 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=acceptsHealthBasedPolicy(default) orLatencyPolicyto 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), andselection_policy=. - Failover tuning —
failover_total_timeout,failover_idempotency("safe"/"never"/"always"),same_provider_retries, andcircuit_breaker_recovery_timeout_jitter. Per-call override with thesolwyn_idempotentkeyword. - New exceptions —
UntranslatableRequestErrorandUntranslatableModelError, both carrying structural labels only. - New metadata fields —
is_provider_fallback,requested_provider,requested_model,failover_reason,failover_error_class,attempt_index,possibly_succeeded, andcall_idon the reported event.
Changed (BREAKING)
fallback_modelandprimary_providerremoved. Same-provider model fallback is now expressed as a same-provider entry in thefallback=chain (e.g.fallback=[(OpenAI(), "gpt-4o-mini")]); the primary provider is always auto-detected from the wrapped client. TheSOLWYN_FALLBACK_MODELandSOLWYN_PRIMARY_PROVIDERenvironment variables are no longer read. Migration: replacefallback_model="x"withfallback=[(SameClient(), "x")], and delete anyprimary_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 aThreadPoolExecutorwith the active run tag preserved (contextvarsdo 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_idandagent_run_nameadded 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) andcache_creation_1h_tokens(1-hour TTL, 2× rate), with an aggregate fallback for older responses. - OpenAI service tier — The response
service_tieris captured and reported on the metadata event when present.
0.1.3 — 2026-05-01
Changed (BREAKING)
- Single-credential SDK contract.
project_idremoved fromSolwyn()/AsyncSolwyn()constructors and theSOLWYN_PROJECT_IDenvironment variable is no longer read. The project is resolved server-side from the API key. Migration: delete theproject_id=kwarg and unsetSOLWYN_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 thefallback=chain.)
0.1.1 — 2026-04-15
Changed
- Tag-derived versioning — Migrated to
hatch-vcsso 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())andAsyncSolwyn(AsyncOpenAI())work with no kwargs whenSOLWYN_API_KEYis set in the environment. SolwynandAsyncSolwynclient 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
TokenDetailsformat. - Budget enforcement — Pre-call budget checks via Solwyn Cloud API with local fallback. Supports
alert_only(default) andhard_denymodes. - 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,ConfigurationErrorwith structured attributes. - OpenAI support — Chat Completions and Responses API response shapes. Optional
tiktokenextra 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_metadatahandling. - Fail-open default — LLM calls proceed when Solwyn Cloud is unreachable. Local tracking until connectivity is restored.
- Context manager support —
with Solwyn(...)andasync with AsyncSolwyn(...)for automatic resource cleanup.