oc_chat
Loading...
Searching...
No Matches
provider_plugin.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/core/provider_plugin.py
4
5"""
6Provider plugin abstraction — harness re-export + harness-coupled glue.
7
8Purpose
9-------
10Plan 154 relocated the provider-plugin *contract* (the ``ProviderPlugin``
11ABC, the typed shapes ``ModelCaps`` / ``UsageStats`` / ``PerformanceStats``
12/ ``ModelInfo`` / ``ProviderEndpoint``, the ``BaseClient`` Protocol, and the
13``ProviderRegistry`` + ``registry`` singleton) into the shared leaf
14``option_c_agent_contracts.provider_plugin`` so provider bundles depend only
15on that leaf and never import ``oc_chat`` (breaking the
16``providers -> oc_chat.core`` cycle named in plan 151 §8).
17
18This module re-exports that contract surface unchanged, so the ~15 harness
19call sites that do ``from oc_chat.core.provider_plugin import ...`` keep
20working byte-for-byte. It ALSO owns the two default behaviours that carried
21harness dependencies and therefore could not live in the pure leaf:
22:func:`render_request` (needs ``oc_chat.core.request_plan``) and
23:func:`endpoints` (needs ``oc_chat.core.endpoint_registry``). They are free
24functions here (no provider overrides either, verified at dispatch), with an
25override-delegation hook preserved for future providers.
26
27Responsibilities
28----------------
29- Re-export the provider-plugin contract from the leaf.
30- :func:`render_request` — render a :class:`ProviderRequestPlan` to the wire
31 shape (OpenAI-Chat-compatible default).
32- :func:`endpoints` — synthesise the provider's named base-URL targets.
33
34Diagnostics
35-----------
36Domain: OC_CHAT.MAIN
37Levels:
38 L2 — lifecycle events
39 L3 — semantic details
40 L4 — deep tracing
41
42Contracts
43---------
44- The re-exported names are identical objects to the leaf's — an
45 ``isinstance`` / registration against either import path is equivalent.
46- :func:`render_request` / :func:`endpoints` delegate to a provider-defined
47 method of the same name when one exists (future extension), else apply the
48 default.
49
50Dependencies
51------------
52- oc_diagnostics (structured debug logging via _dbg)
53- option_c_agent_contracts.provider_plugin (the relocated contract)
54- oc_chat.core.endpoint_registry (lazy; classify_lan_policy for the default endpoint)
55- oc_chat.core.request_plan (lazy; RenderedRequest and the chat composers for render_request)
56"""
57from __future__ import annotations
58
59from typing import TYPE_CHECKING
60
61from oc_diagnostics import _dbg
62
63from option_c_agent_contracts.provider_plugin import (
64 BaseClient,
65 ModelCaps,
66 ModelInfo,
67 PerformanceStats,
68 ProviderEndpoint,
69 ProviderPlugin,
70 ProviderRegistry,
71 SupportsCancellation,
72 UsageStats,
73 registry,
74)
75
76if TYPE_CHECKING:
77 from oc_chat.core.request_plan import ProviderRequestPlan, RenderedRequest
78
79_dbg("OC_CHAT.MAIN", "module", "loaded", 4)
80
81
82def render_request(provider: ProviderPlugin, plan: "ProviderRequestPlan") -> "RenderedRequest":
83 """Render a provider-neutral :class:`ProviderRequestPlan` to the wire shape.
84
85 Default = OpenAI-Chat-compatible (plan 135 §3.4): recompose the chat
86 ``messages`` list (``messages[0]`` reproduced byte-for-byte from
87 ``policy_prompt + tool_reference``), narrow the tool catalog to
88 ``allowed_tools``, and leave ``system`` as ``None`` (the system prompt
89 lives in ``messages[0]`` for chat providers; ``effort``/``extra_body``
90 keep their caller-side ownership). DeepSeek/ollama/example inherit this;
91 the future Anthropic bundle (plan 137) supplies its own ``render_request``
92 method, which this function delegates to when present.
93
94 Relocated out of the ``ProviderPlugin`` ABC by plan 154 because the pure
95 contract leaf must not import ``oc_chat.core.request_plan``.
96 """
97 override = getattr(type(provider), "render_request", None)
98 if callable(override):
99 return override(provider, plan)
100
101 from oc_chat.core.request_plan import (
102 RenderedRequest,
103 compose_chat_messages,
104 filtered_tool_catalog,
105 )
106
107 return RenderedRequest(
108 messages=compose_chat_messages(plan),
109 tools=filtered_tool_catalog(plan),
110 extra_body=None,
111 max_tokens=plan.output_max_tokens,
112 system=None,
113 )
114
115
116def endpoints(provider: ProviderPlugin) -> dict[str, ProviderEndpoint]:
117 """Return the named base-URL targets *provider* can serve (Phase O2).
118
119 Default implementation synthesises exactly one entry named ``"default"``
120 whose ``base_url`` is read from ``os.environ[provider.base_url_env_var]``
121 when set, else :attr:`ProviderPlugin.default_base_url`. LAN policy is
122 classified by ``oc_chat.core.endpoint_registry.classify_lan_policy``.
123
124 A provider can expose multiple curated endpoints by defining its own
125 ``endpoints`` method, which this function delegates to when present; the
126 registry layer (env var + ``~/.oc_chat/endpoints.toml``) merges on top.
127
128 Relocated out of the ``ProviderPlugin`` ABC by plan 154 because the pure
129 contract leaf must not import ``oc_chat.core.endpoint_registry``.
130 """
131 override = getattr(type(provider), "endpoints", None)
132 if callable(override):
133 return override(provider)
134
135 import os
136 from oc_chat.core.endpoint_registry import classify_lan_policy
137
138 if provider.base_url_env_var:
139 base_url = os.environ.get(provider.base_url_env_var, provider.default_base_url)
140 else:
141 base_url = provider.default_base_url
142 if not base_url:
143 return {}
144 policy = classify_lan_policy(base_url)
145 return {"default": ProviderEndpoint(name="default", base_url=base_url, lan_policy_status=policy)}
146
147
148__all__ = [
149 "ModelCaps",
150 "UsageStats",
151 "PerformanceStats",
152 "ModelInfo",
153 "ProviderEndpoint",
154 "BaseClient",
155 "SupportsCancellation",
156 "ProviderPlugin",
157 "ProviderRegistry",
158 "registry",
159 "render_request",
160 "endpoints",
161]
dict[str, ProviderEndpoint] endpoints(ProviderPlugin provider)
"RenderedRequest" render_request(ProviderPlugin provider, "ProviderRequestPlan" plan)