A provider plugin ships a transport via `register_transport(api_mode, cls)` and
declares that same string as its profile's `api_mode`. Transports are selected by
that string, but every gate that validates an api_mode compared it against a
closed literal, so a plugin's mode was rejected at each one and rewritten to
`chat_completions`. The plugin's transport was then never selected: no error, no
tool call, the turn silently degraded to prose. `register_transport` was a public
seam with no way through.
Accept a mode when the transport registry knows it, via a new
`agent.transports.registered_api_modes()`, at each gate:
* `agent_init._resolve_api_mode` - the agent's mode ladder;
* `runtime_provider._parse_api_mode` - the config gate;
* `delegate_tool_config` - the delegation resolver;
* `providers.get_provider` - the reverse `TRANSPORT_TO_API_MODE` lookup recorded
an unknown mode as `openai_chat`, which made `determine_api_mode` report
`chat_completions` for a provider that has a dialect transport. This one is the
most deceptive: the other gates already pass, and the transport still is not used.
* `providers.determine_api_mode` - the same table lookup at the other end.
The registry read is deliberately lazy (`sys.modules.get("agent.transports")`,
never an import): this code is reached from `determine_api_mode`, which provider
discovery itself calls while the registry is being populated, and importing the
transport package there re-enters discovery.
Also add `ProviderTransport.normalize_stream_delta()`, the response-side twin of
`convert_messages()`: a provider that streams a tool call on the legacy OpenAI
`delta.function_call` pair instead of indexed `delta.tool_calls` had nowhere to
translate it, and the streaming assembler dropped the call. The default returns
the delta unchanged, so existing transports are untouched; the assembler now asks
the transport instead of hardcoding one provider's shape.
Finally, make plugin-provider registration repeatable. `hermes_cli.auth` registered
plugin profiles once, at import, from a list `hermes_cli.config` had already
partially discovered while importing itself. A profile that sorts LAST in discovery
was absent from that snapshot and never reached `PROVIDER_REGISTRY`, so every
consumer reported it unauthenticated and it silently vanished from the model
picker while working fine from the CLI. `ensure_plugin_providers_registered()` is
now called from `get_auth_status()` and `resolve_provider()`, so a late profile is
picked up instead of staying invisible.
Unregistered modes are still rejected everywhere, and the in-tree literal sets are
unchanged - they are simply no longer the only way in.
57 lines
2.0 KiB
Python
57 lines
2.0 KiB
Python
"""Transport registry for provider response normalization.
|
|
transport = get_transport("anthropic_messages")
|
|
result = transport.normalize_response(raw_response)"""
|
|
|
|
import contextlib
|
|
import importlib
|
|
|
|
from agent.transports.types import ( # noqa: F401
|
|
NormalizedResponse,
|
|
ToolCall,
|
|
Usage,
|
|
build_tool_call,
|
|
map_finish_reason,
|
|
)
|
|
|
|
_REGISTRY: dict = {}
|
|
_discovered: bool = False
|
|
_TRANSPORT_MODULES = ("anthropic", "codex", "chat_completions", "bedrock")
|
|
|
|
|
|
def register_transport(api_mode: str, transport_cls: type) -> None:
|
|
"""Register a transport class for an api_mode string."""
|
|
_REGISTRY[api_mode] = transport_cls
|
|
|
|
|
|
def get_transport(api_mode: str):
|
|
"""Return a transport instance for ``api_mode``, or None so callers can fall back to the legacy path."""
|
|
# A directly-imported transport leaves the registry partial; (re)discover on first use and on misses.
|
|
if not _discovered or api_mode not in _REGISTRY:
|
|
_discover_transports()
|
|
cls = _REGISTRY.get(api_mode)
|
|
return None if cls is None else cls()
|
|
|
|
|
|
def _discover_transports() -> None:
|
|
"""Import all transport modules to trigger auto-registration."""
|
|
global _discovered
|
|
_discovered = True
|
|
for name in _TRANSPORT_MODULES:
|
|
with contextlib.suppress(ImportError):
|
|
importlib.import_module(f"agent.transports.{name}")
|
|
|
|
|
|
def registered_api_modes() -> frozenset:
|
|
"""Every api_mode with a registered transport, in-tree and plugin-supplied.
|
|
|
|
``register_transport`` is the public seam for a provider plugin that speaks
|
|
its own dialect, but the api_mode gates elsewhere are closed literals — a
|
|
plugin's mode was rejected there and rewritten to ``chat_completions``, with
|
|
the dialect translation dropped and no error raised. Callers that validate an
|
|
api_mode use this so a registered transport is accepted on every path.
|
|
Discovers first, so a cold registry never reports an empty set.
|
|
"""
|
|
if not _discovered:
|
|
_discover_transports()
|
|
return frozenset(_REGISTRY)
|