mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-17 13:45:34 +02:00
merge: preserve tested tool-choice provider source 2950
This commit is contained in:
@@ -16,6 +16,7 @@ from llm.core.interface import (
|
||||
)
|
||||
from llm.core.types import LLMInput, LLMOutput, ModelInfo, ProviderType, ToolCall
|
||||
from llm.providers.constants import EMPTY_FILTERED_RESPONSE_ERROR
|
||||
from llm.providers.openai_compat import to_openai_message_param
|
||||
|
||||
ASTRAFLOW_BASE_URL = "https://api.umodelverse.ai/v1"
|
||||
ASTRAFLOW_CN_BASE_URL = "https://api.modelverse.cn/v1"
|
||||
@@ -70,7 +71,7 @@ class _AstraflowBaseProvider(LLMProvider):
|
||||
try:
|
||||
params: dict[str, Any] = {
|
||||
"model": llm_input.model or self.default_model,
|
||||
"messages": [msg.to_dict() for msg in llm_input.messages],
|
||||
"messages": [to_openai_message_param(msg) for msg in llm_input.messages],
|
||||
}
|
||||
if llm_input.temperature != 1.0:
|
||||
params["temperature"] = llm_input.temperature
|
||||
|
||||
@@ -16,6 +16,7 @@ from llm.core.interface import (
|
||||
)
|
||||
from llm.core.types import LLMInput, LLMOutput, ModelInfo, ProviderType, ToolCall
|
||||
from llm.providers.constants import EMPTY_FILTERED_RESPONSE_ERROR
|
||||
from llm.providers.openai_compat import to_openai_message_param
|
||||
|
||||
ATLAS_BASE_URL = "https://api.atlascloud.ai/v1"
|
||||
DEFAULT_ATLAS_MODEL = "deepseek-ai/deepseek-v4-pro"
|
||||
@@ -83,7 +84,7 @@ class AtlasProvider(LLMProvider):
|
||||
try:
|
||||
params: dict[str, Any] = {
|
||||
"model": llm_input.model or self.default_model,
|
||||
"messages": [msg.to_dict() for msg in llm_input.messages],
|
||||
"messages": [to_openai_message_param(msg) for msg in llm_input.messages],
|
||||
}
|
||||
if llm_input.temperature != 1.0:
|
||||
params["temperature"] = llm_input.temperature
|
||||
|
||||
@@ -22,6 +22,7 @@ from llm.core.types import (
|
||||
ToolCall,
|
||||
)
|
||||
from llm.providers.constants import EMPTY_FILTERED_RESPONSE_ERROR
|
||||
from llm.providers.openai_compat import to_openai_message_param
|
||||
|
||||
|
||||
class OpenAIProvider(LLMProvider):
|
||||
@@ -72,7 +73,7 @@ class OpenAIProvider(LLMProvider):
|
||||
try:
|
||||
params: dict[str, Any] = {
|
||||
"model": input.model or "gpt-4o-mini",
|
||||
"messages": [msg.to_dict() for msg in input.messages],
|
||||
"messages": [to_openai_message_param(msg) for msg in input.messages],
|
||||
"temperature": input.temperature,
|
||||
}
|
||||
if input.max_tokens:
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Message adapters shared by OpenAI-compatible providers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from llm.core.types import Message, Role, ToolCall
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from openai.types.chat import (
|
||||
ChatCompletionAssistantMessageParam,
|
||||
ChatCompletionMessageParam,
|
||||
ChatCompletionMessageToolCallParam,
|
||||
ChatCompletionSystemMessageParam,
|
||||
ChatCompletionToolMessageParam,
|
||||
ChatCompletionUserMessageParam,
|
||||
)
|
||||
|
||||
|
||||
def _to_openai_tool_call_param(
|
||||
tool_call: ToolCall,
|
||||
) -> ChatCompletionMessageToolCallParam:
|
||||
"""Convert a domain tool call to the OpenAI request schema."""
|
||||
try:
|
||||
arguments = json.dumps(tool_call.arguments, allow_nan=False)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError("OpenAI tool call arguments must be valid JSON") from exc
|
||||
|
||||
return {
|
||||
"id": tool_call.id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_call.name,
|
||||
"arguments": arguments,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def to_openai_message_param(message: Message) -> ChatCompletionMessageParam:
|
||||
"""Convert a domain message to the OpenAI chat-completions schema."""
|
||||
if message.role == Role.SYSTEM:
|
||||
system_message: ChatCompletionSystemMessageParam = {
|
||||
"role": "system",
|
||||
"content": message.content,
|
||||
}
|
||||
if message.name:
|
||||
system_message["name"] = message.name
|
||||
return system_message
|
||||
|
||||
if message.role == Role.USER:
|
||||
user_message: ChatCompletionUserMessageParam = {
|
||||
"role": "user",
|
||||
"content": message.content,
|
||||
}
|
||||
if message.name:
|
||||
user_message["name"] = message.name
|
||||
return user_message
|
||||
|
||||
if message.role == Role.ASSISTANT:
|
||||
assistant_message: ChatCompletionAssistantMessageParam = {
|
||||
"role": "assistant",
|
||||
"content": message.content,
|
||||
}
|
||||
if message.name:
|
||||
assistant_message["name"] = message.name
|
||||
if message.tool_calls:
|
||||
assistant_message["tool_calls"] = [
|
||||
_to_openai_tool_call_param(tool_call)
|
||||
for tool_call in message.tool_calls
|
||||
]
|
||||
return assistant_message
|
||||
|
||||
if message.role == Role.TOOL:
|
||||
if not message.tool_call_id:
|
||||
raise ValueError("OpenAI tool messages require a tool_call_id")
|
||||
tool_message: ChatCompletionToolMessageParam = {
|
||||
"role": "tool",
|
||||
"content": message.content,
|
||||
"tool_call_id": message.tool_call_id,
|
||||
}
|
||||
return tool_message
|
||||
|
||||
raise ValueError(f"Unsupported OpenAI message role: {message.role}")
|
||||
@@ -1,11 +1,16 @@
|
||||
import json
|
||||
import urllib.request
|
||||
from collections.abc import Callable
|
||||
from io import BytesIO
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
|
||||
from llm.core.types import LLMInput, Message, Role, ToolDefinition
|
||||
from llm.core.interface import LLMProvider
|
||||
from llm.core.types import LLMInput, Message, Role, ToolCall, ToolDefinition
|
||||
from llm.providers.astraflow import AstraflowProvider
|
||||
from llm.providers.atlas import AtlasProvider
|
||||
from llm.providers.claude import ClaudeProvider
|
||||
from llm.providers.constants import EMPTY_FILTERED_RESPONSE_ERROR
|
||||
from llm.providers.ollama import OllamaProvider
|
||||
@@ -88,6 +93,94 @@ def test_openai_provider_serializes_tools_for_chat_completions():
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
"provider_type",
|
||||
[OpenAIProvider, AstraflowProvider, AtlasProvider],
|
||||
)
|
||||
def test_openai_compatible_provider_serializes_assistant_tool_calls(
|
||||
provider_type: Callable[..., LLMProvider],
|
||||
) -> None:
|
||||
provider = cast(Any, provider_type(api_key="test"))
|
||||
client = _OpenAIClient()
|
||||
provider.client = client
|
||||
|
||||
provider.generate(
|
||||
LLMInput(
|
||||
messages=[
|
||||
Message(role=Role.USER, content="Find ECC"),
|
||||
Message(
|
||||
role=Role.ASSISTANT,
|
||||
content="",
|
||||
tool_calls=[
|
||||
ToolCall(
|
||||
id="call_1",
|
||||
name="search",
|
||||
arguments={"query": "ecc"},
|
||||
)
|
||||
],
|
||||
),
|
||||
Message(
|
||||
role=Role.TOOL,
|
||||
content="result",
|
||||
tool_call_id="call_1",
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
assert client.completions.params is not None
|
||||
assert client.completions.params["messages"] == [
|
||||
{"role": "user", "content": "Find ECC"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_1",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "search",
|
||||
"arguments": '{"query": "ecc"}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{"role": "tool", "content": "result", "tool_call_id": "call_1"},
|
||||
]
|
||||
|
||||
submitted_params = client.completions.params
|
||||
with pytest.raises(ValueError, match="tool messages require a tool_call_id"):
|
||||
provider.generate(
|
||||
LLMInput(messages=[Message(role=Role.TOOL, content="unlinked result")])
|
||||
)
|
||||
assert client.completions.params is submitted_params
|
||||
|
||||
for invalid_arguments in ({"score": float("nan")}, {"value": object()}):
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="tool call arguments must be valid JSON",
|
||||
):
|
||||
provider.generate(
|
||||
LLMInput(
|
||||
messages=[
|
||||
Message(
|
||||
role=Role.ASSISTANT,
|
||||
content="",
|
||||
tool_calls=[
|
||||
ToolCall(
|
||||
id="call_invalid",
|
||||
name="score",
|
||||
arguments=invalid_arguments,
|
||||
)
|
||||
],
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
assert client.completions.params is submitted_params
|
||||
|
||||
|
||||
def test_openai_provider_can_be_constructed_without_credentials(monkeypatch):
|
||||
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user