crprotocol 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. crp/__init__.py +126 -0
  2. crp/__main__.py +8 -0
  3. crp/_typing.py +27 -0
  4. crp/_version.py +5 -0
  5. crp/adapters.py +31 -0
  6. crp/advanced/__init__.py +40 -0
  7. crp/advanced/auto_ingest.py +400 -0
  8. crp/advanced/cqs.py +235 -0
  9. crp/advanced/cross_window.py +477 -0
  10. crp/advanced/curator.py +265 -0
  11. crp/advanced/feedback.py +146 -0
  12. crp/advanced/hierarchical.py +211 -0
  13. crp/advanced/meta_learning.py +401 -0
  14. crp/advanced/parallel.py +98 -0
  15. crp/advanced/review_cycle.py +329 -0
  16. crp/advanced/scale_mode.py +129 -0
  17. crp/advanced/source_grounding.py +207 -0
  18. crp/ckf/__init__.py +35 -0
  19. crp/ckf/community.py +377 -0
  20. crp/ckf/fabric.py +445 -0
  21. crp/ckf/gc.py +175 -0
  22. crp/ckf/graph_walk.py +87 -0
  23. crp/ckf/merge.py +133 -0
  24. crp/ckf/pattern_query.py +122 -0
  25. crp/ckf/pubsub.py +128 -0
  26. crp/ckf/semantic.py +207 -0
  27. crp/cli/__init__.py +7 -0
  28. crp/cli/main.py +329 -0
  29. crp/cli/sidecar.py +929 -0
  30. crp/cli/startup.py +272 -0
  31. crp/continuation/__init__.py +103 -0
  32. crp/continuation/completion.py +348 -0
  33. crp/continuation/degradation.py +157 -0
  34. crp/continuation/document_map.py +160 -0
  35. crp/continuation/flow.py +109 -0
  36. crp/continuation/gap.py +419 -0
  37. crp/continuation/manager.py +484 -0
  38. crp/continuation/quality_monitor.py +179 -0
  39. crp/continuation/stitch.py +419 -0
  40. crp/continuation/trigger.py +142 -0
  41. crp/continuation/voice.py +157 -0
  42. crp/core/__init__.py +69 -0
  43. crp/core/batch.py +77 -0
  44. crp/core/circuit_breaker.py +116 -0
  45. crp/core/config.py +377 -0
  46. crp/core/context_tools.py +540 -0
  47. crp/core/dispatch_router.py +3977 -0
  48. crp/core/errors.py +128 -0
  49. crp/core/extraction_facade.py +384 -0
  50. crp/core/facilitator.py +713 -0
  51. crp/core/idempotency.py +215 -0
  52. crp/core/orchestrator.py +1435 -0
  53. crp/core/relay_strategies.py +613 -0
  54. crp/core/security_manager.py +140 -0
  55. crp/core/session.py +134 -0
  56. crp/core/task_intent.py +36 -0
  57. crp/core/window.py +363 -0
  58. crp/envelope/__init__.py +30 -0
  59. crp/envelope/builder.py +288 -0
  60. crp/envelope/decomposer.py +236 -0
  61. crp/envelope/formatter.py +168 -0
  62. crp/envelope/packer.py +211 -0
  63. crp/envelope/reranker.py +209 -0
  64. crp/envelope/scoring.py +310 -0
  65. crp/extraction/__init__.py +45 -0
  66. crp/extraction/complexity.py +96 -0
  67. crp/extraction/contradiction.py +132 -0
  68. crp/extraction/pipeline.py +360 -0
  69. crp/extraction/quality_gate.py +237 -0
  70. crp/extraction/stage1_regex.py +173 -0
  71. crp/extraction/stage2_statistical.py +244 -0
  72. crp/extraction/stage3_gliner.py +210 -0
  73. crp/extraction/stage4_uie.py +183 -0
  74. crp/extraction/stage5_discourse.py +175 -0
  75. crp/extraction/stage6_llm.py +178 -0
  76. crp/extraction/structured_output.py +219 -0
  77. crp/extraction/types.py +299 -0
  78. crp/license_guard.py +722 -0
  79. crp/observability/__init__.py +30 -0
  80. crp/observability/audit.py +118 -0
  81. crp/observability/events.py +233 -0
  82. crp/observability/metrics.py +264 -0
  83. crp/observability/quality.py +135 -0
  84. crp/observability/structured_logging.py +81 -0
  85. crp/observability/telemetry.py +117 -0
  86. crp/provenance/__init__.py +314 -0
  87. crp/provenance/_embeddings.py +97 -0
  88. crp/provenance/_types.py +378 -0
  89. crp/provenance/attribution_scorer.py +252 -0
  90. crp/provenance/claim_detector.py +229 -0
  91. crp/provenance/contradiction_detector.py +243 -0
  92. crp/provenance/distortion_detector.py +397 -0
  93. crp/provenance/entailment_verifier.py +358 -0
  94. crp/provenance/fabrication_detector.py +203 -0
  95. crp/provenance/hallucination_scorer.py +320 -0
  96. crp/provenance/omission_analyzer.py +106 -0
  97. crp/provenance/provenance_chain.py +205 -0
  98. crp/provenance/report_generator.py +440 -0
  99. crp/providers/__init__.py +43 -0
  100. crp/providers/anthropic.py +270 -0
  101. crp/providers/base.py +135 -0
  102. crp/providers/custom.py +63 -0
  103. crp/providers/diagnostic.py +251 -0
  104. crp/providers/llamacpp.py +224 -0
  105. crp/providers/manager.py +139 -0
  106. crp/providers/ollama.py +243 -0
  107. crp/providers/openai.py +628 -0
  108. crp/providers/tokenizers.py +48 -0
  109. crp/py.typed +0 -0
  110. crp/resources/__init__.py +53 -0
  111. crp/resources/adaptive_allocator.py +525 -0
  112. crp/resources/cost_model.py +388 -0
  113. crp/resources/overhead_manager.py +217 -0
  114. crp/resources/resource_manager.py +262 -0
  115. crp/schemas/__init__.py +20 -0
  116. crp/schemas/cost-estimate.json +33 -0
  117. crp/schemas/crp-error.json +43 -0
  118. crp/schemas/envelope-preview.json +40 -0
  119. crp/schemas/persisted-state-header.json +27 -0
  120. crp/schemas/quality-report.json +94 -0
  121. crp/schemas/session-handle.json +33 -0
  122. crp/schemas/session-status.json +57 -0
  123. crp/schemas/stream-event.json +18 -0
  124. crp/schemas/task-intent.json +42 -0
  125. crp/security/__init__.py +93 -0
  126. crp/security/audit_trail.py +392 -0
  127. crp/security/binding.py +192 -0
  128. crp/security/compliance.py +813 -0
  129. crp/security/consent.py +593 -0
  130. crp/security/embedding_defense.py +161 -0
  131. crp/security/encryption.py +202 -0
  132. crp/security/injection.py +335 -0
  133. crp/security/integrity.py +267 -0
  134. crp/security/privacy.py +662 -0
  135. crp/security/quarantine.py +249 -0
  136. crp/security/rbac.py +221 -0
  137. crp/security/validation.py +164 -0
  138. crp/state/__init__.py +31 -0
  139. crp/state/cold_storage.py +258 -0
  140. crp/state/compaction.py +263 -0
  141. crp/state/critical_state.py +104 -0
  142. crp/state/event_log.py +313 -0
  143. crp/state/fact.py +189 -0
  144. crp/state/serialization.py +189 -0
  145. crp/state/session_cleanup.py +77 -0
  146. crp/state/snapshot.py +290 -0
  147. crp/state/warm_store.py +346 -0
  148. crprotocol-2.0.0.dist-info/METADATA +1295 -0
  149. crprotocol-2.0.0.dist-info/RECORD +153 -0
  150. crprotocol-2.0.0.dist-info/WHEEL +4 -0
  151. crprotocol-2.0.0.dist-info/entry_points.txt +2 -0
  152. crprotocol-2.0.0.dist-info/licenses/LICENSE.md +170 -0
  153. crprotocol-2.0.0.dist-info/licenses/NOTICE +18 -0
@@ -0,0 +1,270 @@
1
+ # Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
2
+ # Licensed under Elastic License 2.0 — see LICENSE.md for details.
3
+ """Anthropic adapter — Claude 3/3.5/4 families (§6.1).
4
+
5
+ Requires ``anthropic>=0.25`` (``pip install crprotocol[full]``).
6
+
7
+ Usage::
8
+
9
+ from crp.providers.anthropic import AnthropicAdapter
10
+
11
+ provider = AnthropicAdapter(model="claude-sonnet-4-20250514")
12
+ output, reason = provider.generate_chat([
13
+ {"role": "system", "content": "You are helpful."},
14
+ {"role": "user", "content": "Hello!"},
15
+ ])
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ import os
22
+ import random
23
+ import time
24
+ from typing import Any
25
+
26
+ from crp.providers.base import LLMProvider
27
+
28
+ logger = logging.getLogger("crp.providers.anthropic")
29
+
30
+ # Model → context window (tokens). Updated as of 2025-Q2.
31
+ _MODEL_CONTEXT: dict[str, int] = {
32
+ "claude-opus-4-20250514": 200_000,
33
+ "claude-sonnet-4-20250514": 200_000,
34
+ "claude-3-7-sonnet-20250219": 200_000,
35
+ "claude-3-5-sonnet-20241022": 200_000,
36
+ "claude-3-5-sonnet-20240620": 200_000,
37
+ "claude-3-5-haiku-20241022": 200_000,
38
+ "claude-3-opus-20240229": 200_000,
39
+ "claude-3-sonnet-20240229": 200_000,
40
+ "claude-3-haiku-20240307": 200_000,
41
+ }
42
+
43
+ # Model → max output tokens.
44
+ _MODEL_MAX_OUTPUT: dict[str, int] = {
45
+ "claude-opus-4-20250514": 32_000,
46
+ "claude-sonnet-4-20250514": 16_000,
47
+ "claude-3-7-sonnet-20250219": 16_000,
48
+ "claude-3-5-sonnet-20241022": 8_192,
49
+ "claude-3-5-sonnet-20240620": 8_192,
50
+ "claude-3-5-haiku-20241022": 8_192,
51
+ "claude-3-opus-20240229": 4_096,
52
+ "claude-3-sonnet-20240229": 4_096,
53
+ "claude-3-haiku-20240307": 4_096,
54
+ }
55
+
56
+
57
+ def _require_anthropic():
58
+ """Import anthropic with a friendly error."""
59
+ try:
60
+ import anthropic
61
+ return anthropic
62
+ except ImportError:
63
+ raise ImportError(
64
+ "Anthropic adapter requires the 'anthropic' package. "
65
+ "Install with: pip install crprotocol[full]"
66
+ ) from None
67
+
68
+
69
+ class AnthropicAdapter(LLMProvider):
70
+ """Anthropic Claude chat adapter.
71
+
72
+ Args:
73
+ model: Model name (e.g. "claude-sonnet-4-20250514").
74
+ api_key: API key. Defaults to ``ANTHROPIC_API_KEY`` env var.
75
+ max_tokens: Max output tokens per request (default: model limit).
76
+ timeout: HTTP timeout in seconds (default: 120).
77
+ """
78
+
79
+ def __init__(
80
+ self,
81
+ model: str = "claude-sonnet-4-20250514",
82
+ *,
83
+ api_key: str | None = None,
84
+ max_tokens: int | None = None,
85
+ timeout: float = 120.0,
86
+ ) -> None:
87
+ anthropic = _require_anthropic()
88
+
89
+ self._model = model
90
+ self._max_tokens = max_tokens or _MODEL_MAX_OUTPUT.get(model, 4_096)
91
+ self._context_size = _MODEL_CONTEXT.get(model, 200_000)
92
+
93
+ key = api_key or os.environ.get("ANTHROPIC_API_KEY")
94
+ if not key:
95
+ raise ValueError(
96
+ "No API key provided. Pass api_key= or set ANTHROPIC_API_KEY."
97
+ )
98
+ self._client = anthropic.Anthropic(
99
+ api_key=key,
100
+ timeout=timeout,
101
+ )
102
+
103
+ logger.debug(
104
+ "AnthropicAdapter initialized: model=%s, ctx=%d",
105
+ model, self._context_size,
106
+ )
107
+
108
+ # -- LLMProvider interface --------------------------------------------
109
+
110
+ # Retry config: 3 attempts, exponential backoff with jitter
111
+ _MAX_RETRIES = 3
112
+ _BASE_DELAY = 1.0 # seconds
113
+
114
+ @staticmethod
115
+ def _is_retryable(exc: Exception) -> bool:
116
+ """Check if an exception is transient and worth retrying."""
117
+ exc_type = type(exc).__name__
118
+ if hasattr(exc, "status_code"):
119
+ return exc.status_code in (429, 500, 502, 503, 529)
120
+ if exc_type in ("RateLimitError", "APIConnectionError", "APITimeoutError",
121
+ "OverloadedError", "InternalServerError"):
122
+ return True
123
+ if isinstance(exc, (ConnectionError, TimeoutError)):
124
+ return True
125
+ return False
126
+
127
+ def generate_chat(
128
+ self, messages: list[dict[str, str]], **kwargs: Any
129
+ ) -> tuple[str, str]:
130
+ """Call Anthropic Messages API with retry on transient failures.
131
+
132
+ Anthropic uses a separate ``system`` parameter instead of a system
133
+ message in the array, so we extract it automatically.
134
+
135
+ Returns (output_text, finish_reason).
136
+ """
137
+ # Extract system message (Anthropic API uses separate param)
138
+ system_text = ""
139
+ chat_messages = []
140
+ for msg in messages:
141
+ if msg.get("role") == "system":
142
+ system_text = msg.get("content", "")
143
+ else:
144
+ chat_messages.append(msg)
145
+
146
+ # Ensure at least one user message
147
+ if not chat_messages:
148
+ chat_messages = [{"role": "user", "content": ""}]
149
+
150
+ params: dict[str, Any] = {
151
+ "model": self._model,
152
+ "messages": chat_messages,
153
+ "max_tokens": kwargs.pop("max_tokens", self._max_tokens),
154
+ }
155
+ if system_text:
156
+ params["system"] = system_text
157
+
158
+ last_exc: Exception | None = None
159
+ for attempt in range(self._MAX_RETRIES):
160
+ try:
161
+ response = self._client.messages.create(**params)
162
+
163
+ # Extract text from content blocks
164
+ text_parts = []
165
+ for block in response.content:
166
+ if hasattr(block, "text"):
167
+ text_parts.append(block.text)
168
+ text = "".join(text_parts)
169
+
170
+ # Map Anthropic stop reasons to CRP convention
171
+ stop_reason = response.stop_reason or "end_turn"
172
+ if stop_reason == "max_tokens":
173
+ finish_reason = "length"
174
+ elif stop_reason in ("end_turn", "stop_sequence"):
175
+ finish_reason = "stop"
176
+ else:
177
+ finish_reason = "stop"
178
+
179
+ return (text, finish_reason)
180
+ except Exception as exc:
181
+ last_exc = exc
182
+ if attempt < self._MAX_RETRIES - 1 and self._is_retryable(exc):
183
+ delay = self._BASE_DELAY * (2 ** attempt) + random.uniform(0, 0.5)
184
+ logger.warning(
185
+ "Anthropic transient error (attempt %d/%d), retrying in %.1fs: %s",
186
+ attempt + 1, self._MAX_RETRIES, delay, exc,
187
+ )
188
+ time.sleep(delay)
189
+ else:
190
+ logger.error("Anthropic API error: %s", exc)
191
+ return ("", "error")
192
+
193
+ logger.error("Anthropic API failed after %d retries: %s", self._MAX_RETRIES, last_exc)
194
+ return ("", "error")
195
+
196
+ def count_tokens(self, text: str) -> int:
197
+ """Count tokens using Anthropic's tokenizer if available.
198
+
199
+ Falls back to ~3.5 chars/token heuristic (Claude uses a modified
200
+ BPE tokenizer similar to GPT-4's).
201
+ """
202
+ try:
203
+ return self._client.count_tokens(text)
204
+ except Exception:
205
+ return max(1, len(text) // 4)
206
+
207
+ def context_window_size(self) -> int:
208
+ return self._context_size
209
+
210
+ @property
211
+ def max_output_tokens(self) -> int | None:
212
+ return self._max_tokens
213
+
214
+ @property
215
+ def model_name(self) -> str:
216
+ return self._model
217
+
218
+ def cost_per_1k_tokens(self) -> tuple[float, float]:
219
+ """Anthropic pricing per 1K tokens (USD) — updated 2025-Q2."""
220
+ pricing = {
221
+ "claude-opus-4-20250514": (0.015, 0.075),
222
+ "claude-sonnet-4-20250514": (0.003, 0.015),
223
+ "claude-3-5-sonnet-20241022": (0.003, 0.015),
224
+ "claude-3-5-haiku-20241022": (0.0008, 0.004),
225
+ "claude-3-opus-20240229": (0.015, 0.075),
226
+ "claude-3-haiku-20240307": (0.00025, 0.00125),
227
+ }
228
+ return pricing.get(self._model, (0.0, 0.0))
229
+
230
+ def generate_chat_stream(
231
+ self,
232
+ messages: list[dict[str, str]],
233
+ **kwargs: object,
234
+ ):
235
+ """Stream token chunks from Anthropic.
236
+
237
+ Yields individual text deltas. Return value is finish_reason.
238
+ """
239
+ # Extract system message
240
+ system_text = ""
241
+ chat_messages = []
242
+ for msg in messages:
243
+ if msg.get("role") == "system":
244
+ system_text = msg.get("content", "")
245
+ else:
246
+ chat_messages.append(msg)
247
+ if not chat_messages:
248
+ chat_messages = [{"role": "user", "content": ""}]
249
+
250
+ params: dict[str, object] = {
251
+ "model": self._model,
252
+ "messages": chat_messages,
253
+ "max_tokens": kwargs.pop("max_tokens", self._max_tokens) if "max_tokens" in kwargs else self._max_tokens,
254
+ }
255
+ if system_text:
256
+ params["system"] = system_text
257
+
258
+ finish_reason = "stop"
259
+ try:
260
+ with self._client.messages.stream(**params) as stream:
261
+ for text in stream.text_stream:
262
+ yield text
263
+ response = stream.get_final_message()
264
+ if response.stop_reason == "max_tokens":
265
+ finish_reason = "length"
266
+ except Exception as exc:
267
+ logger.error("Anthropic streaming error: %s", exc)
268
+ finish_reason = "error"
269
+
270
+ return finish_reason
crp/providers/base.py ADDED
@@ -0,0 +1,135 @@
1
+ # Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
2
+ # Licensed under Elastic License 2.0 — see LICENSE.md for details.
3
+ """LLMProvider abstract base class — §6.1.
4
+
5
+ Every provider adapter MUST implement generate_chat, count_tokens,
6
+ and context_window_size. The protocol uses these three to compute
7
+ window budgets and dispatch tasks.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from abc import ABC, abstractmethod
13
+ from collections.abc import Generator
14
+
15
+
16
+ class LLMProvider(ABC):
17
+ """Abstract interface for LLM backends.
18
+
19
+ Implementations: OpenAIAdapter, AnthropicAdapter, LlamaCppAdapter, CustomProvider.
20
+ """
21
+
22
+ @abstractmethod
23
+ def generate_chat(
24
+ self, messages: list[dict[str, str]], **kwargs: object
25
+ ) -> tuple[str, str]:
26
+ """Generate text completion from a message array.
27
+
28
+ Args:
29
+ messages: [{"role": "system"|"user"|"assistant", "content": str}, ...]
30
+ **kwargs: Provider-specific overrides (max_tokens, temperature, etc.)
31
+
32
+ Returns:
33
+ (output_text, finish_reason) where finish_reason is one of:
34
+ - "length" — model hit generation reserve limit (physical wall)
35
+ - "stop" — model produced natural EOS
36
+ - "error" — provider returned an error
37
+ """
38
+
39
+ @abstractmethod
40
+ def count_tokens(self, text: str) -> int:
41
+ """Count tokens in *text* using this provider's exact tokenizer.
42
+
43
+ MUST return accurate per-model counts — no estimates.
44
+ """
45
+
46
+ @abstractmethod
47
+ def context_window_size(self) -> int:
48
+ """Return the model's physical context window size in tokens.
49
+
50
+ E.g. 128_000 for Claude 3 Opus, 8_192 for GPT-3.5.
51
+ """
52
+
53
+ @property
54
+ def max_output_tokens(self) -> int | None:
55
+ """Optional: provider-reported max output tokens, or None."""
56
+ return None
57
+
58
+ @property
59
+ def model_name(self) -> str:
60
+ """Human-readable model identifier for diagnostics."""
61
+ return self.__class__.__name__
62
+
63
+ @property
64
+ def is_thinking_model(self) -> bool:
65
+ """Return True if the model produces reasoning_content alongside output.
66
+
67
+ Thinking models (qwen3, deepseek-r1, o1, etc.) spend a significant
68
+ portion of tokens on internal reasoning, requiring a larger generation
69
+ reserve to avoid starving the final content output.
70
+ """
71
+ return False
72
+
73
+ def cost_per_1k_tokens(self) -> tuple[float, float]:
74
+ """Return (input_cost_per_1k, output_cost_per_1k) in USD.
75
+
76
+ Returns (0.0, 0.0) for local models or unknown pricing.
77
+ Override in subclasses with known pricing.
78
+ """
79
+ return (0.0, 0.0)
80
+
81
+ # ── Tool-mediated dispatch (§20) ──────────────────────────────────
82
+
83
+ def supports_tools(self) -> bool:
84
+ """Return True if this provider supports function/tool calling.
85
+
86
+ Override in subclasses that implement generate_chat_with_tools().
87
+ The orchestrator uses this to decide between push (envelope) and
88
+ pull (tool-mediated) context relay.
89
+ """
90
+ return False
91
+
92
+ def generate_chat_with_tools(
93
+ self,
94
+ messages: list[dict[str, object]],
95
+ tools: list[dict[str, object]],
96
+ **kwargs: object,
97
+ ) -> tuple[str, str, list[dict[str, object]] | None, dict[str, object] | None]:
98
+ """Generate with tool/function calling support.
99
+
100
+ Args:
101
+ messages: Chat messages (may include tool role messages).
102
+ tools: Tool definitions in OpenAI-compatible format.
103
+ **kwargs: Provider-specific overrides.
104
+
105
+ Returns:
106
+ (output_text, finish_reason, tool_calls, raw_assistant_message)
107
+ - output_text: Generated text content (may be empty if tool_calls)
108
+ - finish_reason: "stop", "length", "tool_calls", or "error"
109
+ - tool_calls: List of tool call dicts if finish_reason=="tool_calls"
110
+ - raw_assistant_message: The full assistant message dict (for
111
+ appending to conversation history in the tool loop)
112
+ """
113
+ raise NotImplementedError(
114
+ f"{self.__class__.__name__} does not support tool calling. "
115
+ "Override supports_tools() and generate_chat_with_tools()."
116
+ )
117
+
118
+ def generate_chat_stream(
119
+ self,
120
+ messages: list[dict[str, str]],
121
+ **kwargs: object,
122
+ ) -> Generator[str, None, str]:
123
+ """Stream token chunks from the LLM.
124
+
125
+ Yields individual token chunks as strings.
126
+ The return value (accessible via StopIteration.value) is the
127
+ finish_reason ("stop" or "length").
128
+
129
+ Default implementation falls back to generate_chat() and yields
130
+ the full output as a single chunk. Override in subclasses for
131
+ real streaming.
132
+ """
133
+ output, finish_reason = self.generate_chat(messages, **kwargs)
134
+ yield output
135
+ return finish_reason
@@ -0,0 +1,63 @@
1
+ # Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
2
+ # Licensed under Elastic License 2.0 — see LICENSE.md for details.
3
+ """CustomProvider — user-supplied LLM adapter (Axiom 6: Portability).
4
+
5
+ Allows any LLM to be used with CRP if the user provides three callables:
6
+ generate_fn(messages) → (output, finish_reason)
7
+ count_tokens_fn(text) → int
8
+ context_size → int
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from collections.abc import Callable
14
+ from typing import Any
15
+
16
+ from crp.providers.base import LLMProvider
17
+
18
+
19
+ class CustomProvider(LLMProvider):
20
+ """User-supplied LLM backend.
21
+
22
+ Example::
23
+
24
+ provider = CustomProvider(
25
+ generate_fn=my_generate,
26
+ count_tokens_fn=my_tokenizer,
27
+ context_size=8192,
28
+ )
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ generate_fn: Callable[[list[dict[str, str]]], tuple[str, str]],
34
+ count_tokens_fn: Callable[[str], int],
35
+ context_size: int,
36
+ *,
37
+ name: str = "custom",
38
+ max_output: int | None = None,
39
+ ) -> None:
40
+ self._generate_fn = generate_fn
41
+ self._count_tokens_fn = count_tokens_fn
42
+ self._context_size = context_size
43
+ self._name = name
44
+ self._max_output = max_output
45
+
46
+ def generate_chat(
47
+ self, messages: list[dict[str, str]], **kwargs: Any
48
+ ) -> tuple[str, str]:
49
+ return self._generate_fn(messages)
50
+
51
+ def count_tokens(self, text: str) -> int:
52
+ return self._count_tokens_fn(text)
53
+
54
+ def context_window_size(self) -> int:
55
+ return self._context_size
56
+
57
+ @property
58
+ def max_output_tokens(self) -> int | None:
59
+ return self._max_output
60
+
61
+ @property
62
+ def model_name(self) -> str:
63
+ return self._name