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,483 @@
1
+ """OpenAI-compatible LLM provider implementation.
2
+
3
+ Provides access to OpenAI and any OpenAI-compatible endpoint
4
+ (Ollama, vLLM, LM Studio, Groq, Together, etc.) via the openai SDK.
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ import logging
10
+ import os
11
+ from typing import TYPE_CHECKING, AsyncIterator, Iterator, Optional
12
+
13
+ import openai
14
+
15
+ from codeframe.adapters.llm.base import (
16
+ LLMProvider,
17
+ LLMResponse,
18
+ ModelSelector,
19
+ Purpose,
20
+ StreamChunk,
21
+ Tool,
22
+ ToolCall,
23
+ )
24
+
25
+ if TYPE_CHECKING:
26
+ from codeframe.core.credentials import CredentialManager
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ _STOP_REASON_MAP = {
31
+ "stop": "end_turn",
32
+ "tool_calls": "tool_use",
33
+ }
34
+
35
+
36
+ class OpenAIProvider(LLMProvider):
37
+ """OpenAI-compatible provider.
38
+
39
+ Uses the openai Python SDK to make API calls.
40
+ A configurable base_url covers the entire OpenAI-compatible ecosystem:
41
+ OpenAI, Ollama, vLLM, LM Studio, Groq, Together, etc.
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ api_key: Optional[str] = None,
47
+ model: str = "gpt-4o",
48
+ base_url: Optional[str] = None,
49
+ model_selector: Optional[ModelSelector] = None,
50
+ credential_manager: Optional["CredentialManager"] = None,
51
+ ):
52
+ """Initialize the OpenAI provider.
53
+
54
+ Args:
55
+ api_key: OpenAI API key (defaults to OPENAI_API_KEY env var)
56
+ model: Default model to use for all purposes
57
+ base_url: Custom endpoint URL for OpenAI-compatible APIs
58
+ model_selector: Optional model selector; when provided, defers to it for per-purpose routing
59
+ credential_manager: Optional credential manager for secure key retrieval
60
+
61
+ Raises:
62
+ ValueError: If no API key is available
63
+ """
64
+ self._has_custom_selector = model_selector is not None
65
+ super().__init__(model_selector)
66
+
67
+ self.model = model
68
+ self.base_url = base_url
69
+ self.api_key = api_key
70
+
71
+ if not self.api_key and credential_manager:
72
+ from codeframe.core.credentials import CredentialProvider
73
+ self.api_key = credential_manager.get_credential(CredentialProvider.LLM_OPENAI)
74
+
75
+ if not self.api_key:
76
+ self.api_key = os.getenv("OPENAI_API_KEY")
77
+
78
+ if not self.api_key:
79
+ raise ValueError(
80
+ "OPENAI_API_KEY not set. "
81
+ "Set the environment variable, pass api_key parameter, "
82
+ "or configure via 'codeframe auth setup --provider openai'."
83
+ )
84
+
85
+ self._client = None
86
+ self._async_client = None
87
+
88
+ def get_model(self, purpose: Purpose) -> str:
89
+ """Return the model for a given purpose.
90
+
91
+ When an explicit model_selector was provided, defers to it so callers
92
+ can route PLANNING/EXECUTION/GENERATION to different OpenAI models.
93
+ Otherwise returns self.model for all purposes (single-model mode).
94
+ """
95
+ if self._has_custom_selector:
96
+ return self.model_selector.for_purpose(purpose)
97
+ return self.model
98
+
99
+ @property
100
+ def client(self):
101
+ """Lazy-load the OpenAI client."""
102
+ if self._client is None:
103
+ self._client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url)
104
+ return self._client
105
+
106
+ def complete(
107
+ self,
108
+ messages: list[dict],
109
+ purpose: Purpose = Purpose.EXECUTION,
110
+ tools: Optional[list[Tool]] = None,
111
+ max_tokens: int = 4096,
112
+ temperature: float = 0.0,
113
+ system: Optional[str] = None,
114
+ ) -> LLMResponse:
115
+ """Generate a completion using an OpenAI-compatible API.
116
+
117
+ Args:
118
+ messages: Conversation messages
119
+ purpose: Purpose of call (for model selection — always returns self.model)
120
+ tools: Available tools for the model to use
121
+ max_tokens: Maximum tokens to generate
122
+ temperature: Sampling temperature
123
+ system: System prompt
124
+
125
+ Returns:
126
+ LLMResponse with content and/or tool calls
127
+ """
128
+ converted = self._convert_messages(messages)
129
+
130
+ if system:
131
+ converted = [{"role": "system", "content": system}] + converted
132
+
133
+ kwargs = {
134
+ "model": self.get_model(purpose),
135
+ "max_tokens": max_tokens,
136
+ "messages": converted,
137
+ "temperature": temperature,
138
+ }
139
+
140
+ if tools:
141
+ kwargs["tools"] = self._convert_tools(tools)
142
+ kwargs["tool_choice"] = "auto"
143
+
144
+ try:
145
+ response = self.client.chat.completions.create(**kwargs)
146
+ except openai.AuthenticationError as exc:
147
+ raise ValueError(f"OpenAI authentication failed: {exc}") from exc
148
+ except openai.RateLimitError as exc:
149
+ raise ValueError(f"OpenAI rate limit exceeded: {exc}") from exc
150
+ except openai.NotFoundError as exc:
151
+ raise ValueError(f"OpenAI model not found: {exc}") from exc
152
+
153
+ return self._parse_response(response)
154
+
155
+ async def async_complete(
156
+ self,
157
+ messages: list[dict],
158
+ purpose: Purpose = Purpose.EXECUTION,
159
+ tools: Optional[list[Tool]] = None,
160
+ max_tokens: int = 4096,
161
+ temperature: float = 0.0,
162
+ system: Optional[str] = None,
163
+ ) -> LLMResponse:
164
+ """True async completion via openai.AsyncOpenAI.
165
+
166
+ Raises LLMAuthError / LLMRateLimitError / LLMConnectionError on failure.
167
+ """
168
+ import openai as _openai
169
+ from codeframe.adapters.llm.base import (
170
+ LLMAuthError,
171
+ LLMRateLimitError,
172
+ LLMConnectionError,
173
+ )
174
+
175
+ if self._async_client is None:
176
+ self._async_client = _openai.AsyncOpenAI(
177
+ api_key=self.api_key, base_url=self.base_url
178
+ )
179
+
180
+ converted = self._convert_messages(messages)
181
+ if system:
182
+ converted = [{"role": "system", "content": system}] + converted
183
+
184
+ kwargs: dict = {
185
+ "model": self.get_model(purpose),
186
+ "max_tokens": max_tokens,
187
+ "messages": converted,
188
+ "temperature": temperature,
189
+ }
190
+ if tools:
191
+ kwargs["tools"] = self._convert_tools(tools)
192
+ kwargs["tool_choice"] = "auto"
193
+
194
+ try:
195
+ response = await self._async_client.chat.completions.create(**kwargs)
196
+ return self._parse_response(response)
197
+ except _openai.AuthenticationError as exc:
198
+ raise LLMAuthError(str(exc)) from exc
199
+ except _openai.RateLimitError as exc:
200
+ raise LLMRateLimitError(str(exc)) from exc
201
+ except _openai.APIConnectionError as exc:
202
+ raise LLMConnectionError(str(exc)) from exc
203
+
204
+ async def async_stream(
205
+ self,
206
+ messages: list[dict],
207
+ system: str,
208
+ tools: list[dict],
209
+ model: str,
210
+ max_tokens: int,
211
+ interrupt_event: Optional[asyncio.Event] = None,
212
+ extended_thinking: bool = False,
213
+ ) -> AsyncIterator[StreamChunk]:
214
+ """Stream using OpenAI async client, yielding StreamChunk objects.
215
+
216
+ Translates OpenAI SSE chunks into the normalized StreamChunk format.
217
+ Tool calls are emitted as tool_use_start chunks (deferred until both
218
+ id and name are known); final inputs are collected and emitted in the
219
+ message_stop chunk via tool_inputs_by_id.
220
+
221
+ ``extended_thinking`` is silently ignored — OpenAI-compatible endpoints
222
+ do not support Anthropic extended thinking.
223
+ """
224
+ import openai as _openai
225
+ from codeframe.adapters.llm.base import (
226
+ LLMAuthError,
227
+ LLMConnectionError,
228
+ LLMRateLimitError,
229
+ )
230
+
231
+ if self._async_client is None:
232
+ self._async_client = _openai.AsyncOpenAI(
233
+ api_key=self.api_key, base_url=self.base_url
234
+ )
235
+
236
+ converted = self._convert_messages(messages)
237
+ if system:
238
+ converted = [{"role": "system", "content": system}] + converted
239
+
240
+ kwargs: dict = {
241
+ "model": model,
242
+ "max_tokens": max_tokens,
243
+ "messages": converted,
244
+ "stream": True,
245
+ "stream_options": {"include_usage": True},
246
+ }
247
+
248
+ if tools:
249
+ kwargs["tools"] = self._convert_raw_tools(tools)
250
+ kwargs["tool_choice"] = "auto"
251
+
252
+ # Track partial tool calls across chunks (OpenAI streams them incrementally).
253
+ # key: index → {id, name, arguments_parts, emitted_start}
254
+ partial_tool_calls: dict[int, dict] = {}
255
+ usage_input: int = 0
256
+ usage_output: int = 0
257
+ stop_reason: str = "end_turn"
258
+
259
+ try:
260
+ async for chunk in await self._async_client.chat.completions.create(**kwargs):
261
+ if interrupt_event and interrupt_event.is_set():
262
+ return
263
+
264
+ # Usage is in the final chunk when stream_options.include_usage is set
265
+ if chunk.usage is not None:
266
+ usage_input = chunk.usage.prompt_tokens or 0
267
+ usage_output = chunk.usage.completion_tokens or 0
268
+
269
+ if not chunk.choices:
270
+ continue
271
+
272
+ choice = chunk.choices[0]
273
+ delta = choice.delta
274
+
275
+ if choice.finish_reason:
276
+ stop_reason = _STOP_REASON_MAP.get(choice.finish_reason, choice.finish_reason)
277
+
278
+ if delta.content:
279
+ yield StreamChunk(type="text_delta", text=delta.content)
280
+
281
+ if delta.tool_calls:
282
+ for tc_delta in delta.tool_calls:
283
+ idx = tc_delta.index
284
+ if idx not in partial_tool_calls:
285
+ partial_tool_calls[idx] = {
286
+ "id": tc_delta.id or "",
287
+ "name": (tc_delta.function.name if tc_delta.function else ""),
288
+ "arguments_parts": [],
289
+ "emitted_start": False,
290
+ }
291
+ else:
292
+ # Accumulate id/name as they arrive across deltas
293
+ if tc_delta.id:
294
+ partial_tool_calls[idx]["id"] = tc_delta.id
295
+ if tc_delta.function and tc_delta.function.name:
296
+ partial_tool_calls[idx]["name"] = tc_delta.function.name
297
+
298
+ if tc_delta.function and tc_delta.function.arguments:
299
+ partial_tool_calls[idx]["arguments_parts"].append(
300
+ tc_delta.function.arguments
301
+ )
302
+
303
+ # Defer tool_use_start until both id and name are known
304
+ tc_info = partial_tool_calls[idx]
305
+ if not tc_info["emitted_start"] and tc_info["id"] and tc_info["name"]:
306
+ yield StreamChunk(
307
+ type="tool_use_start",
308
+ tool_id=tc_info["id"],
309
+ tool_name=tc_info["name"],
310
+ tool_input={},
311
+ )
312
+ tc_info["emitted_start"] = True
313
+
314
+ except _openai.AuthenticationError as exc:
315
+ raise LLMAuthError(str(exc)) from exc
316
+ except _openai.RateLimitError as exc:
317
+ raise LLMRateLimitError(str(exc)) from exc
318
+ except _openai.APIConnectionError as exc:
319
+ raise LLMConnectionError(str(exc)) from exc
320
+
321
+ # Build tool_inputs_by_id from accumulated partial tool calls
322
+ tool_inputs_by_id: dict = {}
323
+ for tc in partial_tool_calls.values():
324
+ raw_args = "".join(tc["arguments_parts"]) or "{}"
325
+ try:
326
+ tool_inputs_by_id[tc["id"]] = json.loads(raw_args)
327
+ except json.JSONDecodeError:
328
+ logger.warning(
329
+ "Failed to parse tool arguments for tool '%s' (id=%s): %r",
330
+ tc["name"],
331
+ tc["id"],
332
+ raw_args,
333
+ )
334
+ tool_inputs_by_id[tc["id"]] = {}
335
+ # Emit tool_use_stop for each completed tool call
336
+ yield StreamChunk(type="tool_use_stop")
337
+
338
+ yield StreamChunk(
339
+ type="message_stop",
340
+ stop_reason=stop_reason,
341
+ input_tokens=usage_input,
342
+ output_tokens=usage_output,
343
+ tool_inputs_by_id=tool_inputs_by_id,
344
+ )
345
+
346
+ def stream(
347
+ self,
348
+ messages: list[dict],
349
+ purpose: Purpose = Purpose.EXECUTION,
350
+ max_tokens: int = 4096,
351
+ temperature: float = 0.0,
352
+ system: Optional[str] = None,
353
+ ) -> Iterator[str]:
354
+ """Stream a completion token by token.
355
+
356
+ Args:
357
+ messages: Conversation messages
358
+ purpose: Purpose of call
359
+ max_tokens: Maximum tokens to generate
360
+ temperature: Sampling temperature
361
+ system: System prompt
362
+
363
+ Yields:
364
+ Text chunks as they are generated
365
+ """
366
+ converted = self._convert_messages(messages)
367
+
368
+ if system:
369
+ converted = [{"role": "system", "content": system}] + converted
370
+
371
+ kwargs = {
372
+ "model": self.get_model(purpose),
373
+ "max_tokens": max_tokens,
374
+ "messages": converted,
375
+ "stream": True,
376
+ "temperature": temperature,
377
+ }
378
+
379
+ for chunk in self.client.chat.completions.create(**kwargs):
380
+ content = chunk.choices[0].delta.content
381
+ if content is not None:
382
+ yield content
383
+
384
+ def _convert_messages(self, messages: list[dict]) -> list[dict]:
385
+ """Convert internal message format to OpenAI Chat Completions format.
386
+
387
+ OpenAI differences from internal format:
388
+ - Tool results must be separate messages with role='tool'
389
+ - Tool calls on assistant messages use a specific nested format
390
+ """
391
+ converted = []
392
+ for msg in messages:
393
+ if msg.get("tool_results"):
394
+ # Each tool result becomes its own role='tool' message
395
+ for tr in msg["tool_results"]:
396
+ converted.append({
397
+ "role": "tool",
398
+ "tool_call_id": tr["tool_call_id"],
399
+ "content": tr["content"],
400
+ })
401
+ elif msg.get("tool_calls"):
402
+ # Assistant message with tool calls
403
+ converted.append({
404
+ "role": "assistant",
405
+ "content": msg.get("content", ""),
406
+ "tool_calls": [
407
+ {
408
+ "id": tc["id"],
409
+ "type": "function",
410
+ "function": {
411
+ "name": tc["name"],
412
+ "arguments": json.dumps(tc["input"]),
413
+ },
414
+ }
415
+ for tc in msg["tool_calls"]
416
+ ],
417
+ })
418
+ else:
419
+ converted.append({"role": msg["role"], "content": msg["content"]})
420
+ return converted
421
+
422
+ def _convert_tools(self, tools: list[Tool]) -> list[dict]:
423
+ """Convert Tool objects to OpenAI function-calling format."""
424
+ return [
425
+ {
426
+ "type": "function",
427
+ "function": {
428
+ "name": tool.name,
429
+ "description": tool.description,
430
+ "parameters": tool.input_schema,
431
+ },
432
+ }
433
+ for tool in tools
434
+ ]
435
+
436
+ def _convert_raw_tools(self, tools: list[dict]) -> list[dict]:
437
+ """Convert already-serialized tool dicts (Anthropic-style) to OpenAI format.
438
+
439
+ The ``async_stream()`` interface receives tools as ``list[dict]`` with an
440
+ ``input_schema`` key (Anthropic API format). This helper converts them to
441
+ the OpenAI ``function`` calling format, mirroring :meth:`_convert_tools`
442
+ for raw dicts instead of :class:`Tool` objects.
443
+ """
444
+ return [
445
+ {
446
+ "type": "function",
447
+ "function": {
448
+ "name": t["name"],
449
+ "description": t.get("description", ""),
450
+ "parameters": t.get("input_schema", {}),
451
+ },
452
+ }
453
+ for t in tools
454
+ ]
455
+
456
+ def _parse_response(self, response) -> LLMResponse:
457
+ """Parse OpenAI ChatCompletion into LLMResponse."""
458
+ choice = response.choices[0]
459
+ message = choice.message
460
+
461
+ content = message.content or ""
462
+ tool_calls = []
463
+
464
+ if message.tool_calls:
465
+ for tc in message.tool_calls:
466
+ tool_calls.append(
467
+ ToolCall(
468
+ id=tc.id,
469
+ name=tc.function.name,
470
+ input=json.loads(tc.function.arguments),
471
+ )
472
+ )
473
+
474
+ stop_reason = _STOP_REASON_MAP.get(choice.finish_reason, choice.finish_reason)
475
+
476
+ return LLMResponse(
477
+ content=content,
478
+ tool_calls=tool_calls,
479
+ stop_reason=stop_reason,
480
+ model=response.model,
481
+ input_tokens=response.usage.prompt_tokens,
482
+ output_tokens=response.usage.completion_tokens,
483
+ )
@@ -0,0 +1,8 @@
1
+ """Agent utilities for CodeFRAME.
2
+
3
+ The legacy multi-agent orchestration (LeadAgent/WorkerAgent/AgentFactory) was
4
+ removed during the v1 cleanup. Only the dependency resolver remains, used by the
5
+ live ``cf`` task-scheduling commands. Import it directly:
6
+
7
+ from codeframe.agents.dependency_resolver import ...
8
+ """