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.
- crp/__init__.py +126 -0
- crp/__main__.py +8 -0
- crp/_typing.py +27 -0
- crp/_version.py +5 -0
- crp/adapters.py +31 -0
- crp/advanced/__init__.py +40 -0
- crp/advanced/auto_ingest.py +400 -0
- crp/advanced/cqs.py +235 -0
- crp/advanced/cross_window.py +477 -0
- crp/advanced/curator.py +265 -0
- crp/advanced/feedback.py +146 -0
- crp/advanced/hierarchical.py +211 -0
- crp/advanced/meta_learning.py +401 -0
- crp/advanced/parallel.py +98 -0
- crp/advanced/review_cycle.py +329 -0
- crp/advanced/scale_mode.py +129 -0
- crp/advanced/source_grounding.py +207 -0
- crp/ckf/__init__.py +35 -0
- crp/ckf/community.py +377 -0
- crp/ckf/fabric.py +445 -0
- crp/ckf/gc.py +175 -0
- crp/ckf/graph_walk.py +87 -0
- crp/ckf/merge.py +133 -0
- crp/ckf/pattern_query.py +122 -0
- crp/ckf/pubsub.py +128 -0
- crp/ckf/semantic.py +207 -0
- crp/cli/__init__.py +7 -0
- crp/cli/main.py +329 -0
- crp/cli/sidecar.py +929 -0
- crp/cli/startup.py +272 -0
- crp/continuation/__init__.py +103 -0
- crp/continuation/completion.py +348 -0
- crp/continuation/degradation.py +157 -0
- crp/continuation/document_map.py +160 -0
- crp/continuation/flow.py +109 -0
- crp/continuation/gap.py +419 -0
- crp/continuation/manager.py +484 -0
- crp/continuation/quality_monitor.py +179 -0
- crp/continuation/stitch.py +419 -0
- crp/continuation/trigger.py +142 -0
- crp/continuation/voice.py +157 -0
- crp/core/__init__.py +69 -0
- crp/core/batch.py +77 -0
- crp/core/circuit_breaker.py +116 -0
- crp/core/config.py +377 -0
- crp/core/context_tools.py +540 -0
- crp/core/dispatch_router.py +3977 -0
- crp/core/errors.py +128 -0
- crp/core/extraction_facade.py +384 -0
- crp/core/facilitator.py +713 -0
- crp/core/idempotency.py +215 -0
- crp/core/orchestrator.py +1435 -0
- crp/core/relay_strategies.py +613 -0
- crp/core/security_manager.py +140 -0
- crp/core/session.py +134 -0
- crp/core/task_intent.py +36 -0
- crp/core/window.py +363 -0
- crp/envelope/__init__.py +30 -0
- crp/envelope/builder.py +288 -0
- crp/envelope/decomposer.py +236 -0
- crp/envelope/formatter.py +168 -0
- crp/envelope/packer.py +211 -0
- crp/envelope/reranker.py +209 -0
- crp/envelope/scoring.py +310 -0
- crp/extraction/__init__.py +45 -0
- crp/extraction/complexity.py +96 -0
- crp/extraction/contradiction.py +132 -0
- crp/extraction/pipeline.py +360 -0
- crp/extraction/quality_gate.py +237 -0
- crp/extraction/stage1_regex.py +173 -0
- crp/extraction/stage2_statistical.py +244 -0
- crp/extraction/stage3_gliner.py +210 -0
- crp/extraction/stage4_uie.py +183 -0
- crp/extraction/stage5_discourse.py +175 -0
- crp/extraction/stage6_llm.py +178 -0
- crp/extraction/structured_output.py +219 -0
- crp/extraction/types.py +299 -0
- crp/license_guard.py +722 -0
- crp/observability/__init__.py +30 -0
- crp/observability/audit.py +118 -0
- crp/observability/events.py +233 -0
- crp/observability/metrics.py +264 -0
- crp/observability/quality.py +135 -0
- crp/observability/structured_logging.py +81 -0
- crp/observability/telemetry.py +117 -0
- crp/provenance/__init__.py +314 -0
- crp/provenance/_embeddings.py +97 -0
- crp/provenance/_types.py +378 -0
- crp/provenance/attribution_scorer.py +252 -0
- crp/provenance/claim_detector.py +229 -0
- crp/provenance/contradiction_detector.py +243 -0
- crp/provenance/distortion_detector.py +397 -0
- crp/provenance/entailment_verifier.py +358 -0
- crp/provenance/fabrication_detector.py +203 -0
- crp/provenance/hallucination_scorer.py +320 -0
- crp/provenance/omission_analyzer.py +106 -0
- crp/provenance/provenance_chain.py +205 -0
- crp/provenance/report_generator.py +440 -0
- crp/providers/__init__.py +43 -0
- crp/providers/anthropic.py +270 -0
- crp/providers/base.py +135 -0
- crp/providers/custom.py +63 -0
- crp/providers/diagnostic.py +251 -0
- crp/providers/llamacpp.py +224 -0
- crp/providers/manager.py +139 -0
- crp/providers/ollama.py +243 -0
- crp/providers/openai.py +628 -0
- crp/providers/tokenizers.py +48 -0
- crp/py.typed +0 -0
- crp/resources/__init__.py +53 -0
- crp/resources/adaptive_allocator.py +525 -0
- crp/resources/cost_model.py +388 -0
- crp/resources/overhead_manager.py +217 -0
- crp/resources/resource_manager.py +262 -0
- crp/schemas/__init__.py +20 -0
- crp/schemas/cost-estimate.json +33 -0
- crp/schemas/crp-error.json +43 -0
- crp/schemas/envelope-preview.json +40 -0
- crp/schemas/persisted-state-header.json +27 -0
- crp/schemas/quality-report.json +94 -0
- crp/schemas/session-handle.json +33 -0
- crp/schemas/session-status.json +57 -0
- crp/schemas/stream-event.json +18 -0
- crp/schemas/task-intent.json +42 -0
- crp/security/__init__.py +93 -0
- crp/security/audit_trail.py +392 -0
- crp/security/binding.py +192 -0
- crp/security/compliance.py +813 -0
- crp/security/consent.py +593 -0
- crp/security/embedding_defense.py +161 -0
- crp/security/encryption.py +202 -0
- crp/security/injection.py +335 -0
- crp/security/integrity.py +267 -0
- crp/security/privacy.py +662 -0
- crp/security/quarantine.py +249 -0
- crp/security/rbac.py +221 -0
- crp/security/validation.py +164 -0
- crp/state/__init__.py +31 -0
- crp/state/cold_storage.py +258 -0
- crp/state/compaction.py +263 -0
- crp/state/critical_state.py +104 -0
- crp/state/event_log.py +313 -0
- crp/state/fact.py +189 -0
- crp/state/serialization.py +189 -0
- crp/state/session_cleanup.py +77 -0
- crp/state/snapshot.py +290 -0
- crp/state/warm_store.py +346 -0
- crprotocol-2.0.0.dist-info/METADATA +1295 -0
- crprotocol-2.0.0.dist-info/RECORD +153 -0
- crprotocol-2.0.0.dist-info/WHEEL +4 -0
- crprotocol-2.0.0.dist-info/entry_points.txt +2 -0
- crprotocol-2.0.0.dist-info/licenses/LICENSE.md +170 -0
- 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
|
crp/providers/custom.py
ADDED
|
@@ -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
|