merge: preserve tested tool-choice provider source 2950

This commit is contained in:
haelyra
2026-10-09 16:07:23 -04:00
5 changed files with 184 additions and 4 deletions
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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:
+84
View File
@@ -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}")
+94 -1
View File
@@ -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)