oc_chat
Loading...
Searching...
No Matches
client.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# providers/example/client.py
4
5"""
6ExampleClient — deterministic stub that satisfies :class:`BaseClient`.
7
8Purpose
9-------
10SDK-free and network-free. ``stream_chat`` and ``one_shot`` build a
11fake response from the last user message ("echo: <text>"). Useful as
12a template for new provider clients and as a test harness for code
13that needs a working :class:`BaseClient` without hitting a real API.
14``oc_diagnostics`` is the only external import — it's first-party
15Option C tooling, not a foreign SDK.
16
17When porting this file to a real provider:
18
19 1. Replace the constructor body with the SDK client construction
20 (e.g. ``self._sdk = OpenAI(api_key=..., base_url=...)``).
21 2. Replace ``stream_chat`` with a real streaming loop that yields
22 ``("reasoning", str)``, ``("content", str)``,
23 ``("tool_call", dict)``, and ``("usage", dict)`` chunks as the
24 SDK produces them. See ``providers/deepseek/client.py`` for the
25 canonical shape.
26 3. Replace ``one_shot`` with a non-streaming completion call.
27 4. Replace ``list_models`` with a real API call returning model ids.
28
29Responsibilities
30----------------
31- :class:`ExampleClient` — :class:`BaseClient` stub with no I/O.
32- :func:`_last_user_text` — pluck the most recent user message
33 content from the messages list.
34- Deterministic fake usage block (constant token counts) so tests
35 can assert on it.
36
37Diagnostics
38-----------
39Domain: OC_CHAT.CLIENT
40Levels:
41 L2 — lifecycle events
42 L3 — semantic details
43 L4 — deep tracing
44
45Contracts
46---------
47- ``stream_chat`` always yields exactly one ``("content", str)``
48 chunk and one ``("usage", dict)`` chunk — no reasoning, no tool
49 calls.
50- ``one_shot`` returns a plain ``str`` — never ``None``.
51- ``list_models`` returns the static :data:`MODELS` keys.
52- No exceptions are raised under normal use; the stub is meant to
53 be reliable in tests.
54
55Dependencies
56------------
57- oc_diagnostics (structured debug logging via _dbg)
58- providers.example.models (MODELS registry whose keys list_models returns)
59- option_c_agent_contracts (lazy; SupportsCancellation type of the stream_chat cancellation token)
60"""
61from __future__ import annotations
62
63from typing import Any, Iterator, TYPE_CHECKING
64
65from oc_diagnostics import _dbg
66
67if TYPE_CHECKING:
68 from option_c_agent_contracts import SupportsCancellation
69
70from .models import MODELS
71
72
73def _last_user_text(messages: list[dict]) -> str:
74 """Return the most recent user message's content, or empty string."""
75 for msg in reversed(messages):
76 if msg.get("role") == "user":
77 content = msg.get("content")
78 if isinstance(content, str):
79 return content
80 return ""
81
82
84 """Stub :class:`BaseClient` for the example provider.
85
86 Stores the constructor arguments so tests can assert on them, but
87 does nothing with them otherwise.
88 """
89
90 # OCT-LINT: disable=no_hardcoded_secrets reason="Empty-string default for optional api_key param, not a credential"
91 def __init__(self, api_key: str = "", base_url: str = "") -> None:
92 func = "__init__"
93 self.api_key = api_key
94 self.base_url = base_url
95 _dbg("OC_CHAT.CLIENT", func, f"ExampleClient base_url={base_url!r}", 4)
96
97 # ── Streaming chat ──────────────────────────────────────────────────────
98
100 self,
101 messages: list[dict],
102 model: str,
103 *,
104 effort: str | None = None,
105 extra_body: dict | None = None,
106 tools: list[dict] | None = None,
107 max_tokens: int | None = None,
108 cancellation: "SupportsCancellation | None" = None,
109 ) -> Iterator[tuple[str, Any]]:
110 """Yield a fixed sequence: one ``content`` chunk, then ``usage``.
111
112 Real providers stream multiple ``content`` chunks plus optional
113 ``reasoning`` and ``tool_call`` chunks before the final
114 ``usage`` chunk. See ``providers/deepseek/client.py``.
115
116 If *cancellation* is provided, the token is polled before each
117 yield; a cancelled token raises :class:`CancellationError` and
118 no further chunks are produced (Phase O3c).
119 """
120 text = f"echo: {_last_user_text(messages)}"
121 if cancellation is not None:
122 cancellation.raise_if_cancelled()
123 yield ("content", text)
124 if cancellation is not None:
125 cancellation.raise_if_cancelled()
126 yield ("usage", {
127 "prompt_tokens": len(_last_user_text(messages)),
128 "completion_tokens": len(text),
129 "total_tokens": len(_last_user_text(messages)) + len(text),
130 })
131
132 # ── One-shot (non-streaming) ────────────────────────────────────────────
133
135 self,
136 messages: list[dict],
137 model: str,
138 *,
139 effort: str | None = None,
140 extra_body: dict | None = None,
141 max_tokens: int | None = None,
142 ) -> str:
143 """Return ``"echo: <last user msg>"`` synchronously."""
144 return f"echo: {_last_user_text(messages)}"
145
146 # ── Model listing ───────────────────────────────────────────────────────
147
148 def list_models(self) -> list[str]:
149 """Return the curated example model ids (no network call)."""
150 return list(MODELS.keys())
str one_shot(self, list[dict] messages, str model, *, str|None effort=None, dict|None extra_body=None, int|None max_tokens=None)
Definition client.py:142
Iterator[tuple[str, Any]] stream_chat(self, list[dict] messages, str model, *, str|None effort=None, dict|None extra_body=None, list[dict]|None tools=None, int|None max_tokens=None, "SupportsCancellation | None" cancellation=None)
Definition client.py:109
None __init__(self, str api_key="", str base_url="")
Definition client.py:91
str _last_user_text(list[dict] messages)
Definition client.py:73