codeframe-ai 0.9.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 (197) hide show
  1. codeframe/__init__.py +11 -0
  2. codeframe/__main__.py +20 -0
  3. codeframe/adapters/__init__.py +5 -0
  4. codeframe/adapters/e2b/__init__.py +13 -0
  5. codeframe/adapters/e2b/adapter.py +342 -0
  6. codeframe/adapters/e2b/budget.py +71 -0
  7. codeframe/adapters/e2b/credential_scanner.py +134 -0
  8. codeframe/adapters/llm/__init__.py +92 -0
  9. codeframe/adapters/llm/anthropic.py +414 -0
  10. codeframe/adapters/llm/base.py +444 -0
  11. codeframe/adapters/llm/mock.py +281 -0
  12. codeframe/adapters/llm/openai.py +483 -0
  13. codeframe/agents/__init__.py +8 -0
  14. codeframe/agents/dependency_resolver.py +714 -0
  15. codeframe/auth/__init__.py +16 -0
  16. codeframe/auth/api_key_router.py +238 -0
  17. codeframe/auth/api_keys.py +156 -0
  18. codeframe/auth/dependencies.py +358 -0
  19. codeframe/auth/manager.py +178 -0
  20. codeframe/auth/models.py +30 -0
  21. codeframe/auth/router.py +93 -0
  22. codeframe/auth/schemas.py +15 -0
  23. codeframe/auth/scopes.py +53 -0
  24. codeframe/cli/__init__.py +12 -0
  25. codeframe/cli/__main__.py +20 -0
  26. codeframe/cli/api_client.py +275 -0
  27. codeframe/cli/app.py +5688 -0
  28. codeframe/cli/auth.py +122 -0
  29. codeframe/cli/auth_commands.py +958 -0
  30. codeframe/cli/commands/__init__.py +5 -0
  31. codeframe/cli/config_commands.py +79 -0
  32. codeframe/cli/dashboard_commands.py +67 -0
  33. codeframe/cli/engines_commands.py +205 -0
  34. codeframe/cli/env_commands.py +409 -0
  35. codeframe/cli/helpers.py +56 -0
  36. codeframe/cli/hooks_commands.py +208 -0
  37. codeframe/cli/import_commands.py +129 -0
  38. codeframe/cli/pr_commands.py +549 -0
  39. codeframe/cli/proof_commands.py +415 -0
  40. codeframe/cli/stats_commands.py +311 -0
  41. codeframe/cli/telemetry_runtime.py +153 -0
  42. codeframe/cli/validators.py +123 -0
  43. codeframe/config/rate_limits.py +165 -0
  44. codeframe/core/__init__.py +15 -0
  45. codeframe/core/adapters/__init__.py +43 -0
  46. codeframe/core/adapters/agent_adapter.py +114 -0
  47. codeframe/core/adapters/builtin.py +326 -0
  48. codeframe/core/adapters/claude_code.py +62 -0
  49. codeframe/core/adapters/codex.py +393 -0
  50. codeframe/core/adapters/git_utils.py +40 -0
  51. codeframe/core/adapters/kilocode.py +126 -0
  52. codeframe/core/adapters/opencode.py +48 -0
  53. codeframe/core/adapters/streaming_chat.py +483 -0
  54. codeframe/core/adapters/subprocess_adapter.py +213 -0
  55. codeframe/core/adapters/verification_wrapper.py +269 -0
  56. codeframe/core/agent.py +2183 -0
  57. codeframe/core/agents_config.py +569 -0
  58. codeframe/core/api_key_service.py +211 -0
  59. codeframe/core/artifacts.py +428 -0
  60. codeframe/core/blocker_detection.py +218 -0
  61. codeframe/core/blockers.py +433 -0
  62. codeframe/core/checkpoints.py +481 -0
  63. codeframe/core/conductor.py +2255 -0
  64. codeframe/core/config.py +827 -0
  65. codeframe/core/config_watcher.py +268 -0
  66. codeframe/core/context.py +542 -0
  67. codeframe/core/context_packager.py +234 -0
  68. codeframe/core/credentials.py +735 -0
  69. codeframe/core/dependency_analyzer.py +229 -0
  70. codeframe/core/dependency_graph.py +290 -0
  71. codeframe/core/diagnostic_agent.py +712 -0
  72. codeframe/core/diagnostics.py +616 -0
  73. codeframe/core/editor.py +556 -0
  74. codeframe/core/engine_registry.py +256 -0
  75. codeframe/core/engine_stats.py +231 -0
  76. codeframe/core/environment.py +697 -0
  77. codeframe/core/events.py +375 -0
  78. codeframe/core/executor.py +1005 -0
  79. codeframe/core/fix_tracker.py +480 -0
  80. codeframe/core/gates.py +1322 -0
  81. codeframe/core/git.py +477 -0
  82. codeframe/core/github_connect_service.py +178 -0
  83. codeframe/core/github_integration_config.py +118 -0
  84. codeframe/core/github_issues_service.py +449 -0
  85. codeframe/core/hooks.py +184 -0
  86. codeframe/core/importers/__init__.py +1 -0
  87. codeframe/core/importers/ralph.py +540 -0
  88. codeframe/core/installer.py +650 -0
  89. codeframe/core/models.py +1026 -0
  90. codeframe/core/notifications_config.py +183 -0
  91. codeframe/core/planner.py +437 -0
  92. codeframe/core/prd.py +670 -0
  93. codeframe/core/prd_discovery.py +1118 -0
  94. codeframe/core/prd_stress_test.py +499 -0
  95. codeframe/core/progress.py +126 -0
  96. codeframe/core/proof/__init__.py +34 -0
  97. codeframe/core/proof/capture.py +79 -0
  98. codeframe/core/proof/evidence.py +56 -0
  99. codeframe/core/proof/ledger.py +574 -0
  100. codeframe/core/proof/models.py +162 -0
  101. codeframe/core/proof/obligations.py +103 -0
  102. codeframe/core/proof/runner.py +233 -0
  103. codeframe/core/proof/scope.py +81 -0
  104. codeframe/core/proof/stubs.py +156 -0
  105. codeframe/core/quick_fixes.py +558 -0
  106. codeframe/core/react_agent.py +1650 -0
  107. codeframe/core/reconciliation.py +183 -0
  108. codeframe/core/replay.py +788 -0
  109. codeframe/core/review.py +285 -0
  110. codeframe/core/runtime.py +1134 -0
  111. codeframe/core/sandbox/__init__.py +27 -0
  112. codeframe/core/sandbox/context.py +98 -0
  113. codeframe/core/sandbox/worktree.py +20 -0
  114. codeframe/core/schedule.py +396 -0
  115. codeframe/core/stall_detector.py +71 -0
  116. codeframe/core/stall_monitor.py +134 -0
  117. codeframe/core/state_machine.py +121 -0
  118. codeframe/core/streaming.py +502 -0
  119. codeframe/core/task_tree.py +400 -0
  120. codeframe/core/tasks.py +1022 -0
  121. codeframe/core/telemetry.py +232 -0
  122. codeframe/core/templates.py +221 -0
  123. codeframe/core/tools.py +942 -0
  124. codeframe/core/workspace.py +887 -0
  125. codeframe/core/worktrees.py +276 -0
  126. codeframe/git/__init__.py +5 -0
  127. codeframe/git/github_integration.py +505 -0
  128. codeframe/lib/__init__.py +0 -0
  129. codeframe/lib/audit_logger.py +248 -0
  130. codeframe/lib/metrics_tracker.py +800 -0
  131. codeframe/lib/quality/__init__.py +7 -0
  132. codeframe/lib/quality/complexity_analyzer.py +316 -0
  133. codeframe/lib/quality/owasp_patterns.py +284 -0
  134. codeframe/lib/quality/security_scanner.py +250 -0
  135. codeframe/lib/rate_limiter.py +312 -0
  136. codeframe/notifications/__init__.py +0 -0
  137. codeframe/notifications/webhook.py +380 -0
  138. codeframe/planning/__init__.py +30 -0
  139. codeframe/planning/issue_generator.py +219 -0
  140. codeframe/planning/prd_template_functions.py +137 -0
  141. codeframe/planning/prd_templates.py +975 -0
  142. codeframe/planning/task_scheduler.py +511 -0
  143. codeframe/planning/task_templates.py +533 -0
  144. codeframe/platform_store/__init__.py +5 -0
  145. codeframe/platform_store/database.py +277 -0
  146. codeframe/platform_store/repositories/__init__.py +24 -0
  147. codeframe/platform_store/repositories/api_key_repository.py +245 -0
  148. codeframe/platform_store/repositories/audit_repository.py +67 -0
  149. codeframe/platform_store/repositories/base.py +295 -0
  150. codeframe/platform_store/repositories/interactive_sessions.py +165 -0
  151. codeframe/platform_store/repositories/token_repository.py +598 -0
  152. codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
  153. codeframe/platform_store/schema_manager.py +321 -0
  154. codeframe/templates/AGENTS.md.default +94 -0
  155. codeframe/tui/__init__.py +5 -0
  156. codeframe/tui/app.py +256 -0
  157. codeframe/tui/data_service.py +103 -0
  158. codeframe/ui/__init__.py +0 -0
  159. codeframe/ui/dependencies.py +103 -0
  160. codeframe/ui/models.py +999 -0
  161. codeframe/ui/response_models.py +201 -0
  162. codeframe/ui/routers/__init__.py +5 -0
  163. codeframe/ui/routers/_helpers.py +29 -0
  164. codeframe/ui/routers/batches_v2.py +315 -0
  165. codeframe/ui/routers/blockers_v2.py +320 -0
  166. codeframe/ui/routers/checkpoints_v2.py +310 -0
  167. codeframe/ui/routers/costs_v2.py +322 -0
  168. codeframe/ui/routers/diagnose_v2.py +225 -0
  169. codeframe/ui/routers/discovery_v2.py +417 -0
  170. codeframe/ui/routers/environment_v2.py +284 -0
  171. codeframe/ui/routers/events_v2.py +75 -0
  172. codeframe/ui/routers/gates_v2.py +166 -0
  173. codeframe/ui/routers/git_v2.py +284 -0
  174. codeframe/ui/routers/github_integrations_v2.py +532 -0
  175. codeframe/ui/routers/interactive_sessions_v2.py +238 -0
  176. codeframe/ui/routers/pr_v2.py +709 -0
  177. codeframe/ui/routers/prd_v2.py +695 -0
  178. codeframe/ui/routers/proof_v2.py +755 -0
  179. codeframe/ui/routers/review_v2.py +360 -0
  180. codeframe/ui/routers/schedule_v2.py +214 -0
  181. codeframe/ui/routers/session_chat_ws.py +354 -0
  182. codeframe/ui/routers/settings_v2.py +562 -0
  183. codeframe/ui/routers/streaming_v2.py +155 -0
  184. codeframe/ui/routers/tasks_v2.py +1098 -0
  185. codeframe/ui/routers/templates_v2.py +232 -0
  186. codeframe/ui/routers/terminal_ws.py +267 -0
  187. codeframe/ui/routers/workspace_v2.py +527 -0
  188. codeframe/ui/server.py +568 -0
  189. codeframe/ui/shared.py +241 -0
  190. codeframe/workspace/__init__.py +5 -0
  191. codeframe/workspace/manager.py +249 -0
  192. codeframe_ai-0.9.0.dist-info/METADATA +517 -0
  193. codeframe_ai-0.9.0.dist-info/RECORD +197 -0
  194. codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
  195. codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
  196. codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
  197. codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,444 @@
1
+ """Base LLM adapter interface.
2
+
3
+ Defines the protocol that all LLM providers must implement,
4
+ along with shared data structures for requests and responses.
5
+ """
6
+
7
+ import asyncio
8
+ import os
9
+ from abc import ABC, abstractmethod
10
+ from dataclasses import dataclass, field
11
+ from enum import Enum
12
+ from typing import AsyncIterator, Iterator, Optional
13
+
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # Common exception hierarchy
17
+ # ---------------------------------------------------------------------------
18
+
19
+
20
+ class LLMError(Exception):
21
+ """Base exception for LLM provider errors."""
22
+
23
+
24
+ class LLMAuthError(LLMError):
25
+ """Authentication failure (bad key, expired token, etc.)."""
26
+
27
+
28
+ class LLMRateLimitError(LLMError):
29
+ """Rate limit exceeded — caller may retry after a backoff."""
30
+
31
+
32
+ class LLMConnectionError(LLMError):
33
+ """Network or connection error."""
34
+
35
+
36
+ class Purpose(str, Enum):
37
+ """Purpose of an LLM call, used for model selection."""
38
+
39
+ PLANNING = "planning" # Complex reasoning, architecture decisions
40
+ EXECUTION = "execution" # Code generation, editing
41
+ GENERATION = "generation" # Simple text generation, summaries
42
+ CORRECTION = "correction" # Self-correction after errors (uses stronger model)
43
+ SUPERVISION = "supervision" # Supervisor decisions (blocker triage, tactical resolution)
44
+
45
+
46
+ # Default model aliases (use valid Anthropic model identifiers)
47
+ DEFAULT_PLANNING_MODEL = "claude-sonnet-4-20250514"
48
+ DEFAULT_EXECUTION_MODEL = "claude-sonnet-4-20250514"
49
+ DEFAULT_GENERATION_MODEL = "claude-3-5-haiku-20241022"
50
+ DEFAULT_CORRECTION_MODEL = "claude-opus-4-20250514" # Step up for fixing errors
51
+ DEFAULT_SUPERVISION_MODEL = "claude-opus-4-20250514" # Strong model for supervisor decisions
52
+
53
+
54
+ @dataclass
55
+ class ModelSelector:
56
+ """Task-based model selection heuristics.
57
+
58
+ Maps operation purposes to appropriate models. Model names can be
59
+ overridden via environment variables:
60
+ - CODEFRAME_PLANNING_MODEL
61
+ - CODEFRAME_EXECUTION_MODEL
62
+ - CODEFRAME_GENERATION_MODEL
63
+ - CODEFRAME_CORRECTION_MODEL
64
+ - CODEFRAME_SUPERVISION_MODEL
65
+ """
66
+
67
+ planning_model: str = ""
68
+ execution_model: str = ""
69
+ generation_model: str = ""
70
+ correction_model: str = ""
71
+ supervision_model: str = ""
72
+
73
+ def __post_init__(self):
74
+ """Load model names from environment or use defaults.
75
+
76
+ Only assigns from environment/defaults when the field is empty/falsy,
77
+ respecting any explicit constructor overrides.
78
+ """
79
+ if not self.planning_model:
80
+ self.planning_model = os.getenv(
81
+ "CODEFRAME_PLANNING_MODEL", DEFAULT_PLANNING_MODEL
82
+ )
83
+ if not self.execution_model:
84
+ self.execution_model = os.getenv(
85
+ "CODEFRAME_EXECUTION_MODEL", DEFAULT_EXECUTION_MODEL
86
+ )
87
+ if not self.generation_model:
88
+ self.generation_model = os.getenv(
89
+ "CODEFRAME_GENERATION_MODEL", DEFAULT_GENERATION_MODEL
90
+ )
91
+ if not self.correction_model:
92
+ self.correction_model = os.getenv(
93
+ "CODEFRAME_CORRECTION_MODEL", DEFAULT_CORRECTION_MODEL
94
+ )
95
+ if not self.supervision_model:
96
+ self.supervision_model = os.getenv(
97
+ "CODEFRAME_SUPERVISION_MODEL", DEFAULT_SUPERVISION_MODEL
98
+ )
99
+
100
+ def for_purpose(self, purpose: Purpose) -> str:
101
+ """Get the model for a given purpose.
102
+
103
+ Args:
104
+ purpose: The purpose of the LLM call
105
+
106
+ Returns:
107
+ Model identifier string
108
+ """
109
+ if purpose == Purpose.PLANNING:
110
+ return self.planning_model
111
+ elif purpose == Purpose.EXECUTION:
112
+ return self.execution_model
113
+ elif purpose == Purpose.GENERATION:
114
+ return self.generation_model
115
+ elif purpose == Purpose.CORRECTION:
116
+ return self.correction_model
117
+ elif purpose == Purpose.SUPERVISION:
118
+ return self.supervision_model
119
+ else:
120
+ return self.execution_model # Default fallback
121
+
122
+
123
+ @dataclass
124
+ class StreamChunk:
125
+ """A normalized chunk from a streaming LLM response.
126
+
127
+ Provider-specific streaming formats are translated into this common type
128
+ by each :class:`LLMProvider` implementation.
129
+
130
+ Attributes:
131
+ type: Event type — one of ``"text_delta"``, ``"thinking_delta"``,
132
+ ``"tool_use_start"``, ``"tool_use_stop"``, ``"message_stop"``.
133
+ text: Text content for ``text_delta`` and ``thinking_delta`` types.
134
+ tool_id: Tool call ID for ``tool_use_start``.
135
+ tool_name: Tool name for ``tool_use_start``.
136
+ tool_input: Tool input dict for ``tool_use_start`` (may be empty;
137
+ final inputs are provided in the ``message_stop`` chunk).
138
+ input_tokens: Input token count, populated for ``message_stop``.
139
+ output_tokens: Output token count, populated for ``message_stop``.
140
+ stop_reason: Why the model stopped, populated for ``message_stop``.
141
+ tool_inputs_by_id: Mapping of tool_id → final input dict, populated
142
+ for ``message_stop``. More reliable than streaming incremental
143
+ input deltas.
144
+
145
+ .. note:: ``tool_use_stop`` ordering differs by provider:
146
+
147
+ - **Anthropic**: emitted immediately when each tool call's content
148
+ block ends (``content_block_stop`` event), so consumers see
149
+ ``tool_use_start → [deltas] → tool_use_stop`` interleaved.
150
+ - **OpenAI-compatible**: emitted after the full stream ends (before
151
+ ``message_stop``), because the SSE protocol has no per-tool stop
152
+ marker. All ``tool_use_stop`` chunks arrive together at the end.
153
+
154
+ Consumers MUST use ``tool_inputs_by_id`` from the ``message_stop``
155
+ chunk for final tool inputs rather than relying on ``tool_use_stop``
156
+ ordering.
157
+ """
158
+
159
+ type: str
160
+ text: Optional[str] = None
161
+ tool_id: Optional[str] = None
162
+ tool_name: Optional[str] = None
163
+ tool_input: Optional[dict] = None
164
+ input_tokens: Optional[int] = None
165
+ output_tokens: Optional[int] = None
166
+ stop_reason: Optional[str] = None
167
+ tool_inputs_by_id: Optional[dict] = None
168
+
169
+
170
+ @dataclass
171
+ class ToolCall:
172
+ """Represents a tool call requested by the LLM.
173
+
174
+ Attributes:
175
+ id: Unique identifier for this tool call
176
+ name: Name of the tool to call
177
+ input: Input arguments for the tool (as dict)
178
+ """
179
+
180
+ id: str
181
+ name: str
182
+ input: dict
183
+
184
+
185
+ @dataclass
186
+ class ToolResult:
187
+ """Result of executing a tool call.
188
+
189
+ Attributes:
190
+ tool_call_id: ID of the tool call this is responding to
191
+ content: Result content (string or structured data)
192
+ is_error: Whether this result represents an error
193
+ """
194
+
195
+ tool_call_id: str
196
+ content: str
197
+ is_error: bool = False
198
+
199
+
200
+ @dataclass
201
+ class LLMResponse:
202
+ """Response from an LLM completion.
203
+
204
+ Attributes:
205
+ content: Text content of the response (may be empty if tool calls)
206
+ tool_calls: List of tool calls requested by the model
207
+ stop_reason: Why the model stopped generating
208
+ model: Model that generated this response
209
+ input_tokens: Number of input tokens used
210
+ output_tokens: Number of output tokens generated
211
+ """
212
+
213
+ content: str
214
+ tool_calls: list[ToolCall] = field(default_factory=list)
215
+ stop_reason: str = "end_turn"
216
+ model: str = ""
217
+ input_tokens: int = 0
218
+ output_tokens: int = 0
219
+
220
+ @property
221
+ def has_tool_calls(self) -> bool:
222
+ """Check if response contains tool calls."""
223
+ return len(self.tool_calls) > 0
224
+
225
+
226
+ @dataclass
227
+ class Message:
228
+ """A message in a conversation.
229
+
230
+ Attributes:
231
+ role: Message role ("user", "assistant", "system")
232
+ content: Message content
233
+ tool_calls: Tool calls (for assistant messages)
234
+ tool_results: Tool results (for user messages responding to tool calls)
235
+ """
236
+
237
+ role: str
238
+ content: str
239
+ tool_calls: list[ToolCall] = field(default_factory=list)
240
+ tool_results: list[ToolResult] = field(default_factory=list)
241
+
242
+ def to_dict(self) -> dict:
243
+ """Convert to dict format for API calls."""
244
+ result = {"role": self.role, "content": self.content}
245
+ if self.tool_calls:
246
+ result["tool_calls"] = [
247
+ {"id": tc.id, "name": tc.name, "input": tc.input}
248
+ for tc in self.tool_calls
249
+ ]
250
+ if self.tool_results:
251
+ result["tool_results"] = [
252
+ {
253
+ "tool_call_id": tr.tool_call_id,
254
+ "content": tr.content,
255
+ "is_error": tr.is_error,
256
+ }
257
+ for tr in self.tool_results
258
+ ]
259
+ return result
260
+
261
+
262
+ @dataclass
263
+ class Tool:
264
+ """Definition of a tool the LLM can use.
265
+
266
+ Attributes:
267
+ name: Tool name
268
+ description: What the tool does
269
+ input_schema: JSON schema for tool input
270
+ """
271
+
272
+ name: str
273
+ description: str
274
+ input_schema: dict
275
+
276
+
277
+ class LLMProvider(ABC):
278
+ """Abstract base class for LLM providers.
279
+
280
+ Implementations must provide complete() and optionally stream().
281
+ Model selection is handled via the purpose parameter.
282
+ """
283
+
284
+ def __init__(self, model_selector: Optional[ModelSelector] = None):
285
+ """Initialize the provider.
286
+
287
+ Args:
288
+ model_selector: Custom model selector (uses defaults if None)
289
+ """
290
+ self.model_selector = model_selector or ModelSelector()
291
+
292
+ @abstractmethod
293
+ def complete(
294
+ self,
295
+ messages: list[dict],
296
+ purpose: Purpose = Purpose.EXECUTION,
297
+ tools: Optional[list[Tool]] = None,
298
+ max_tokens: int = 4096,
299
+ temperature: float = 0.0,
300
+ system: Optional[str] = None,
301
+ ) -> LLMResponse:
302
+ """Generate a completion.
303
+
304
+ Args:
305
+ messages: Conversation messages
306
+ purpose: Purpose of call (for model selection)
307
+ tools: Available tools for the model to use
308
+ max_tokens: Maximum tokens to generate
309
+ temperature: Sampling temperature
310
+ system: System prompt
311
+
312
+ Returns:
313
+ LLMResponse with content and/or tool calls
314
+ """
315
+ pass
316
+
317
+ def stream(
318
+ self,
319
+ messages: list[dict],
320
+ purpose: Purpose = Purpose.EXECUTION,
321
+ max_tokens: int = 4096,
322
+ temperature: float = 0.0,
323
+ system: Optional[str] = None,
324
+ ) -> Iterator[str]:
325
+ """Stream a completion token by token.
326
+
327
+ Default implementation falls back to complete() and yields full response.
328
+ Override for true streaming support.
329
+
330
+ Args:
331
+ messages: Conversation messages
332
+ purpose: Purpose of call (for model selection)
333
+ max_tokens: Maximum tokens to generate
334
+ temperature: Sampling temperature
335
+ system: System prompt
336
+
337
+ Yields:
338
+ Text chunks as they are generated
339
+ """
340
+ response = self.complete(
341
+ messages=messages,
342
+ purpose=purpose,
343
+ max_tokens=max_tokens,
344
+ temperature=temperature,
345
+ system=system,
346
+ )
347
+ yield response.content
348
+
349
+ async def async_complete(
350
+ self,
351
+ messages: list[dict],
352
+ purpose: Purpose = Purpose.EXECUTION,
353
+ tools: Optional[list["Tool"]] = None,
354
+ max_tokens: int = 4096,
355
+ temperature: float = 0.0,
356
+ system: Optional[str] = None,
357
+ ) -> "LLMResponse":
358
+ """Async completion.
359
+
360
+ Default implementation wraps the synchronous :meth:`complete` in a
361
+ thread-pool executor so it never blocks the event loop. Subclasses
362
+ should override this with a truly async implementation when the
363
+ underlying SDK supports it.
364
+
365
+ Args:
366
+ messages: Conversation messages
367
+ purpose: Purpose of call (for model selection)
368
+ tools: Available tools for the model to use
369
+ max_tokens: Maximum tokens to generate
370
+ temperature: Sampling temperature
371
+ system: System prompt
372
+
373
+ Returns:
374
+ LLMResponse with content and/or tool calls
375
+ """
376
+ loop = asyncio.get_running_loop()
377
+ return await loop.run_in_executor(
378
+ None,
379
+ lambda: self.complete(messages, purpose, tools, max_tokens, temperature, system),
380
+ )
381
+
382
+ def supports(self, capability: str) -> bool:
383
+ """Check whether this provider supports an optional capability.
384
+
385
+ Args:
386
+ capability: Capability name, e.g. ``"extended_thinking"``.
387
+
388
+ Returns:
389
+ ``True`` if the capability is supported, ``False`` otherwise.
390
+ """
391
+ return False
392
+
393
+ # Not decorated with @abstractmethod intentionally: providers that only
394
+ # support synchronous completion (e.g. thin wrappers) don't need to
395
+ # implement streaming. Calling async_stream() on such a provider raises
396
+ # NotImplementedError at call time rather than at instantiation.
397
+ async def async_stream(
398
+ self,
399
+ messages: list[dict],
400
+ system: str,
401
+ tools: list[dict],
402
+ model: str,
403
+ max_tokens: int,
404
+ interrupt_event: Optional[asyncio.Event] = None,
405
+ extended_thinking: bool = False,
406
+ ) -> AsyncIterator["StreamChunk"]:
407
+ """Stream a completion as normalized :class:`StreamChunk` objects.
408
+
409
+ Subclasses should override this with a provider-specific implementation.
410
+ The default raises :exc:`NotImplementedError`.
411
+
412
+ Args:
413
+ messages: Conversation messages in the provider's expected format.
414
+ system: System prompt string.
415
+ tools: Already-serialized tool definitions (list of dicts).
416
+ model: Model identifier to use for this call.
417
+ max_tokens: Maximum output tokens.
418
+ interrupt_event: When set, the stream should stop at the next
419
+ opportunity.
420
+ extended_thinking: When ``True``, request extended thinking tokens
421
+ from providers that support them (see :meth:`supports`).
422
+ Providers that do not support this capability should silently
423
+ ignore the flag.
424
+
425
+ Yields:
426
+ :class:`StreamChunk` objects in order of generation.
427
+ """
428
+ raise NotImplementedError(
429
+ f"{type(self).__name__} does not implement async_stream(). "
430
+ "Override this method in your provider subclass."
431
+ )
432
+ if False: # pragma: no cover # makes this an async generator
433
+ yield # type: ignore[misc]
434
+
435
+ def get_model(self, purpose: Purpose) -> str:
436
+ """Get the model for a given purpose.
437
+
438
+ Args:
439
+ purpose: The purpose of the LLM call
440
+
441
+ Returns:
442
+ Model identifier string
443
+ """
444
+ return self.model_selector.for_purpose(purpose)