6OpenAICompatProvider — one class, one configured instance per lane.
10Concrete :class:`ProviderPlugin` for any OpenAI-compatible lane. Unlike
11the single-provider bundles, this class is NOT configured by class
12attributes: each instance is built from a :class:`Lane` catalogue row, so
13seven rows produce seven independently-named providers backed by one
14implementation (plan 215 P1). The registry keys on ``name`` and only
15rejects a DIFFERENT class re-using an existing name, so registering one
16class under seven names is exactly what its contract allows.
20- :class:`OpenAICompatProvider` — implement the abstract surface from a
22- :meth:`OpenAICompatProvider.register_lane` — build and register one
24- :func:`register_all` — validate the catalogue, then register every row.
25- :func:`registered_lanes` — the lane rows this bundle registered, in
26 catalogue declaration order.
38- Identity (``name`` / ``api_key_env_var`` / ``base_url_env_var`` /
39 ``default_base_url`` / ``default_model``) is set per INSTANCE from the
40 row, shadowing the ABC's class-level defaults. Two instances never
42- :meth:`build_extra_body` returns ``None`` always: no catalogue lane is
43 configured for reasoning (the OCON as-shipped condition), so there is
44 no provider-specific payload to add. A lane that later needs one adds
45 it here keyed on its own name — never a blanket default.
46- :meth:`transform_outgoing_messages` is a true pass-through: no lane has
47 a prefix-cache trick to serve.
48- :meth:`parse_usage` delegates to :func:`normalise_usage`, which is
49 total over malformed input.
50- :func:`register_all` is idempotent and validates FIRST: an invalid row
51 raises :exc:`CatalogueError` before ANY lane is registered, so a
52 half-registered bundle is unreachable (plan 215 P5).
53- Nothing here reads an API key. The harness resolves the key from the
54 env-var NAME this provider advertises.
58- oc_diagnostics (structured debug logging via _dbg)
59- option_c_agent_contracts (ProviderPlugin ABC, ModelCaps, UsageStats, registry)
60- option_c_agent_contracts.provider_plugin (OAuthCapability return type of the oauth property)
61- providers.openai_compat.catalogue (LANES, Lane rows and validate_catalogue for register_all)
62- providers.openai_compat.client (OpenAICompatClient returned by build_client)
63- providers.openai_compat.usage (normalise_usage behind parse_usage)
64- providers.openai_compat.oauth (lazy; OpenRouterOAuth for the openrouter lane's oauth property)
66from __future__
import annotations
68from typing
import Sequence
70from oc_diagnostics
import _dbg
72from option_c_agent_contracts
import (
79from option_c_agent_contracts.provider_plugin
import OAuthCapability
81from .catalogue
import LANES, Lane, validate_catalogue
82from .client
import OpenAICompatClient
83from .usage
import normalise_usage
89_REGISTERED: list[Lane] = []
93 """A :class:`ProviderPlugin` configured from one :class:`Lane` row."""
106 _dbg(
"OC_CHAT.CLIENT", func,
107 f
"lane={lane.name} models={len(self._models)} free={lane.free} "
108 f
"verification={lane.verification}", 3)
111 def oauth(self) -> OAuthCapability | None:
112 """Expose OAuth only for the OpenRouter lane; all other rows opt out."""
113 if self.
name !=
'openrouter':
115 from .oauth
import OpenRouterOAuth
116 return OpenRouterOAuth()
121 func =
"build_client"
122 _dbg(
"OC_CHAT.CLIENT", func, f
"lane={self.name} base_url={base_url}", 2)
126 extra_headers=self.
lane.extra_headers,
132 def models(self) -> dict[str, ModelCaps]:
136 return client.list_models()
139 """Caps for a live-discovered model absent from the curated set.
141 Deliberately the same honest shape the catalogue uses: tools
142 attemptable, nothing claimed about thinking/reasoning, and the
143 contract's conservative context default rather than a guess.
146 supports_thinking=
False,
147 supports_reasoning=
False,
149 max_context_tokens=65_536,
150 tool_schema_dialect=
"openai_chat",
155 def tier(self, name: str) -> str |
None:
156 return self.
lane.tiers.get(name)
164 enable_thinking: bool,
167 """Return ``None`` — no catalogue lane carries a thinking payload.
169 DeepSeek returns ``{"thinking": {"type": "enabled"}}`` here. Every
170 lane in this bundle runs as-shipped: no ``ModelCaps`` in the
171 catalogue advertises ``supports_thinking``, so honouring
172 *enable_thinking* would mean inventing a payload shape that was
173 never proven against the endpoint.
179 messages: list[dict],
180 tool_schemas: list[dict] |
None,
182 """Return *messages* unchanged — no lane has a prefix-cache trick."""
188 return normalise_usage(usage)
194 """Build and register one provider instance for *lane*."""
195 from option_c_agent_contracts
import registry
198 registry.register(instance)
202def register_all(lanes: Sequence[Lane] = LANES) -> list[
"OpenAICompatProvider"]:
203 """Validate *lanes*, then register one provider per row.
205 Validation runs to completion BEFORE the first registration, so an
206 invalid row aborts loudly with its name and leaves the registry
207 untouched (plan 215 P5/W1).
209 func =
"register_all"
210 validated = validate_catalogue(lanes)
211 instances = [OpenAICompatProvider.register_lane(lane)
for lane
in validated]
213 _REGISTERED.extend(validated)
214 _dbg(
"OC_CHAT.CLIENT", func,
215 f
"registered {len(instances)} lane(s): "
216 f
"{', '.join(i.name for i in instances)}", 2)
221 """The lane rows this bundle registered, in catalogue declaration order."""
222 return tuple(_REGISTERED)
226 "OpenAICompatProvider",
dict[str, ModelCaps] models(self)
dict|None build_extra_body(self, str model, *, bool enable_thinking, str|None effort)
list[str] fetch_live_models(self, BaseClient client)
str|None tier(self, str name)
ModelCaps default_caps(self)
"OpenAICompatProvider" register_lane(cls, Lane lane)
list[dict] transform_outgoing_messages(self, list[dict] messages, list[dict]|None tool_schemas)
UsageStats parse_usage(self, dict usage)
BaseClient build_client(self, str api_key, str base_url)
None __init__(self, Lane lane)
tuple[Lane,...] registered_lanes()
list["OpenAICompatProvider"] register_all(Sequence[Lane] lanes=LANES)