SOLWYN
Providers

OpenAI

Use Solwyn with OpenAI — sync and async examples, token details, model support

import os
from openai import OpenAI
from solwyn import Solwyn

client = Solwyn(
    OpenAI(),
    api_key=os.environ["SOLWYN_API_KEY"],
)

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Explain how a CPU works in two sentences."}],
)

print(response.choices[0].message.content)
client.close()

Sync usage

Pass an openai.OpenAI client to Solwyn(). Calls use client.chat.completions.create(), exactly like the standard OpenAI SDK:

import os
from openai import OpenAI
from solwyn import Solwyn

with Solwyn(
    OpenAI(),
    api_key=os.environ["SOLWYN_API_KEY"],
) as client:
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": "You are a helpful assistant."},
            {"role": "user", "content": "Explain how a CPU works in two sentences."},
        ],
    )
    print(response.choices[0].message.content)

Async usage

Pass an openai.AsyncOpenAI client to AsyncSolwyn():

import asyncio
import os
from openai import AsyncOpenAI
from solwyn import AsyncSolwyn

async def main():
    async with AsyncSolwyn(
        AsyncOpenAI(),
        api_key=os.environ["SOLWYN_API_KEY"],
    ) as client:
        response = await client.chat.completions.create(
            model="gpt-4o",
            messages=[
                {"role": "system", "content": "You are a helpful assistant."},
                {"role": "user", "content": "Explain how a CPU works in two sentences."},
            ],
        )
        print(response.choices[0].message.content)

asyncio.run(main())

Responses API

SDK v0.6.0+. client.responses.create(...), client.responses.parse(...), and the client.responses.stream(...) helper are intercepted, budget-checked, and metered on native OpenAI and Azure OpenAI clients — sync and async — exactly like Chat Completions:

import os
from openai import OpenAI
from solwyn import Solwyn

with Solwyn(OpenAI(), api_key=os.environ["SOLWYN_API_KEY"]) as client:
    response = client.responses.create(
        model="gpt-4o",
        instructions="You are a helpful assistant.",
        input="Explain how a CPU works in two sentences.",
    )
    print(response.output_text)

The pre-flight estimate counts the text parts of input and instructions (image parts contribute nothing and are not separately priced; provider-reported image tokens fold into input_tokens), the call is recorded with modality text, and a lease-backed call's output allowance reads max_output_tokens. The service_tier on the response or stream is captured as for chat.

Call shapeSettlement
create / parse, usage presentExact provider counts — all eight token categories, at parity with Chat Completions.
create / parse, usage missing or zeroedA marked estimate: input tokens from request length, output 0, is_estimated=true. A lease-backed call keeps its full reservation.
create(stream=True)Usage read from the terminal response.completed event; the SDK never sends stream_options — Responses has no include_usage, and usage arrives on that terminal event.
stream() helper consumed to completionTerminal usage, via until_done() / get_final_response() or plain iteration.
stream() helper entered, then abandonedMarked input-only estimate; lease reservation kept.
stream() helper closed before entryReservation released; no cost event.

Three request shapes are refused with ConfigurationError before the budget check, because they cannot be metered:

ShapefieldWhy
background=True (as an argument, in default_params, or via extra_body)"background"A queued background response exposes no create-time usage. Use the raw OpenAI client.
parse(...) with streaming in effect"stream"Use create(stream=True) or the stream() helper, or the raw client for streaming parse.
extra_body overriding model, input, instructions, max_output_tokens, or stream"extra_body"Pass these as top-level arguments so preflight and dispatch see the same values. Other extra_body keys pass through.

Responses calls are primary-only: they never fail over, the budget check declares no fallback chain, and an open primary breaker raises ProviderUnavailableError with attempted=[]. Only the constructor's global default_params apply, with the chat-only keys max_tokens, max_completion_tokens, and stream_options filtered out; a default instructions or max_output_tokens reaches both preflight and the wire.

responses.stream(response_id=...) and starting_after=... — resuming an existing response — are raw pass-through and never metered. Every other responses leaf (retrieve, delete, cancel, compact, input_items.list, input_tokens.count, the with_raw_response family) is untracked and follows on_unmetered; responses.retrieve can be acknowledged, responses.create / parse / stream cannot, because they are metered. Behind a corporate gateway base_url, pin provider="openai" to keep this metering — auto-detection would otherwise select the generic compatible adapter, where every Responses leaf is untracked and merely follows your on_unmetered posture. See Streaming for the two streaming shapes and the sync/async difference.

Decisions

SDK v0.9.0+. Native OpenAI client.decisions.create(...) (openai>=3.26) is budget-checked and settled with text pricing on sync and async clients.

  • Provider usage supplies input, output, cache-read, cache-write, and reasoning token counts. Missing or zeroed usage falls back to a length estimate marked token_details.is_estimated=True. Settlement uses the requested model even when the response echoes a dated snapshot.
  • Pre-flight counts the text in input and in question instructions, choices, and score levels. Image content contributes no estimated text tokens. Unmeasured output keeps its reserved lease bound, as do ambiguous failures and interruptions after dispatch.
  • Pass model, input, and questions as top-level arguments. Overriding them, or stream, in extra_body raises ConfigurationError before dispatch.
  • Decisions calls are served by the primary only, without model or provider failover.
  • The OpenAI DecisionsToolRunner and its async twin meter every API call when their run(...) method receives the Solwyn-wrapped client.
  • Azure OpenAI and compatible providers keep decisions.create unmetered, under your on_unmetered posture. So do the raw/streaming response mirrors and the read-only admin.organization.usage.decisions historical usage report.
  • Upgrading from 0.8.0: remove decisions.create from acknowledge_untracked on native OpenAI clients. A stale token raises ConfigurationError, including when the list is shared with Azure or compatible clients.

Supported models

The SDK recognizes OpenAI models by the following prefixes:

PrefixExamples
gpt-*gpt-4o, gpt-4o-mini, gpt-4.1, gpt-5
o3*o3, o3-mini
o4*o4-mini

Model detection is used for token estimation before calls. After each call, the SDK reads exact token counts from the provider response regardless of model prefix.

OpenAI is priced for all five modalities — text, embedding, image, audio, and video. See the provider × modality matrix.

Token detail fields

The SDK extracts the following fields from OpenAI responses — Chat Completions and Responses API calls alike:

Normalized fieldChat Completions API sourceResponses API source
input_tokensusage.prompt_tokensusage.input_tokens
output_tokensusage.completion_tokensusage.output_tokens
cached_input_tokensusage.prompt_tokens_details.cached_tokensusage.input_tokens_details.cached_tokens
reasoning_tokensusage.completion_tokens_details.reasoning_tokensusage.output_tokens_details.reasoning_tokens
audio_input_tokensusage.prompt_tokens_details.audio_tokensusage.input_tokens_details.audio_tokens
audio_output_tokensusage.completion_tokens_details.audio_tokensusage.output_tokens_details.audio_tokens
accepted_prediction_tokensusage.completion_tokens_details.accepted_prediction_tokensusage.output_tokens_details.accepted_prediction_tokens
rejected_prediction_tokensusage.completion_tokens_details.rejected_prediction_tokensusage.output_tokens_details.rejected_prediction_tokens
cache_creation_5m_tokensusage.prompt_tokens_details.cache_write_tokensusage.input_tokens_details.cache_write_tokens
cache_creation_1h_tokens----

The SDK handles both the Chat Completions API response shape (prompt_tokens / completion_tokens) and the Responses API shape (input_tokens / output_tokens) automatically, with all eight token categories at parity. Detail sub-objects may be None on older responses -- all missing fields default to 0.

New in 0.3.0: the SDK now extracts prompt-cache writes from cache_write_tokens and reports them 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 that maps OpenAI's cache writes onto the existing cache_creation_5m_tokens bucket to preserve the cross-provider contract, not a claim about OpenAI's TTL. cache_creation_1h_tokens always reports 0 for OpenAI. Cache-write tracking is independent of cache reads (cached_input_tokens, unchanged above) and requires no opt-in or configuration. Missing or unusable values (wrong type, negative, boolean, string) degrade to 0 rather than raising.

Service tier

When OpenAI returns a service_tier on the response, Solwyn captures it (bounded and string-only) and reports it on the metadata event's service_tier field, so the dashboard can distinguish standard, flex, and priority traffic. It is absent when the provider does not return one. See Privacy.

Image generation

client.images.generate(...) and client.images.edit(...) are intercepted. The pre-flight budget check is exact — computed from the request's image count, size, and quality — so an over-budget request is denied before OpenAI is called. gpt-image models bill text and image tokens at separate rates and return image-token usage on the response, which the SDK reports as image_input_tokens / image_output_tokens. Each call is recorded as a cost event with modality image and bounded media details — image count, resolution, and quality — never the prompt and never the returned image bytes.

Audio

Both audio surfaces are intercepted, budget-checked, and recorded as cost events with modality audio.

Transcription — client.audio.transcriptions.create(...) is intercepted. Token-billed transcription models (the gpt-4o-transcribe family) price from their usage token buckets, with audio input tokens reported separately as audio_input_tokens. whisper-1 prices from the whole-second audio duration the provider reports on its usage block — present whenever you request a JSON response_format (json or verbose_json). A transcription made with a non-JSON response_format (text, srt, or vtt) returns no usage, so the call is recorded but left unpriced, and the SDK warns once suggesting a JSON response_format.

Speech (TTS) — client.audio.speech.create(...) is intercepted. tts-1 and tts-1-hd price exactly from the input's character count, measured inside the privacy firewall — the input text never leaves your process — so the pre-flight budget check is exact (characters × the model's per-character rate) and an over-budget call is denied before OpenAI is reached. Token-billed TTS models (gpt-4o-mini-tts) return no usage metadata of any kind, so they are deliberately not tracked: the call follows your on_unmetered posture (warn once by default) — never a fabricated $0 or an estimate. Acknowledge it with the conditional token audio.speech.create:gpt-4o-mini-tts.

client.audio.translations is recognized but untracked — it follows on_unmetered too (the resource is a scoped escape; audio.translations.create is the operation).

Only token counts, whole-second durations, and character counts leave your process — never audio bytes, never the input text, never the transcript.

Video

client.videos.create(...) (Sora) is intercepted, budget-checked, and recorded as a cost event with modality video. The pre-flight budget check is exact — the request's seconds at the resolution derived from size, priced at that resolution variant's per-second rate — so an over-budget generation is denied before OpenAI is called. OpenAI's documented defaults apply when omitted: seconds defaults to 4 and size to 720x1280.

Video generation is asynchronous. videos.create returns a video job that carries no usage, and Solwyn passes that job through untouched — you poll or retrieve it as usual. Because the job offers no billable basis and OpenAI does not charge for failed or blocked generations, billing settles at initiation: the cost event is recorded at request time from the requested duration and resolution and is always flagged is_estimated, a deliberate and conservative over-count. Every other client.videos attribute (retrieve, list, download_content, ...) passes through to the underlying client.

Only the requested duration and the resolution selector leave your process — never the prompt, never a reference image, never the generated video.

The Sora price cards are lifecycle-deprecated in Solwyn's pricing dataset but still bill: historical and present traffic on those ids prices normally, never rejected as an unknown model.

The openai extra

pip install "solwyn[openai]"

The extra pins the tested openai SDK floor. That is all it does.

Changed in 0.4.0: it no longer installs tiktoken. Pre-call token estimation is now always the heuristic estimator (roughly 4 characters per token), which is what budget pre-flight needs — a conservative estimate, not an exact count. Exact tokenization was never used for billing.

Post-call token counts are unaffected and remain exact: they are read directly from the OpenAI response, not estimated. See Privacy for why estimation is deliberately length-based.

Passthrough attributes

Any attribute not intercepted by Solwyn is resolved against the underlying openai.OpenAI client through the coverage guard. SDK v0.6.0+: a capability Solwyn does not meter — client.models.list(), files, batches, fine_tuning, moderations, vector_stores, the non-metered responses leaves, with_raw_response — warns once per process by default and is then forwarded; set on_unmetered="raise" to refuse such calls, "allow" to silence them, or acknowledge exact leaves with acknowledge_untracked. Namespaces come back as guarded resources whose members are classified individually; structural attributes such as base_url and timeout are always silent (in strict mode, client.timeout reads are preserved since 0.9.0). SDK v0.7.0+: openai 3.8's safety.alerts.retrieve and its raw- and streaming-response variants are classified the same way, with safety and safety.alerts as guarded namespaces. SDK v0.8.0+: the ledger is audited against openai 3.16.2. openai 3.14's beta.agents tree (agents; environments with files and templates; sessions with artifacts, events, items, turns, and subagents; vaults with credentials) and live (sessions, forks, sideband) are guarded namespaces whose leaf operations — beta.agents.create, beta.agents.sessions.create, live.create, live.sessions.accept, and the rest — are unmetered spend acknowledgeable by exact token, with their with_raw_response / with_streaming_response mirrors as raw_response-scoped escapes. openai 3.16's webhook management — webhooks.create, .retrieve, .update, .delete, .list, .rotate_secret, .test, and the webhooks.event_types namespace with its list — is unmetered spend under the same posture, next to the existing webhooks.unwrap / verify_signature. Nothing was promoted to metered. SDK v0.9.0+: the ledger is audited through openai 3.37, and native decisions.create is now metered. Newly recognized unmetered surfaces include audio.voices and audio.voice_consents, the realtime translation websocket realtime.translations.connect (classified like realtime.connect) and realtime.translations.client_secrets, beta.agents.environments.files, beta.agents.sessions.artifacts, beta.agents.sessions.traces, beta.agents.sessions.turns.items, beta.agents.vaults, admin.organization.projects.api_keys, admin.organization.usage.decisions, admin.organization.external_storage, and safety.cases. They follow on_unmetered and need exact acknowledgment tokens in strict mode; raw and streaming response helpers keep their separate acknowledgment scope.

On this page