axio-transport-openai¶
OpenAI and OpenAI-compatible completion transports.
api selects the endpoint. OpenAITransport defaults to "responses" and posts to
/v1/responses, reading it through axio-responses. NebiusTransport,
OpenRouterTransport and OpenAICompatibleTransport set "chat" and post to
/v1/chat/completions. That endpoint refuses function tools beside any reasoning effort
other than "none" — see the troubleshooting entry.
extra_params is folded into the request. Its tools are merged with the agent’s
function declarations rather than replacing them, so adding a hosted tool does not take
away the calls the agent is there to dispatch.
- class axio_transport_openai.OpenAITransport(name: 'str' = 'OpenAI', base_url: 'str' = <factory>, api_key: 'str' = <factory>, model: 'ModelSpec' = <factory>, models: 'ModelRegistry' = <factory>, session: 'aiohttp.ClientSession | None' = None, max_retries: 'int' = 10, retry_base_delay: 'float' = 5.0, extra_params: 'Mapping[str, Any]'=mappingproxy({}), *, api: "Literal['responses', 'chat'] | None"=None, reasoning_summary: "Literal['auto', 'concise', 'detailed']"='auto')[source]¶
- api: Literal['responses', 'chat'] | None¶
Which endpoint this server speaks.
"responses"is the one that takes function tools and reasoning together, and OpenAI recommends it for new work. Left unset it followsbase_url: only OpenAI’s own host is assumed to implement it.
- build_responses_payload(messages: list[Message], tools: list[Tool[Any]], system: str) dict[str, Any][source]¶
The request /v1/responses takes.
The system prompt goes in
instructionsrather than in a message. Tool calls and their outputs are items beside the messages rather than blocks inside them. This endpoint takes tools and reasoning together, which is the reason to prefer it. /v1/chat/completions refuses that pair outright for a model that reasons.
- async embed(texts: list[str]) list[list[float]][source]¶
Call the OpenAI-compatible /v1/embeddings endpoint.
- reasoning_summary: Literal['auto', 'concise', 'detailed']¶
How much of its reasoning the model summarises for a reader on /v1/responses. The raw chain is never returned; without a summary there is nothing to show.
- class axio_transport_openai.nebius.NebiusTransport(name: 'str' = 'Nebius AI Studio', base_url: 'str' = 'https://api.tokenfactory.nebius.com/v1', api_key: 'str' = <factory>, model: 'ModelSpec' = <factory>, models: 'ModelRegistry' = <factory>, session: 'aiohttp.ClientSession | None' = None, max_retries: 'int' = 10, retry_base_delay: 'float' = 5.0, extra_params: 'Mapping[str, Any]'=mappingproxy({}), thinking: 'bool' = False, *, api: "Literal['responses', 'chat']"='chat', reasoning_summary: "Literal['auto', 'concise', 'detailed']"='auto')[source]¶
- class axio_transport_openai.openrouter.OpenRouterTransport(name: 'str' = 'OpenRouter', base_url: 'str' = 'https://openrouter.ai/api/v1', api_key: 'str' = <factory>, model: 'ModelSpec' = ModelSpec(id='google/gemini-2.5-pro-preview', capabilities=frozenset(), max_output_tokens=8192, context_window=128000, input_cost=0.0, output_cost=0.0), models: 'ModelRegistry' = <factory>, session: 'aiohttp.ClientSession | None' = None, max_retries: 'int' = 10, retry_base_delay: 'float' = 5.0, extra_params: 'Mapping[str, Any]'=mappingproxy({}), thinking: 'bool' = False, *, api: "Literal['responses', 'chat']"='chat', reasoning_summary: "Literal['auto', 'concise', 'detailed']"='auto')[source]¶
- class axio_transport_openai.custom.OpenAICompatibleTransport(name: str = 'OpenAI', base_url: str = '', api_key: str = <factory>, model: ModelSpec = <factory>, models: ModelRegistry = <factory>, session: aiohttp.ClientSession | None = None, max_retries: int = 10, retry_base_delay: float = 5.0, extra_params: Mapping[str, Any]=mappingproxy({}), *, api: Literal['responses', 'chat']='chat', reasoning_summary: Literal['auto', 'concise', 'detailed']='auto')[source]¶
OpenAI-compatible transport for a single user-defined provider.
Instances are created by
CustomHubScreenwithname,base_url,api_key, andmodelspopulated from the JSON config. Supports JSON round-trip viato_dict()/from_dict().- api: Literal['responses', 'chat']¶
Which endpoint this server speaks.
"responses"is the one that takes function tools and reasoning together, and OpenAI recommends it for new work. Left unset it followsbase_url: only OpenAI’s own host is assumed to implement it.
- classmethod from_dict(data: dict[str, Any], *, session: ClientSession | None = None) Self[source]¶
As
OpenAITransport.from_dict(), butbase_url/api_keycome verbatim fromdata.The base implementation reads a value saved empty as empty, and falls back to the
OPENAI_BASE_URL/OPENAI_API_KEYenv vars only where the key is absent altogether. That is right for the built-in OpenAI provider, whose settings dict is written by hand and omits what it wants the default for.A custom provider’s config is a full round-trip of
to_dict(), so an absent key means the same thing as an empty one: this server takes no credential. Reading it through the environment would point a local server at an unrelated real endpoint.
- reasoning_summary: Literal['auto', 'concise', 'detailed']¶
How much of its reasoning the model summarises for a reader on /v1/responses. The raw chain is never returned; without a summary there is nothing to show.
ChatGPT subscription¶
ChatGPTTransport uses a pinned ChatGPTSession. Ordinary sign-in uses the public
Responses endpoint; axio-chatgpt login --codex uses native ChatGPT OAuth and the
Codex backend for completion and image generation.
It discovers visible models for that account, builds subscription-compatible requests,
and delegates HTTP/SSE recovery to ChatGPTResponses. Model copies share the account
session and catalog; account changes create a separate catalog. Subscription prices
are unknown (NaN). All server completion models receive function tools. Vision, reasoning,
and context limits come from the API metadata instead of the API-key registry.
Install axio-transport-openai and run axio-chatgpt login
to save and select a registration in the default XDG credentials file. Use --manual
for sign-in over SSH. The command prints an issued client ID after successful sign-in.
Keep this ID as the account reference; keep tokens in the credential store.
Use axio-chatgpt accounts, select CLIENT_ID, status, models, and logout
to manage saved accounts. status is offline; models fetches the account catalog.
These commands accept --credentials PATH. Status and model listing accept
--json and --account CLIENT_ID; the latter does not change the saved selection.
Reauthorize a registration with axio-chatgpt login --account CLIENT_ID.
import aiohttp
from axio import Agent, MemoryContextStore
from axio_transport_openai import ChatGPTTransport
from axio_transport_openai.credential_stores import FileCredentialStore
from axio_transport_openai.oauth import ChatGPTOAuth
from axio_transport_openai.session import ChatGPTSession
async def ask_saved_account(client_id: str, model_id: str) -> str:
store = FileCredentialStore()
async with aiohttp.ClientSession() as http:
oauth = ChatGPTOAuth(http, await store.host_id())
async with ChatGPTSession(store, oauth, client_id) as account:
transport = ChatGPTTransport(account=account, session=http)
await transport.fetch_models()
selected = transport.with_model(model_id)
agent = Agent(system="Be concise.", transport=selected)
return await agent.run("Hello", MemoryContextStore())
Choose model_id from that account’s visible catalog. Image input is enabled by
the catalog’s input_modalities. Explicit model_specs supply fallback metadata
for missing fields; a new model ID does not disable tools. Session closure stops
background renewal and retains credentials for the next process. Conversation persistence is separate;
use a persistent context store or serialize Message objects to resume history.
PlanConsentRequired means identity sign-in succeeded without subscription
permission. SignInRequired requires reauthorization of the selected registration.
ResponsesAPIError preserves the HTTP status, provider code, and request ID.
Quota exhaustion pauses inference for the shared account session. Call
resume_plan_usage() only after resolving the limit. Errors never select another
account or switch to API-key billing.
Run axio-repl --transport chatgpt --model gpt-6-luna to use the selected account
in the CLI. Scripts can use accounts.open_chatgpt(store, http, model=...) to
resolve the selected registration, fetch its catalog, and manage session lifetime.
It also accepts client_id= for an explicit account. Selection changes affect
future sessions only. TUI Configure ChatGPT provides sign-in, registration selection,
permission status and sign-out. The registry opens managed account sessions and calls
with_model() for model copies; generic config stores only account references.
The native sign-in mode saves credentials in the same Axio store. It does not
import the separate axio-transport-codex transport’s credentials.
Images¶
Authorize with axio-chatgpt login --codex before generating images. Native login
uses port 1455 by default; --manual supports SSH. The ordinary public plan-sharing
token is not accepted by the native image endpoint. The account’s authorization
mode is persisted and determines routing after restart.
ChatGPTImageTransport implements ImageGenTransport. Its models registry is
CHATGPT_IMAGE_MODELS, currently containing gpt-image-2 as a ChatGPTImageModel
with Capability.image_generation and Capability.image_editing.
Native ChatGPTTransport.fetch_models() also
appends these image presets to the completion catalog. They describe implemented
image support, not guaranteed account entitlement. Filter completion choices by
Capability.text and image choices by Capability.image_generation.
import aiohttp
from axio_transport_openai import ChatGPTImageTransport
from axio_transport_openai.chatgpt_images import image_media_type
from axio_transport_openai.session import ChatGPTSession
async def draw(account: ChatGPTSession, http: aiohttp.ClientSession) -> tuple[bytes, str]:
generator = ChatGPTImageTransport(account=account, session=http)
images = await generator.generate_images("An owl reading a book", quality="low")
return images[0], image_media_type(images[0])
For an open completion transport, chatgpt.image_transport() returns this class
with the same sessions. chatgpt.with_model("gpt-image-2").generate_images(prompt)
also returns image bytes. Image models reject stream() calls.
One image per request (n=1) is supported. The standalone generator accepts
size (auto, 1024x1024, 1536x1024, 1024x1536), quality (auto, low,
medium, high), and transparent_background. Native output is PNG.
Output format selection, partial images, and seeds are not exposed.
The protocol follows Codex CLI rust-v0.163.0
and posts to /backend-api/codex/images/generations on ChatGPT. Backend access
and quotas depend on the account. ChatGPTImageError preserves API diagnostics;
invalid results raise an error. Quota errors pause the shared account session.
Generation POSTs are never automatically replayed, including after server errors
or cancellation. A connection failure can leave the server outcome unknown.
The caller owns both sessions, and closing them retains credentials.
ChatGPTImageTransport also implements ImageEditTransport from axio.transport.
Use edit_images(prompt, images=[ImageBlock(...)]) for changes to existing images.
Sources are required and retain their order; PNG/JPEG/WebP bytes are sent as inline
image references to /backend-api/codex/images/edits. ChatGPTTransport.edit_images()
delegates to the same implementation. The standalone class accepts the generation
size/quality/background settings. The native protocol has no mask field;
mask must remain None. Editing shares the account’s quota gate and never retries
requests automatically. image_editing does not imply vision.
ChatGPTImageTools(chatgpt.image_transport()) creates two Tool instances in
.tools: generate_image(prompt) and edit_image(prompt, images). Attach these
to Agent.tools. The model supplies image IDs to the edit tool; the host registers
the actual attachments with add_image(ImageBlock(...)) and includes their IDs in
the conversation. Results contain new IDs and ImageBlocks for subsequent edits.
Use one tool set per conversation. Its read-only .images catalog can be persisted
by the host and restored with the constructor’s images= argument. No filesystem
paths, URLs, or raw base64 are requested from the model. Both tools declare external
access, so standard tool guards apply.
- class axio_transport_openai.ChatGPTTransport(*, account: ChatGPTSession, session: ClientSession, model: str | None = None, model_specs: Iterable[ModelSpec] = (), base_url: str | None = None, image_base_url: str = 'https://chatgpt.com/backend-api/codex', max_attempts: int = 3, retry_base_delay: float = 2.0, reasoning_effort: str | None = None, reasoning_summary: Literal['auto', 'concise', 'detailed'] = 'auto')[source]¶
A completion transport pinned to one authenticated account session.
The caller owns the HTTP and account sessions. with_model() shares both and the account catalog. with_account() starts a new catalog without changing existing transports. No API-key, embedding, or realtime interface is exposed.
- build_payload(messages: list[Message], tools: list[Tool[Any]], system: str) dict[str, Any][source]¶
Build a request after fetching and selecting a visible account model.
- property catalog: tuple[ChatGPTModel, ...]¶
Server entries and native image presets, with visibility and display names.
- async edit_images(prompt: str, *, images: list[ImageBlock], mask: ImageBlock | None = None, model: str | None = None, n: int = 1) list[bytes][source]¶
Edit supplied images with the selected image model, or GPT Image 2.
- async fetch_models() None[source]¶
Refresh this account’s catalog. Failure invalidates it and propagates.
- static format_error(error: Exception) str[source]¶
Return safe account recovery text for interactive applications.
- async generate_images(prompt: str, *, model: str | None = None, n: int = 1) list[bytes][source]¶
Generate with the selected image model, or the default GPT Image model.
- image_transport(model: str | ModelSpec = 'gpt-image-2') ChatGPTImageTransport[source]¶
Create a standalone generator sharing this account and HTTP session.
- property models: ModelRegistry¶
Visible server models followed by native image presets; empty until fetched.
- classmethod open_for_app(*, session: ClientSession, client_id: str | None = None, account: str | None = None, credentials: str | None = None) AbstractAsyncContextManager[ChatGPTTransport][source]¶
Open an account session for an application that owns transport lifetimes.
- static select_model(transport: ChatGPTTransport, model: ModelSpec) ChatGPTTransport[source]¶
Reset an incompatible reasoning setting when an application changes models.
- static supported_thinking_levels(model: ModelSpec | None) tuple[str, ...]¶
Use catalog levels when present; otherwise retain the generic fallback.
- with_account(account: ChatGPTSession, *, model: str | None = None) ChatGPTTransport[source]¶
Create a separately fetched catalog for another selected account.
- with_model(model: str | ModelSpec) ChatGPTTransport[source]¶
Select a visible catalog model while sharing the current account session.
- class axio_transport_openai.chatgpt_models.ChatGPTModel(id: str, capabilities: frozenset[Capability] = frozenset({}), max_output_tokens: int = 8192, context_window: int = 128000, input_cost: float = nan, output_cost: float = nan, display_name: str = '', visibility: str = '', metadata: Mapping[str, ~typing.Any]=<factory>)[source]¶
A server catalog entry with capabilities and limits from API metadata.
Subscription costs are unknown, represented as NaN rather than zero. Generic ModelSpec token budgets remain fallbacks when no model metadata is available. All catalog models support text and function tools. Missing metadata can use an explicitly supplied ModelSpec. The API-key registry is not consulted.
- class axio_transport_openai.ChatGPTImageModel(id: str, capabilities: frozenset[Capability] = frozenset({Capability.image_editing, Capability.image_generation}), max_output_tokens: int = 0, context_window: int = 0, input_cost: float = nan, output_cost: float = nan, display_name: str = '', visibility: str = 'list', metadata: Mapping[str, ~typing.Any]=<factory>)[source]¶
A native ChatGPT Images model, separate from completion catalog metadata.
Token budgets and token prices do not describe image generation. Availability is checked by the image endpoint for the selected account on each request.
- class axio_transport_openai.ChatGPTImageTransport(*, account: ChatGPTSession, session: ClientSession, model: str | ModelSpec = 'gpt-image-2', base_url: str = 'https://chatgpt.com/backend-api/codex')[source]¶
Generate image bytes using native ChatGPT OAuth, with no API-key fallback.
The caller owns both sessions. Models are supported image endpoint presets, not a promise of account quota. POST requests are never automatically replayed: a lost response, cancellation, or server error may hide a completed generation.
- async edit_images(prompt: str, *, images: list[ImageBlock], mask: ImageBlock | None = None, model: str | None = None, n: int = 1, size: Literal['auto', '1024x1024', '1536x1024', '1024x1536'] = 'auto', quality: Literal['auto', 'low', 'medium', 'high'] = 'auto', transparent_background: bool = False) list[bytes][source]¶
Edit one or more PNG/JPEG/WebP sources using native ChatGPT Images.
The native Codex protocol does not expose masks. Passing one fails before authorization or HTTP; sources are never silently treated as a text prompt.
- async generate_images(prompt: str, *, model: str | None = None, n: int = 1, size: Literal['auto', '1024x1024', '1536x1024', '1024x1536'] = 'auto', quality: Literal['auto', 'low', 'medium', 'high'] = 'auto', transparent_background: bool = False) list[bytes][source]¶
Return complete images; use image_media_type() to identify their format.
- with_model(model: str | ModelSpec) ChatGPTImageTransport[source]¶
Select an image model while sharing account renewal and quota state.
- class axio_transport_openai.ChatGPTImageTools(transport: ChatGPTImageTransport, *, images: Mapping[str, ImageBlock] | None = None)[source]¶
Bind two model-facing tools to an image transport and a local image catalog.
The host registers attachments with add_image() and tells the agent their IDs. Tool results contain new image IDs and ImageBlocks, allowing subsequent edits without model-generated base64 or filesystem access. Create one instance per conversation. The host owns session lifetime and persists images if desired.
- add_image(image: ImageBlock, *, image_id: str | None = None) str[source]¶
Register an attachment and return its ID for the agent’s prompt.
- async edit_image(prompt: ~typing.Annotated[str, ~axio.field.FieldInfo(description=, default=MISSING, ge=None, le=None, strict=True)], images: list[str]) list[TextBlock | ImageBlock][source]¶
Edit existing images according to the requested changes. images must contain IDs registered by the application or returned by earlier image tool calls. Keep their order when referring to first/second images. Returns a new image and ID; source images remain available. Masks are unsupported.
- async generate_image(prompt: ~typing.Annotated[str, ~axio.field.FieldInfo(description=, default=MISSING, ge=None, le=None, strict=True)]) list[TextBlock | ImageBlock][source]¶
Create a new image from a description when the user requests generation. To change an existing image or use reference images, call edit_image instead. Returns an image and its ID, which can be used in later edits.
- property images: Mapping[str, ImageBlock]¶
Read-only catalog of attachments and generated results, keyed by ID.
- axio_transport_openai.chatgpt_images.image_media_type(data: bytes) ImageMediaType[source]¶
Identify a PNG, JPEG, or WebP result without relying on a file extension.
- axio_transport_openai.chatgpt.build_chatgpt_payload(messages: list[Message], tools: list[Tool[Any]], system: str, model: ModelSpec, *, reasoning_effort: str | None = None, reasoning_summary: str = 'auto') dict[str, Any][source]¶
Declare current tools at input position zero, then replay complete history.
Keeping this position stable makes unchanged tool sets reusable across turns. Selector or schema changes replace this declaration in place; old calls remain in history, but removed tools are no longer declared. No tool-loading state is held in the transport, so forks, restarts, and concurrent contexts are isolated.
- class axio_transport_openai.session.ChatGPTSession(store: CredentialStore[CredentialRecord], oauth: ChatGPTOAuth, client_id: str, *, refresh_margin: float = 60, retry_delay: float = 5)[source]¶
Keep one issued client registration current for the lifetime of a run.
Use as an async context manager to enable background renewal. Without it, access_token() still renews on demand. close() stops background work without signing out. The caller owns the OAuth client’s aiohttp session and must keep it open until close() finishes. last_error reports background failures.
- async access_token() str[source]¶
Get a current plan token, reloading credentials under the process lock.
- property client_id: str¶
The registration pinned when this session was created.
- async request_headers(*, images: bool = False) dict[str, str][source]¶
Resolve token and workspace together from the pinned registration.
- resume_plan_usage() None[source]¶
Allow another plan request after the caller resolves a reported usage limit.
A quota response provides no reliable reset time. Resuming is explicit; it does not change accounts, credentials, or the billing path.
- async sign_out() SignOutResult[source]¶
Revoke the latest renewable session and retain its registration.
Temporary revocation errors get two retries. Local tokens are cleared even if the server cannot confirm revocation. A storage failure raises CredentialError; it never reports successful local sign-out.
- property supports_images: bool¶
Whether the last validated credentials use native ChatGPT authorization.
Realtime¶
- class axio_transport_openai.OpenAIRealtimeTransport(api_key: str = <factory>, base_url: str = <factory>, model: str = 'gpt-realtime', http_session: ClientSession | None = None, server_vad: bool = True, transcription_model: str | None = None, vad_threshold: float = 0.5, vad_prefix_padding_ms: int = 300, vad_silence_duration_ms: int = 500, vad_create_response: bool = True, vad_interrupt_response: bool = True, input_noise_reduction: str | None = 'near_field')[source]¶
RealtimeTransport for OpenAI’s Realtime WebSocket API.
Server-VAD knobs live here so callers can tune barge-in sensitivity without touching the transport internals. When AEC is imperfect or the room is loud, the defaults can cause the model’s own audio to trip
interrupt_responseand cancel its response. Bumpvad_thresholdhigher, lengthenvad_silence_duration_ms, or setvad_interrupt_response=Falseto require an explicitagent.interrupt().- input_noise_reduction: str | None¶
Server-side noise reduction profile applied to mic audio. Options:
"near_field"(close-talking mic / headset),"far_field"(laptop or conference mic), orNoneto disable.
- server_vad: bool¶
Set False for explicit utterance boundaries via commit().
- transcription_model: str | None¶
Opt in to user transcripts, e.g. gpt-4o-mini-transcribe.
- vad_create_response: bool¶
Whether the server auto-creates a response after each user turn.
- vad_interrupt_response: bool¶
Whether incoming user speech cancels the model’s in-flight response. Setting this to False makes interruption explicit (call
agent.interrupt()) — useful when AEC isn’t strong enough and the model’s own voice would otherwise loop back through the mic and interrupt itself.
- vad_prefix_padding_ms: int¶
Audio retained before a detected speech start.
- vad_silence_duration_ms: int¶
Silence required before the server considers a turn complete.
- vad_threshold: float¶
Server VAD activation threshold (0..1). Higher → less sensitive to quiet sounds and bleed-through. Default 0.5.
- class axio_transport_openai.OpenAIRealtimeSession(ws: Any, input_audio_media_type: str = 'audio/pcm;rate=24000', output_audio_media_type: str = 'audio/pcm;rate=24000', http_session: ClientSession | None = None, own_http_session: bool = False)[source]¶
RealtimeSession backed by an aiohttp WebSocket.
wsmay be any object that quacks likeaiohttp.ClientWebSocketResponse(send_str, async iteration yieldingaiohttp.WSMessage-like objects,close,closed). Tests inject a stub.- async events() AsyncIterator[StreamEvent][source]¶
Async iterator over server events for the lifetime of this session.
- async send(content: ContentBlock | list[ContentBlock]) None[source]¶
Append user content (audio chunk, text, image) to the input buffer.
- async send_tool_result(tool_use_id: ToolCallID, name: ToolName, content: str | list[ContentBlock]) None[source]¶
Deliver a tool’s result to the provider so generation can resume.
nameis included because some providers (e.g. Gemini Live) require the tool name alongside the call id. OpenAI realtime can ignore it.
Standalone API speech¶
These API-key transports are independent of completion and ChatGPT subscription credentials. See the package speech examples for billing boundaries, encoding and streaming cleanup requirements.
- class axio_transport_openai.OpenAISTTTransport(*, api_key: str = <factory>, base_url: str = <factory>, session: ClientSession | None = None, timeout_seconds: float = 120.0, model: str = 'gpt-4o-mini-transcribe', language: str | None = None, prompt: str | None = None)[source]¶
Transcribe an encoded audio file via
/audio/transcriptions(not raw PCM).
- class axio_transport_openai.OpenAITTSTransport(*, api_key: str = <factory>, base_url: str = <factory>, session: ClientSession | None = None, timeout_seconds: float = 120.0, model: str = 'gpt-4o-mini-tts', voice: str = 'coral', response_format: Literal['mp3', 'opus', 'aac', 'flac', 'wav', 'pcm']='mp3', speed: float = 1.0, instructions: str | None = None)[source]¶
Stream binary audio via
/audio/speech. No replay/retry of paid requests.PCM output is signed 16-bit little-endian mono at 24 kHz without a header. Network chunks are arbitrary byte boundaries, not PCM frames or standalone files. Close an abandoned generator with
contextlib.aclosingto release its response.