6DeepSeek provider implementation.
10Concrete :class:`oc_chat.core.provider_plugin.ProviderPlugin` for the
11DeepSeek API. Encapsulates everything provider-specific: client
12construction, the model/tier registry, the ``extra_body`` shape for
13thinking, the prefix-cache sentinel embedding, and prompt-cache
16Importing the parent package (``import providers.deepseek``) triggers
17self-registration via ``DeepSeekProvider.register()``. The rest of
18the app retrieves it via
19``oc_chat.core.provider_plugin.registry.get("deepseek")``.
23- :class:`DeepSeekProvider` — implement every abstract method on
24 :class:`ProviderPlugin` for DeepSeek.
25- :meth:`build_client` — construct a :class:`DeepSeekClient` with
26 the supplied API key and base URL.
27- :meth:`build_extra_body` — produce the
28 ``{"thinking": {"type": "enabled"}}`` shape when the target model
30- :meth:`transform_outgoing_messages` — deprecated no-op since plan 135
31 (the dump is gone; ``render_request`` is the seam now).
32- :meth:`parse_usage` — convert raw usage dicts to :class:`UsageStats`
33 via the shared cache-hit/miss helper.
34- :meth:`tier` / :meth:`models` — expose the static registries.
35- :meth:`fetch_live_models` — list live models, dropping retired ids.
36- :meth:`migrate_model_id` — map a retired alias to its V4 replacement
37 for saved-session migration (plan 135 §4.6).
49- ``transform_outgoing_messages`` returns its input unchanged (plan 135
50 neutered the dump; it must never re-embed).
51- ``build_extra_body`` returns ``None`` for models whose ``ModelCaps``
52 do not advertise ``supports_thinking``.
53- ``tier`` returns ``None`` (not a model id) when the requested tier
54 is not in :data:`TIER_MAP`.
55- ``migrate_model_id`` returns ``None`` for live ids; for a retired alias
56 it returns ``(replacement, notice)`` with a distinct per-alias target.
57- All abstract surface lives here; the bundle's other modules are
58 pure data / SDK plumbing.
62- oc_diagnostics (structured debug logging via _dbg)
63- option_c_agent_contracts (ProviderPlugin base class and the ModelCaps, UsageStats and BaseClient types)
64- providers.deepseek.client (DeepSeekClient returned by build_client)
65- providers.deepseek.cache_usage (prompt-cache hit and miss extraction for parse_usage)
66- providers.deepseek.models (MODELS, TIER_MAP and RETIRED_ALIASES registries)
68from __future__
import annotations
70from oc_diagnostics
import _dbg
72from option_c_agent_contracts
import (
79from .client
import DeepSeekClient
80from .cache_usage
import usage_prompt_cache_hit_miss
81from .models
import MODELS, TIER_MAP, RETIRED_ALIASES
86_DEFAULT_BASE_URL =
"https://api.deepseek.com"
94TOOL_SCHEMA_SENTINEL =
"<!--dc:embedded-tool-schemas-->"
100 api_key_env_var =
"DEEPSEEK_API_KEY"
101 base_url_env_var =
"DEEPSEEK_BASE_URL"
102 default_base_url = _DEFAULT_BASE_URL
105 default_model =
"deepseek-v4-pro"
110 func =
"build_client"
111 _dbg(
"OC_CHAT.CLIENT", func, f
"DeepSeekProvider base_url={base_url}", 2)
116 def models(self) -> dict[str, ModelCaps]:
122 return [m
for m
in client.list_models()
if m
not in RETIRED_ALIASES]
128 supports_thinking=
False,
129 supports_reasoning=
False,
131 max_context_tokens=65_536,
135 func =
"migrate_model_id"
136 replacement = RETIRED_ALIASES.get(model_id)
137 if replacement
is None:
140 tier_desc =
"reasoning tier" if replacement ==
"deepseek-v4-pro" else "cheap tier"
142 f
"[Model '{model_id}' is retired — migrated this session to "
143 f
"'{replacement}' ({tier_desc}).]"
145 _dbg(
"OC_CHAT.CLIENT", func, f
"{model_id} -> {replacement}", 2)
146 return replacement, notice
150 def tier(self, name: str) -> str |
None:
151 return TIER_MAP.get(name)
159 enable_thinking: bool,
162 """Return the ``extra_body`` payload that toggles thinking on
163 DeepSeek's thinking-capable models (e.g. deepseek-v4-pro).
165 Effort is *not* part of ``extra_body`` for DeepSeek — it's a
166 top-level ``reasoning_effort`` SDK kwarg, handled by the client
167 directly. This method only owns the thinking flag.
169 if not enable_thinking:
171 caps = MODELS.get(model)
172 if caps
is None or not caps.supports_thinking:
174 return {
"thinking": {
"type":
"enabled"}}
178 messages: list[dict],
179 tool_schemas: list[dict] |
None,
181 """Return *messages* unchanged (plan 135 §4.6 — neutered).
183 Deprecated: this hook used to embed the full tool-schema JSON dump into
184 ``messages[0]`` for DeepSeek's disk prefix cache. Plan 135 removed the
185 dump everywhere — ``session.messages[0]`` already carries the slim
186 policy + compact tool map (the full schema travels via the API ``tools``
187 parameter), and :meth:`render_request` is the provider seam now. This is
188 kept as a no-op for interface stability; it must never re-embed.
194 _warned_opts: set = set()
197 from option_c_agent_contracts
import GenerationOptions
198 func =
"translate_generation_options"
199 _SUPPORTED = {
"temperature",
"top_p",
"seed"}
201 for fname, val
in opts.model_dump(exclude_none=
True).items():
202 if fname
in _SUPPORTED:
206 _dbg(
"OC_CHAT.CLIENT", func,
207 f
"DeepSeek ignores option {fname!r}", 2)
208 return d
if d
else None
213 if not isinstance(usage, dict):
215 hit, miss = usage_prompt_cache_hit_miss(usage)
216 details = usage.get(
"completion_tokens_details")
or {}
218 prompt_tokens=int(usage.get(
"prompt_tokens")
or 0),
219 completion_tokens=int(usage.get(
"completion_tokens")
or 0),
220 total_tokens=int(usage.get(
"total_tokens")
or 0),
221 cache_hit_tokens=hit,
222 cache_miss_tokens=miss,
224 reasoning_tokens=int((details.get(
"reasoning_tokens")
if isinstance(details, dict)
else 0)
or 0),
tuple[str, str]|None migrate_model_id(self, str model_id)
dict|None translate_generation_options(self, "GenerationOptions" opts)
UsageStats parse_usage(self, dict usage)
ModelCaps default_caps(self)
list[dict] transform_outgoing_messages(self, list[dict] messages, list[dict]|None tool_schemas)
dict|None build_extra_body(self, str model, *, bool enable_thinking, str|None effort)
str|None tier(self, str name)
BaseClient build_client(self, str api_key, str base_url)
list[str] fetch_live_models(self, BaseClient client)