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 shape | Settlement |
|---|---|
create / parse, usage present | Exact provider counts — all eight token categories, at parity with Chat Completions. |
create / parse, usage missing or zeroed | A 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 completion | Terminal usage, via until_done() / get_final_response() or plain iteration. |
stream() helper entered, then abandoned | Marked input-only estimate; lease reservation kept. |
stream() helper closed before entry | Reservation released; no cost event. |
Three request shapes are refused with ConfigurationError before the budget check, because they cannot be metered:
| Shape | field | Why |
|---|---|---|
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.
Supported models
The SDK recognizes OpenAI models by the following prefixes:
| Prefix | Examples |
|---|---|
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 field | Chat Completions API source | Responses API source |
|---|---|---|
input_tokens | usage.prompt_tokens | usage.input_tokens |
output_tokens | usage.completion_tokens | usage.output_tokens |
cached_input_tokens | usage.prompt_tokens_details.cached_tokens | usage.input_tokens_details.cached_tokens |
reasoning_tokens | usage.completion_tokens_details.reasoning_tokens | usage.output_tokens_details.reasoning_tokens |
audio_input_tokens | usage.prompt_tokens_details.audio_tokens | usage.input_tokens_details.audio_tokens |
audio_output_tokens | usage.completion_tokens_details.audio_tokens | usage.output_tokens_details.audio_tokens |
accepted_prediction_tokens | usage.completion_tokens_details.accepted_prediction_tokens | usage.output_tokens_details.accepted_prediction_tokens |
rejected_prediction_tokens | usage.completion_tokens_details.rejected_prediction_tokens | usage.output_tokens_details.rejected_prediction_tokens |
cache_creation_5m_tokens | usage.prompt_tokens_details.cache_write_tokens | usage.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.