OpenAI Client
The OpenAI client provides access to GPT models through the OpenAI API. It shares its chat base (_OpenAIBase) with Grok, Mistral, and Voyage.
Configuration
from padwan_ai.openai import OpenAIClient
client = OpenAIClient(
api_key="sk-...", # or set OPENAI_API_KEY env var
model="gpt-4o", # default model
)
padwan_ai.openai.supports_audio(model, fmt=None) / supports_vision(model) check model capabilities; AUDIO_FORMATS lists supported formats.
Usage
Basic Chat
from padwan_ai.conversation import Message
async with OpenAIClient() as client:
response, usage = await client.complete_chat(
[Message(role="user", content="Hello!")]
)
print(response["content"])
Streaming
from padwan_ai.conversation import Message
async with OpenAIClient() as client:
stream = client.stream_chat([Message(role="user", content="Tell me a story")])
async for chunk in stream:
print(chunk, end="")
With System Prompt
from padwan_ai import ConversationState
state = ConversationState(system="You are a helpful assistant.")
state.add_user_message("Hello!")
async with OpenAIClient() as client:
response, usage = await client.complete_chat(state.messages)
state.add_assistant_response(response)
state.accumulate_usage(usage)
Method Outputs
response, usage = await client.complete_chat(messages)
stream = client.stream_chat(messages)
async for chunk in stream:
...
usage = stream.usage
Embeddings
from padwan_ai import vectors
async with OpenAIClient(model="text-embedding-3-small") as client:
resp = await client.fetch_embeddings(["text 1", "text 2"], dimensions=512)
vecs = vectors(resp) # one list[float] per input, matched by index
padwan_ai.vectors(resp, provider) returns the vectors in input order for any provider's payload; provider defaults to the OpenAI shape, pass "gemini" for Gemini. fetch_embeddings is available on every OpenAI-compatible client (Mistral, Grok, custom base_url endpoints). Pass model= to reuse a chat client; provider-only body fields go through extra_params. With extra_params={"encoding_format": "base64"} (about 4× smaller responses), vectors() decodes the float32 payload back to floats.
complete_chat/stream_chat also accept extra_params for provider-only body fields. For raw request bodies, complete/stream bypass the ChatMessage shaping. All four take headers= for headers sent with that request only (e.g. a gateway's per-user attribution on a client every user shares), on every OpenAI-compatible client; they are merged over the session's, so a same-named key, Authorization included, replaces it. AgentSession does not forward them.
stream_raw(body, headers=...) yields the provider's SSE bytes unparsed (data: lines and [DONE] included), for a backend that relays them to a client reading the OpenAI wire format, chunk by chunk as they arrive. A compressed response arrives decoded, so a relay must not copy the upstream Content-Encoding. The status is checked before the first byte, so an error raises the same exceptions as stream; OpenTelemetry instrumentation does not cover it.
Transcription
transcribe posts an audio file to /audio/transcriptions and returns {"text": ...}
(the default json response format; verbose_json timestamps are not exposed). file=
takes a path or raw bytes; the provider reads the format from the file name, so raw bytes
need filename.
result = await client.transcribe(
file=audio_bytes,
filename="note.webm",
model="whisper-1",
language="fr",
prompt="Soren, Loxam", # spelling hints
)
print(result["text"])
It also accepts content_type, temperature, headers= (sent with that request only) and extra_params, form fields for provider-only parameters (e.g. a gateway's metadata, as a JSON string); pass pairs to repeat a field (e.g. timestamp_granularities[]); a field that has a named argument (model, language, ...) raises ValueError.
Batch Processing
OpenAI supports batch processing via a file-based API. Requests are serialized to JSONL, uploaded, and results are retrieved as a file once the batch completes.
Creating a batch
from padwan_ai.openai import OpenAIClient, BatchRequest
async with OpenAIClient() as client:
requests = [
BatchRequest(
body={"messages": [{"role": "user", "content": "Question 1"}]},
custom_id="q1",
),
BatchRequest(
body={"messages": [{"role": "user", "content": "Question 2"}]},
custom_id="q2",
),
]
job = await client.create_batch(requests, model="gpt-4o")
print(job.id) # e.g. "batch_abc123"
BatchRequest wraps a CreateChatCompletionRequest body and an optional custom_id. If custom_id is omitted, requests are auto-keyed as request-0, request-1, etc. The model field is injected into each JSONL line automatically.
Polling for results
from padwan_ai.openai import BatchResult
job = await client.get_batch(job.id)
if job.succeeded:
results = await client.get_batch_results(job.output_file_id)
for result in results:
print(result.custom_id, result.content)
Listing and cancelling
jobs, next_cursor = await client.list_batches(limit=10)
job = await client.cancel_batch("batch_abc123")
Pass after= (the previous cursor) to list_batches to page further. upload_batch_file uploads a JSONL file directly if you'd rather build it yourself than pass BatchRequests to create_batch.
Batch types reference
| Type | Description |
|---|---|
BatchRequest |
Single request: body, custom_id |
BatchJob |
Job state: id, status, input_file_id, output_file_id, error_file_id, errors, request_counts, is_terminal, succeeded |
BatchResult |
Parsed result: custom_id, content, input_tokens, output_tokens, total_tokens |
Realtime (Speech-to-Speech)
RealtimeClient opens speech-to-speech sessions with the GA gpt-realtime model over a WebSocket. Wire shapes follow the OpenAI Realtime guide. Requires the realtime extra (niquests WebSocket support):
Audio in both directions is mono little-endian PCM16 at 24 kHz (REALTIME_SAMPLE_RATE), the gpt-realtime native rate.
from padwan_ai import RealtimeClient
client = RealtimeClient(
api_key="sk-...", # or set OPENAI_API_KEY
model="gpt-realtime", # default
timeout=30.0, # bounds the upgrade handshake only
)
timeout bounds only the WebSocket upgrade handshake. Reads on the open socket are unbounded, so the model can stay silent between turns without the connection being torn down.
async with client as conn: opens the underlying niquests.AsyncSession, performs the handshake, and yields the live RealtimeConnection; everything is closed on exit.
Conversation with server VAD (default)
By default the server decides when you have stopped talking. Stream microphone audio in with append_audio and consume events by async-iterating the connection:
from padwan_ai import RealtimeClient
from padwan_ai.openai import RealtimeServerEvent
async with RealtimeClient(instructions="Answer briefly.", voice="marin") as conn:
await conn.append_audio(pcm16_chunk) # mono PCM16 @ 24 kHz
async for event in conn:
if audio := conn.audio_delta_bytes(event):
playback.write(audio) # PCM16 bytes
elif event["type"] == RealtimeServerEvent.RESPONSE_DONE:
break
Push-to-talk (manual turns)
Pass NO_TURN_DETECTION to disable server VAD, then drive each turn yourself:
from padwan_ai import RealtimeClient
from padwan_ai.openai import NO_TURN_DETECTION
async with RealtimeClient(turn_detection=NO_TURN_DETECTION) as conn:
await conn.append_audio(recorded_pcm16)
await conn.commit_audio()
await conn.create_response()
async for event in conn:
...
Client parameters
| Parameter | Default | Description |
|---|---|---|
model |
"gpt-realtime" |
Realtime model id |
instructions |
None |
System prompt steering the voice agent |
voice |
"marin" |
Spoken voice (RealtimeVoice) |
turn_detection |
None |
None/falsy uses server_vad; a mapping (e.g. {"type": "semantic_vad"}) is sent verbatim; NO_TURN_DETECTION disables VAD |
transcription_model |
"whisper-1" |
Transcribes your own speech; None disables |
output_modalities |
("audio",) |
Response modalities; ("audio", "text") also emits text |
sample_rate |
24_000 |
PCM16 sample rate in both directions |
timeout |
30.0 |
Bounds the upgrade handshake |
api_key |
None |
Falls back to OPENAI_API_KEY |
base_url |
realtime endpoint | Custom wss:// endpoint |
Constructing OpenAIRealtimeClient directly additionally accepts session_kwargs= to forward constructor arguments (e.g. proxies) to the managed AsyncSession. To reconfigure a live session, call conn.configure(...). For events not covered by the helper methods, conn.send_event(...) sends a raw event dict; conn.session_payload holds the session config sent on connect.
Server events
Iterating the connection yields each server event as a decoded JSON dict. RealtimeServerEvent names the common ones; any other event type passes through as a plain dict.
| Event | Meaning |
|---|---|
SESSION_CREATED / SESSION_UPDATED |
Session lifecycle acknowledgements |
SPEECH_STARTED / SPEECH_STOPPED |
Server VAD detected speech boundaries |
AUDIO_DELTA / AUDIO_DONE |
Model audio chunks (decode with audio_delta_bytes) and end of audio |
AUDIO_TRANSCRIPT_DELTA / AUDIO_TRANSCRIPT_DONE |
Transcript of the model's speech |
INPUT_TRANSCRIPT_DELTA / INPUT_TRANSCRIPT_COMPLETED |
Transcript of your speech |
RESPONSE_CREATED / RESPONSE_DONE |
Response lifecycle |
ERROR |
Server-reported error |
Realtime limitations
- Reconnection is the caller's responsibility: a dropped socket ends iteration, and a new
async with client as conn:starts a fresh session with no server-side memory of the previous one.