Choose a transport in application code

Use axio.transport.discover_transports() to select an installed transport by name. This API belongs to core and works in scripts, services, and custom agent harnesses. It does not require REPL or TUI.

Install core and the providers your application supports in the same Python environment. For example:

uv add axio aiohttp axio-transport-openai axio-transport-anthropic

Core supplies discovery and the adapter. Provider packages supply the transport implementations and register them in the axio.transport entry-point group. Import these helpers from axio.transport; they are not top-level axio exports. You can still construct a provider class directly when your application only needs that provider.

Discover providers and select one

from axio.transport import discover_transports

catalog = discover_transports()
for name, entry in catalog.entries.items():
    print(name, entry.display_name)
for name, error in catalog.errors.items():
    print(f"Cannot load {name}: {error}")

catalog.entries contains successfully loaded adapters. catalog.errors contains failures to load or adapt installed entry points. catalog.sources identifies each entry’s distribution and target. Discovery loads plugin code; it does not open a transport, fetch models, or validate credentials.

Select a provider explicitly with catalog.require(name). The name is the entry point ID, such as openai, chatgpt, anthropic, or google. It is separate from the model ID. Third-party providers use the same API.

require() raises TransportError when a provider is absent or failed to load. An empty environment produces an empty catalog. Duplicate names use the first declaration, including when it fails; later declarations produce a warning. Discovery does not select a default provider or switch to another provider after an error.

Open a transport and choose a model

TransportEntry.open() is an async context manager. For a class entry, it constructs the transport and calls fetch_models() when available. For a managed plugin, its factory supplies the ready transport and model catalog. You do not need a second fetch_models() call after opening it.

The following functions use only core interfaces and the HTTP client. Provider names, model IDs, and settings can come from your application’s configuration.

from __future__ import annotations

from typing import Any

import aiohttp

from axio import Agent, MemoryContextStore
from axio.transport import discover_transports


async def list_models(provider: str, **settings: Any) -> list[str]:
    entry = discover_transports().require(provider)
    async with aiohttp.ClientSession() as http:
        async with entry.open(session=http, **settings) as transport:
            return list(transport.models.keys())


async def ask(
    provider: str,
    model_id: str,
    prompt: str,
    **settings: Any,
) -> str:
    entry = discover_transports().require(provider)
    async with aiohttp.ClientSession() as http:
        async with entry.open(session=http, **settings) as transport:
            model = transport.models.get(model_id)
            if model is None:
                raise ValueError(f"Model {model_id!r} is unavailable for {provider!r}")
            selected = entry.select_model(transport, model)
            agent = Agent(system="Answer clearly.", transport=selected)
            return await agent.run(prompt, MemoryContextStore())

For an API-key provider, first list the models available through that transport:

import asyncio
import os

print(asyncio.run(list_models("openai", api_key=os.environ["OPENAI_API_KEY"])))

Set AXIO_MODEL to one of the returned IDs, then run a request:

print(
    asyncio.run(
        ask(
            "openai",
            os.environ["AXIO_MODEL"],
            "Explain what an async context manager owns.",
            api_key=os.environ["OPENAI_API_KEY"],
        )
    )
)

These two calls use the functions above and make real provider requests. AXIO_MODEL is an application setting in this example; core does not read it. The example rejects a missing model instead of silently selecting another one. Catalog contents and fallback behavior belong to the provider, so opening a transport does not guarantee that every listed model is accessible to an account.

Use entry.select_model(transport, model) to preserve the provider’s cloning rules. It first uses the plugin’s select_model hook. Without that hook, it tries the transport’s select_model, then with_model, and otherwise makes a shallow copy with the new model. Provider hooks can reset settings that are incompatible with the selected model. If you already have a transport without an entry, core also provides select_transport_model().

Keep all requests and model copies inside the entry.open() context. Your code owns the HTTP session; factory-managed account sessions close before that HTTP session closes. A model copy shares these resources and must not outlive them. For a long-running service, open them at service startup and close them after active agent runs finish. Supply a separate context store for each conversation.

Configure authentication and provider options

An API key identifies a provider account for one transport. An OAuth integration also needs saved registration data and rotating tokens. The transport or its account session handles authorization and refresh; the credential store retains that state across process restarts. Conversation history belongs in a separate ContextStore.

Pass constructor or factory settings through entry.open(). Their names and accepted values belong to the provider. For example, an API-key transport can accept api_key and base_url. The adapter passes supplied values through; it does not translate environment-variable names into constructor arguments.

entry.has_credentials() checks the provider’s local configuration or declared environment variables. It does not authenticate a request. Its optional mapping uses provider-defined keys; the default implementation checks the names in entry.env_vars. This check does not configure a later open() call.

For account providers, entry.auth exposes the optional TransportAuth API. Your application can call sign_in(credentials, account) to authorize an account and select_account(credentials, account) to save its selection. The former returns the account ID. These are explicit user actions; discovery does not start a browser login. Pass a credential-file Path or None to these methods.

After signing in with a provider’s account command, the same ask() function can open the saved account. For example, after axio-chatgpt login:

print(asyncio.run(list_models("chatgpt")))
print(asyncio.run(ask("chatgpt", os.environ["AXIO_MODEL"], "Hello")))

Use the common account and credentials options to select a specific account registration and credential file for open(). Providers without an account factory reject these options. See axio-transport-openai and Google Transport for provider-specific sign-in and account behavior.

The supports_temperature, supports_max_tokens, supports_debug, and supports_thinking fields describe application controls. open() does not validate options against these flags. Check them before exposing generic controls, and follow the provider’s configuration API for the actual settings. The auto_select flag is also metadata: core does not implement the REPL’s automatic provider-selection policy.

Opening, model loading, and inference can raise provider-specific exceptions. TransportError only covers core selection and adapter errors. Preserve the original exception for recovery logic; entry.format_error(error) provides a provider-defined display message, or str(error) when no formatter is supplied.

Keep account credentials across restarts

For a supported provider, use its account commands and account/session helpers. They already know its credential record format and token refresh rules. For example, axio-chatgpt and axio-antigravity manage their own account stores; MCP HTTP OAuth has a separate store. A credential store does not sign in, refresh tokens, or determine account entitlement by itself.

When implementing your own account integration, axio.credentials supplies typed file and SQLite stores. The record is a dataclass with exactly one string field marked credential_key. Hold the account lock while reading and updating its credentials so concurrent processes cannot refresh the same account independently:

import asyncio
from dataclasses import dataclass, field
from pathlib import Path
from tempfile import TemporaryDirectory

from axio.credentials import FileCredentialStore


@dataclass(frozen=True, slots=True)
class ServiceAccount:
    name: str = field(metadata={"credential_key": True})
    token: str = field(repr=False)


async def main() -> None:
    with TemporaryDirectory() as directory:
        store = FileCredentialStore[ServiceAccount](Path(directory) / "service.auth")
        async with store.lock("work"):
            await store.save(ServiceAccount("work", "example-token"))

        async with store.lock("work"):
            account = await store.load("work")
            assert account is not None
            # Perform provider-specific refresh here while holding the lock.
            await store.save(ServiceAccount(account.name, "updated-example-token"))


asyncio.run(main())

The example stores only fixture tokens. In a real integration, keep refresh and every resulting write inside the same lock. Each save commits independently; an exception later in the block does not roll it back. The backend validates record types and fields. Subclass a store and set filename to use its default $XDG_CONFIG_HOME/axio/ location (or ~/.config/axio/).

SQLiteCredentialStore offers the same record operations. The local backends use POSIX permissions and locks; stored records are not encrypted. Keep the adjacent lock directory while processes use it. list_records() returns full records, so remove secrets before displaying account lists or diagnostics. Marking a field repr=False only hides it in the dataclass representation.

Use the CredentialStore protocol for another persistence backend. It must provide exclusive account locks and atomic durable saves. See the credentials section of axio for serialization details and extension methods.

Discover other plugins through core

axio.plugins also provides discover_tools(), discover_tool_providers(), discover_guards(), discover_selectors(), and discover_context_backends(). These functions return catalogs with entries, load errors, and sources. Your application initializes the selected components and owns their lifetimes; discovery alone does not attach tools or providers to an Agent.

TUI settings screens remain a separate UI integration. They are not needed to discover or use a transport from core. See axio for the catalogs and lifecycle helpers, Register and discover plugins for all entry-point registrations, and Writing Transports to implement your own transport.