oc_chat
Loading...
Searching...
No Matches
provider.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# providers/example/provider.py
4
5"""
6ExampleProvider — minimal :class:`ProviderPlugin` implementation.
7
8Purpose
9-------
10Every abstract method is implemented with the simplest body that
11satisfies the contract: no provider-specific quirks, no
12``extra_body`` shaping, no message transformation. When porting to a
13real provider, replace each method body in turn — the comments mark
14every DeepSeek-specific pattern that does *not* belong in a generic
15provider.
16
17Responsibilities
18----------------
19- :class:`ExampleProvider` — implement every abstract method on
20 :class:`ProviderPlugin` with the minimal body needed.
21- :meth:`build_client` — return an :class:`ExampleClient`.
22- :meth:`models` / :meth:`tier` — surface the static registries.
23- :meth:`build_extra_body` — always return ``None`` (no thinking
24 payload).
25- :meth:`transform_outgoing_messages` — pass-through (no schema
26 embedding).
27- :meth:`parse_usage` — defer to the shared
28 :func:`usage_prompt_cache_hit_miss` helper.
29
30Diagnostics
31-----------
32Domain: OC_CHAT.CLIENT
33Levels:
34 L2 — lifecycle events
35 L3 — semantic details
36 L4 — deep tracing
37
38Contracts
39---------
40- The example bundle stays dependency-free — no SDK imports, no
41 network code.
42- ``build_extra_body`` returns ``None`` regardless of model
43 capabilities (no thinking shape needed for stubs).
44- ``transform_outgoing_messages`` is a true pass-through (no
45 sentinel embedding) — the example provider exists to prove the
46 contract works without provider-specific transforms.
47- All abstract surface lives here; everything else in the bundle is
48 pure data.
49
50Dependencies
51------------
52- oc_diagnostics (structured debug logging via _dbg)
53- option_c_agent_contracts (ProviderPlugin base class and the ModelCaps, UsageStats and BaseClient types)
54- providers.example.client (ExampleClient returned by build_client)
55- providers.example.models (MODELS and TIER_MAP registries)
56"""
57from __future__ import annotations
58
59from oc_diagnostics import _dbg
60
61from option_c_agent_contracts import (
62 ProviderPlugin,
63 ModelCaps,
64 UsageStats,
65 BaseClient,
66)
67
68from .client import ExampleClient
69from .models import MODELS, TIER_MAP
70
71
72class ExampleProvider(ProviderPlugin):
73 name = "example"
74 # OCT-LINT: disable=no_hardcoded_secrets reason="Env var NAME to read the key from, not a credential"
75 api_key_env_var = "EXAMPLE_API_KEY"
76 base_url_env_var = "EXAMPLE_BASE_URL"
77 default_base_url = ""
78 default_model = "example-echo"
79
80 # ── Client construction ──────────────────────────────────────────────────
81
82 def build_client(self, api_key: str, base_url: str) -> BaseClient:
83 func = "build_client"
84 _dbg("OC_CHAT.CLIENT", func, f"ExampleProvider base_url={base_url!r}", 4)
85 return ExampleClient(api_key=api_key, base_url=base_url)
86
87 # ── Model registry ───────────────────────────────────────────────────────
88
89 def models(self) -> dict[str, ModelCaps]:
90 return MODELS
91
92 def fetch_live_models(self, client: BaseClient) -> list[str]:
93 # Real providers query the live API: ``return client.list_models()``.
94 # The example client returns the curated list deterministically.
95 return client.list_models()
96
97 def default_caps(self) -> ModelCaps:
98 # Permissive default for any model that shows up in fetch_live_models
99 # but is missing from MODELS. Tighten per-provider as appropriate.
100 return ModelCaps(
101 supports_thinking=False,
102 supports_reasoning=False,
103 supports_tools=True,
104 max_context_tokens=4096,
105 )
106
107 # ── Tier mapping ─────────────────────────────────────────────────────────
108
109 def tier(self, name: str) -> str | None:
110 return TIER_MAP.get(name)
111
112 # ── Per-call shaping ─────────────────────────────────────────────────────
113
115 self,
116 model: str,
117 *,
118 enable_thinking: bool,
119 effort: str | None,
120 ) -> dict | None:
121 """No provider-specific ``extra_body`` payload.
122
123 DeepSeek returns ``{"thinking": {"type": "enabled"}}`` here when
124 thinking is requested and the model supports it; other providers
125 with no equivalent concept return ``None``.
126 """
127 return None
128
130 self,
131 messages: list[dict],
132 tool_schemas: list[dict] | None,
133 ) -> list[dict]:
134 """No-op — return messages unchanged.
135
136 DeepSeek embeds JSON tool schemas into the system prompt for
137 prefix-cache stability and appends a sentinel comment. Providers
138 without prefix caching (or with server-side automatic caching)
139 return the messages unchanged.
140 """
141 return messages
142
143 # ── Response parsing ─────────────────────────────────────────────────────
144
145 def parse_usage(self, usage: dict) -> UsageStats:
146 if not isinstance(usage, dict):
147 return UsageStats()
148 return UsageStats(
149 prompt_tokens=int(usage.get("prompt_tokens") or 0),
150 completion_tokens=int(usage.get("completion_tokens") or 0),
151 total_tokens=int(usage.get("total_tokens") or 0),
152 # No prefix-cache accounting — leave cache fields zero.
153 )
UsageStats parse_usage(self, dict usage)
Definition provider.py:145
dict|None build_extra_body(self, str model, *, bool enable_thinking, str|None effort)
Definition provider.py:120
list[dict] transform_outgoing_messages(self, list[dict] messages, list[dict]|None tool_schemas)
Definition provider.py:133
BaseClient build_client(self, str api_key, str base_url)
Definition provider.py:82
list[str] fetch_live_models(self, BaseClient client)
Definition provider.py:92