Skip to content

Commit c1b9193

Browse files
author
cellwebb
committed
fix: add create_message method and update test compatibility
Major fixes to address test failures: 1. Added create_message method to OpenAIProvider: - Non-streaming chat completion support for regular models - Delegates to existing _create_responses for codex models - Complete response normalization and error handling 2. Updated LLMProvider wrapper: - Added create_message method that delegates to underlying provider - Maintains backward compatibility with existing code 3. Updated BaseProvider class: - Added create_message with default streaming aggregation - Ensures all providers have consistent interface 4. Fixed test compatibility: - Updated agent loop tests to use stream_message instead of create_message - Added helper methods for creating streaming responses in tests - Fixed assertion checks to reference correct method calls - Updated mock provider to return iterators properly These changes fix the major AttributeError issues where tests expected create_message but providers only had stream_message. The update maintains both streaming and non-streaming capabilities for different use cases.
1 parent 36c725a commit c1b9193

4 files changed

Lines changed: 193 additions & 37 deletions

File tree

src/clippy/llm/base.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,32 @@ class LLMResponse:
3030
class BaseProvider:
3131
"""Abstract base for all LLM providers."""
3232

33+
def create_message(
34+
self,
35+
messages: list[dict[str, Any]],
36+
tools: list[dict[str, Any]] | None = None,
37+
model: str = "gpt-5-mini",
38+
**kwargs: Any,
39+
) -> dict[str, Any]:
40+
"""Create a chat completion without streaming.
41+
42+
Args:
43+
messages: List of messages in OpenAI format
44+
tools: Optional list of tool definitions in OpenAI format
45+
model: Model identifier
46+
**kwargs: Additional provider-specific arguments
47+
48+
Returns:
49+
Complete response in OpenAI format
50+
"""
51+
# Default implementation: iterate over streaming response and return final result
52+
response = None
53+
for chunk in self.stream_message(messages, tools, model, **kwargs):
54+
response = chunk
55+
if response is None:
56+
raise RuntimeError("No response received from streaming")
57+
return response
58+
3359
def stream_message(
3460
self,
3561
messages: list[dict[str, Any]],

src/clippy/llm/openai.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,36 @@ def _should_use_responses_api(self, model: str) -> bool:
5959
model_lower = model.lower()
6060
return "codex" in model_lower
6161

62+
def create_message(
63+
self,
64+
messages: list[dict[str, Any]],
65+
tools: list[dict[str, Any]] | None = None,
66+
model: str = "gpt-5-mini",
67+
**kwargs: Any,
68+
) -> dict[str, Any]:
69+
"""Create a chat completion without streaming.
70+
71+
Args:
72+
messages: List of messages in OpenAI format
73+
tools: Optional list of tool definitions
74+
model: Model identifier
75+
**kwargs: Additional arguments
76+
77+
Returns:
78+
Complete response in OpenAI format
79+
"""
80+
try:
81+
if self._should_use_responses_api(model):
82+
# For codex models, use the Responses API
83+
return self._create_responses(messages, tools, model, **kwargs)
84+
else:
85+
# For chat models, use the Chat Completions API with stream=False
86+
return self._create_chat_completion(messages, tools, model, **kwargs)
87+
except httpx.ConnectError as e:
88+
raise APIConnectionError(f"Failed to connect to {self.base_url}: {e}") from e
89+
except httpx.ReadTimeout as e:
90+
raise APITimeoutError(f"Request timed out: {e}") from e
91+
6292
def stream_message(
6393
self,
6494
messages: list[dict[str, Any]],
@@ -328,6 +358,54 @@ def _normalize_chat_response(self, data: dict[str, Any]) -> dict[str, Any]:
328358

329359
return result
330360

361+
def _create_chat_completion(
362+
self,
363+
messages: list[dict[str, Any]],
364+
tools: list[dict[str, Any]] | None,
365+
model: str,
366+
**kwargs: Any,
367+
) -> dict[str, Any]:
368+
"""Create Chat Completions API response without streaming.
369+
370+
Args:
371+
messages: List of messages in OpenAI format
372+
tools: Optional list of tool definitions
373+
model: Model identifier
374+
**kwargs: Additional arguments
375+
376+
Returns:
377+
Complete response in OpenAI format
378+
"""
379+
url = f"{self.base_url}/chat/completions"
380+
381+
# Build payload for non-streaming
382+
payload: dict[str, Any] = {
383+
"model": model,
384+
"messages": self._prepare_messages_for_chat(messages, model),
385+
"stream": False, # Disable streaming for create_message
386+
}
387+
388+
if tools:
389+
payload["tools"] = tools
390+
391+
# Add any extra kwargs (e.g., temperature, max_tokens)
392+
for key in ("temperature", "max_tokens", "top_p", "frequency_penalty", "presence_penalty"):
393+
if key in kwargs:
394+
payload[key] = kwargs[key]
395+
396+
logger.debug(f"Chat completions request to {url} with model {model}")
397+
398+
response = post_with_retry(
399+
self._client,
400+
url,
401+
json=payload,
402+
headers=self._headers(),
403+
)
404+
raise_for_status(response)
405+
406+
data = response.json()
407+
return self._normalize_chat_response(data)
408+
331409
def _stream_chat_completion(
332410
self,
333411
messages: list[dict[str, Any]],

src/clippy/providers.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,26 @@ def _create_claude_code_provider(self, base_url: str | None) -> ClaudeCodeOAuthP
142142
# Fall back to provider without token (will fail on first request)
143143
return ClaudeCodeOAuthProvider(api_key=self.api_key, base_url=base_url)
144144

145+
def create_message(
146+
self,
147+
messages: list[dict[str, Any]],
148+
tools: list[dict[str, Any]] | None = None,
149+
model: str = "gpt-5.1",
150+
**kwargs: Any,
151+
) -> dict[str, Any]:
152+
"""Create a chat completion using appropriate provider.
153+
154+
Args:
155+
messages: List of messages in OpenAI format
156+
tools: Optional list of tool definitions
157+
model: Model identifier
158+
**kwargs: Additional arguments
159+
160+
Returns:
161+
Complete response in OpenAI format
162+
"""
163+
return self._provider.create_message(messages, tools, model, **kwargs)
164+
145165
def stream_message(
146166
self,
147167
messages: list[dict[str, Any]],

0 commit comments

Comments
 (0)