cade-cli 0.3.3__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.
- cade_cli-0.3.3.dist-info/METADATA +151 -0
- cade_cli-0.3.3.dist-info/RECORD +44 -0
- cade_cli-0.3.3.dist-info/WHEEL +4 -0
- cade_cli-0.3.3.dist-info/entry_points.txt +2 -0
- cadecoder/__init__.py +1 -0
- cadecoder/ai/__init__.py +6 -0
- cadecoder/ai/prompts.py +572 -0
- cadecoder/cli/__init__.py +0 -0
- cadecoder/cli/app.py +147 -0
- cadecoder/cli/auth.py +483 -0
- cadecoder/cli/commands/__init__.py +5 -0
- cadecoder/cli/commands/auth.py +143 -0
- cadecoder/cli/commands/chat.py +264 -0
- cadecoder/cli/commands/mcp.py +477 -0
- cadecoder/cli/commands/tools.py +226 -0
- cadecoder/core/__init__.py +12 -0
- cadecoder/core/config.py +380 -0
- cadecoder/core/constants.py +281 -0
- cadecoder/core/errors.py +145 -0
- cadecoder/core/logging.py +148 -0
- cadecoder/core/types.py +235 -0
- cadecoder/core/utils.py +279 -0
- cadecoder/execution/__init__.py +46 -0
- cadecoder/execution/context_window.py +521 -0
- cadecoder/execution/orchestrator.py +562 -0
- cadecoder/execution/parallel.py +287 -0
- cadecoder/providers/__init__.py +60 -0
- cadecoder/providers/base.py +294 -0
- cadecoder/providers/openai.py +251 -0
- cadecoder/storage/__init__.py +0 -0
- cadecoder/storage/threads.py +489 -0
- cadecoder/templates/login_failed.html +21 -0
- cadecoder/templates/login_success.html +21 -0
- cadecoder/templates/styles.css +87 -0
- cadecoder/tools/__init__.py +19 -0
- cadecoder/tools/builtin.py +644 -0
- cadecoder/tools/filesystem.py +315 -0
- cadecoder/tools/git.py +221 -0
- cadecoder/tools/manager.py +1635 -0
- cadecoder/ui/__init__.py +7 -0
- cadecoder/ui/display.py +338 -0
- cadecoder/ui/input.py +145 -0
- cadecoder/ui/session.py +455 -0
- cadecoder/ui/state.py +20 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""OpenAI provider adapter.
|
|
2
|
+
|
|
3
|
+
This module implements the Provider interface for OpenAI's API,
|
|
4
|
+
handling both standard completions and streaming responses.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from collections.abc import AsyncIterator
|
|
9
|
+
from typing import Any, cast
|
|
10
|
+
|
|
11
|
+
from openai import AsyncOpenAI
|
|
12
|
+
from openai.types.chat import ChatCompletionChunk
|
|
13
|
+
|
|
14
|
+
from cadecoder.core.logging import log
|
|
15
|
+
from cadecoder.core.types import ExecutionEventType
|
|
16
|
+
from cadecoder.providers.base import (
|
|
17
|
+
Provider,
|
|
18
|
+
ProviderError,
|
|
19
|
+
ProviderRequest,
|
|
20
|
+
ProviderResponse,
|
|
21
|
+
ProviderType,
|
|
22
|
+
StreamEvent,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OpenAIProvider(Provider):
|
|
27
|
+
"""OpenAI provider implementation."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, api_key: str | None = None):
|
|
30
|
+
"""Initialize OpenAI provider.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
api_key: OpenAI API key (uses environment if not provided)
|
|
34
|
+
"""
|
|
35
|
+
self.api_key = api_key or os.environ.get("OPENAI_API_KEY")
|
|
36
|
+
if not self.api_key:
|
|
37
|
+
raise ProviderError("OpenAI API key not configured", ProviderType.OPENAI)
|
|
38
|
+
|
|
39
|
+
self.client = AsyncOpenAI(api_key=self.api_key)
|
|
40
|
+
self._supported_models = [
|
|
41
|
+
"gpt-4o",
|
|
42
|
+
"gpt-4o-mini",
|
|
43
|
+
"gpt-4-turbo",
|
|
44
|
+
"gpt-4",
|
|
45
|
+
"gpt-3.5-turbo",
|
|
46
|
+
"o1-preview",
|
|
47
|
+
"o1-mini",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def provider_type(self) -> ProviderType:
|
|
52
|
+
"""Return the provider type."""
|
|
53
|
+
return ProviderType.OPENAI
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def supported_models(self) -> list[str]:
|
|
57
|
+
"""Return list of supported models."""
|
|
58
|
+
return self._supported_models
|
|
59
|
+
|
|
60
|
+
async def complete(self, request: ProviderRequest) -> ProviderResponse:
|
|
61
|
+
"""Create a completion using OpenAI.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
request: The request to process
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
The provider's response
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
ProviderError: If the request fails
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
# Build OpenAI request
|
|
74
|
+
openai_request = self._build_openai_request(request)
|
|
75
|
+
|
|
76
|
+
# Make API call
|
|
77
|
+
response = await self.client.chat.completions.create(**openai_request)
|
|
78
|
+
|
|
79
|
+
# Convert to unified response
|
|
80
|
+
choice = response.choices[0]
|
|
81
|
+
|
|
82
|
+
# Extract tool calls if present
|
|
83
|
+
tool_calls = None
|
|
84
|
+
if choice.message.tool_calls:
|
|
85
|
+
tool_calls = [
|
|
86
|
+
{
|
|
87
|
+
"id": tc.id,
|
|
88
|
+
"type": tc.type,
|
|
89
|
+
"function": {
|
|
90
|
+
"name": tc.function.name,
|
|
91
|
+
"arguments": tc.function.arguments,
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
for tc in choice.message.tool_calls
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
return ProviderResponse(
|
|
98
|
+
content=choice.message.content,
|
|
99
|
+
tool_calls=tool_calls,
|
|
100
|
+
finish_reason=choice.finish_reason or "stop",
|
|
101
|
+
usage={
|
|
102
|
+
"prompt_tokens": response.usage.prompt_tokens if response.usage else 0,
|
|
103
|
+
"completion_tokens": response.usage.completion_tokens if response.usage else 0,
|
|
104
|
+
"total_tokens": response.usage.total_tokens if response.usage else 0,
|
|
105
|
+
},
|
|
106
|
+
model=response.model,
|
|
107
|
+
provider=ProviderType.OPENAI,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
except Exception as e:
|
|
111
|
+
log.error(f"OpenAI completion failed: {e}")
|
|
112
|
+
raise ProviderError(
|
|
113
|
+
f"OpenAI completion failed: {str(e)}",
|
|
114
|
+
provider=ProviderType.OPENAI,
|
|
115
|
+
details={"error": str(e)},
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
async def stream(self, request: ProviderRequest) -> AsyncIterator[StreamEvent]:
|
|
119
|
+
"""Stream a completion from OpenAI.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
request: The request to process
|
|
123
|
+
|
|
124
|
+
Yields:
|
|
125
|
+
Stream events as they arrive
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
ProviderError: If the request fails
|
|
129
|
+
"""
|
|
130
|
+
try:
|
|
131
|
+
# Build OpenAI request with streaming
|
|
132
|
+
openai_request = self._build_openai_request(request)
|
|
133
|
+
openai_request["stream"] = True
|
|
134
|
+
|
|
135
|
+
# Stream response
|
|
136
|
+
stream = await self.client.chat.completions.create(**openai_request)
|
|
137
|
+
|
|
138
|
+
# Track state for tool calls
|
|
139
|
+
current_tool_calls: dict[int, dict[str, Any]] = {}
|
|
140
|
+
|
|
141
|
+
async for chunk in stream:
|
|
142
|
+
chunk = cast(ChatCompletionChunk, chunk)
|
|
143
|
+
|
|
144
|
+
# Handle content delta
|
|
145
|
+
if chunk.choices and chunk.choices[0].delta.content:
|
|
146
|
+
yield StreamEvent(
|
|
147
|
+
type=ExecutionEventType.CONTENT.value,
|
|
148
|
+
content=chunk.choices[0].delta.content,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Handle tool calls
|
|
152
|
+
if chunk.choices and chunk.choices[0].delta.tool_calls:
|
|
153
|
+
for tc_delta in chunk.choices[0].delta.tool_calls:
|
|
154
|
+
idx = tc_delta.index
|
|
155
|
+
if idx not in current_tool_calls:
|
|
156
|
+
current_tool_calls[idx] = {
|
|
157
|
+
"id": tc_delta.id,
|
|
158
|
+
"type": "function",
|
|
159
|
+
"function": {"name": "", "arguments": ""},
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# Update tool call
|
|
163
|
+
if tc_delta.function:
|
|
164
|
+
if tc_delta.function.name:
|
|
165
|
+
current_tool_calls[idx]["function"]["name"] = tc_delta.function.name
|
|
166
|
+
if tc_delta.function.arguments:
|
|
167
|
+
current_tool_calls[idx]["function"]["arguments"] += (
|
|
168
|
+
tc_delta.function.arguments
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Handle finish
|
|
172
|
+
if chunk.choices and chunk.choices[0].finish_reason:
|
|
173
|
+
# Emit any pending tool calls
|
|
174
|
+
for tool_call in current_tool_calls.values():
|
|
175
|
+
yield StreamEvent(
|
|
176
|
+
type=ExecutionEventType.TOOL_CALL.value,
|
|
177
|
+
content=None,
|
|
178
|
+
metadata={"tool_call": tool_call},
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Emit completion event
|
|
182
|
+
yield StreamEvent(
|
|
183
|
+
type=ExecutionEventType.COMPLETE.value,
|
|
184
|
+
content=None,
|
|
185
|
+
metadata={
|
|
186
|
+
"finish_reason": chunk.choices[0].finish_reason,
|
|
187
|
+
"model": chunk.model,
|
|
188
|
+
},
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
except Exception as e:
|
|
192
|
+
log.error(f"OpenAI streaming failed: {e}")
|
|
193
|
+
yield StreamEvent(
|
|
194
|
+
type="error",
|
|
195
|
+
content=str(e),
|
|
196
|
+
metadata={"provider": ProviderType.OPENAI},
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def _build_openai_request(self, request: ProviderRequest) -> dict[str, Any]:
|
|
200
|
+
"""Build OpenAI API request from unified request.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
request: Unified request
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
OpenAI API request dictionary
|
|
207
|
+
"""
|
|
208
|
+
openai_request: dict[str, Any] = {
|
|
209
|
+
"model": request.model,
|
|
210
|
+
"messages": request.messages,
|
|
211
|
+
"temperature": request.temperature,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# Add optional parameters
|
|
215
|
+
if request.max_tokens:
|
|
216
|
+
openai_request["max_tokens"] = request.max_tokens
|
|
217
|
+
|
|
218
|
+
if request.tools:
|
|
219
|
+
openai_request["tools"] = request.tools
|
|
220
|
+
log.info(f"OpenAI request includes {len(request.tools)} tools")
|
|
221
|
+
# Log first few tool names for debugging
|
|
222
|
+
tool_names = [t.get("function", {}).get("name", "unknown") for t in request.tools[:5]]
|
|
223
|
+
log.debug(f"First few tools: {tool_names}")
|
|
224
|
+
|
|
225
|
+
if request.tool_choice:
|
|
226
|
+
openai_request["tool_choice"] = request.tool_choice
|
|
227
|
+
|
|
228
|
+
# Add any provider-specific metadata
|
|
229
|
+
if "openai" in request.metadata:
|
|
230
|
+
openai_request.update(request.metadata["openai"])
|
|
231
|
+
|
|
232
|
+
return openai_request
|
|
233
|
+
|
|
234
|
+
def supports_feature(self, feature: str) -> bool:
|
|
235
|
+
"""Check if OpenAI supports a feature.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
feature: Feature name
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
True if supported
|
|
242
|
+
"""
|
|
243
|
+
supported_features = {
|
|
244
|
+
"streaming": True,
|
|
245
|
+
"tools": True,
|
|
246
|
+
"tool_choice": True,
|
|
247
|
+
"vision": True,
|
|
248
|
+
"json_mode": True,
|
|
249
|
+
"reasoning": False, # Not yet
|
|
250
|
+
}
|
|
251
|
+
return supported_features.get(feature, False)
|
|
File without changes
|