List validated chat model providers
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from unittest.mock import patch
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from backend.api.server import AgentState, _normalize_model_list, create_app
|
||||
|
||||
|
||||
class ModelListTests(unittest.TestCase):
|
||||
def test_normalize_model_list_uses_provider_prefixed_llm_ids(self) -> None:
|
||||
payload = {
|
||||
'data': [
|
||||
{
|
||||
'id': 'ASR_NonStreaming',
|
||||
'object': 'model',
|
||||
'owned_by': 'xiaomi',
|
||||
'model_type': 'speech2text',
|
||||
},
|
||||
{
|
||||
'id': 'DeepSeek-R1-0528',
|
||||
'object': 'model',
|
||||
'owned_by': 'xiaomi',
|
||||
'model_type': 'llm',
|
||||
},
|
||||
{
|
||||
'id': 'gpt-5',
|
||||
'object': 'model',
|
||||
'owned_by': 'azure_openai',
|
||||
'model_type': 'llm',
|
||||
},
|
||||
{
|
||||
'id': 'gpt-4o-audio-preview',
|
||||
'object': 'model',
|
||||
'owned_by': 'azure_openai',
|
||||
'model_type': 'llm',
|
||||
},
|
||||
{
|
||||
'id': 'embedding-v1',
|
||||
'object': 'model',
|
||||
'owned_by': 'example',
|
||||
'model_type': 'text-embedding',
|
||||
},
|
||||
{
|
||||
'id': 'ernie-4.0-turbo-128k',
|
||||
'object': 'model',
|
||||
'owned_by': 'baidu_qianfan',
|
||||
'model_type': 'llm',
|
||||
},
|
||||
{
|
||||
'id': 'Pro/deepseek-ai/DeepSeek-V3',
|
||||
'object': 'model',
|
||||
'owned_by': 'siliconflow',
|
||||
'model_type': 'llm',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
models = _normalize_model_list(payload)
|
||||
|
||||
self.assertEqual(
|
||||
[model['id'] for model in models],
|
||||
[
|
||||
'azure_openai/gpt-5',
|
||||
'siliconflow/Pro/deepseek-ai/DeepSeek-V3',
|
||||
'xiaomi/DeepSeek-R1-0528',
|
||||
],
|
||||
)
|
||||
self.assertEqual(models[0]['provider'], 'azure_openai')
|
||||
self.assertEqual(models[0]['model_type'], 'llm')
|
||||
|
||||
def test_models_endpoint_returns_provider_prefixed_llm_models(self) -> None:
|
||||
class FakeResponse:
|
||||
def __enter__(self) -> 'FakeResponse':
|
||||
return self
|
||||
|
||||
def __exit__(self, *args: object) -> None:
|
||||
return None
|
||||
|
||||
def read(self) -> bytes:
|
||||
return (
|
||||
b'{"object":"list","data":['
|
||||
b'{"id":"mimo-v2-flash","object":"model","owned_by":"xiaomi","model_type":"llm"},'
|
||||
b'{"id":"ASR_Streaming","object":"model","owned_by":"xiaomi","model_type":"speech2text"},'
|
||||
b'{"id":"gpt-5","object":"model","owned_by":"azure_openai","model_type":"llm"}'
|
||||
b']}'
|
||||
)
|
||||
|
||||
with TemporaryDirectory() as tmp_dir:
|
||||
state = AgentState(
|
||||
cwd=Path(tmp_dir),
|
||||
model='xiaomi/mimo-v2-flash',
|
||||
base_url='http://model.example/v1',
|
||||
api_key='token',
|
||||
allow_shell=False,
|
||||
allow_write=False,
|
||||
session_directory=Path(tmp_dir) / 'sessions',
|
||||
)
|
||||
client = TestClient(create_app(state))
|
||||
with patch('backend.api.server.request.urlopen', return_value=FakeResponse()):
|
||||
response = client.get('/api/models')
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
payload = response.json()
|
||||
self.assertEqual(payload['raw_count'], 3)
|
||||
self.assertEqual(payload['filtered_count'], 2)
|
||||
self.assertEqual(
|
||||
[model['id'] for model in payload['models']],
|
||||
['azure_openai/gpt-5', 'xiaomi/mimo-v2-flash'],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import unittest
|
||||
|
||||
from src.openai_compat import (
|
||||
OpenAICompatClient,
|
||||
OpenAICompatError,
|
||||
_build_response_format,
|
||||
_join_url,
|
||||
@@ -10,8 +11,9 @@ from src.openai_compat import (
|
||||
_optional_int,
|
||||
_parse_tool_arguments,
|
||||
_parse_usage,
|
||||
_temperature_for_model,
|
||||
)
|
||||
from src.agent_types import OutputSchemaConfig, UsageStats
|
||||
from src.agent_types import ModelConfig, OutputSchemaConfig, UsageStats
|
||||
|
||||
|
||||
class TestJoinUrl(unittest.TestCase):
|
||||
@@ -163,5 +165,50 @@ class TestOptionalInt(unittest.TestCase):
|
||||
self.assertEqual(_optional_int('abc'), 0)
|
||||
|
||||
|
||||
class TestProviderTemperatureFloor(unittest.TestCase):
|
||||
def test_minimax_temperature_is_clamped_to_provider_minimum(self):
|
||||
self.assertEqual(_temperature_for_model('minimax/MiniMax-M2.5', 0.0), 0.01)
|
||||
|
||||
def test_wenxin_temperature_is_clamped_to_provider_minimum(self):
|
||||
self.assertEqual(_temperature_for_model('wenxin/ernie-4.0-turbo-128k', 0.0), 0.1)
|
||||
|
||||
def test_regular_models_keep_configured_temperature(self):
|
||||
self.assertEqual(_temperature_for_model('xiaomi/mimo-v2-flash', 0.0), 0.0)
|
||||
|
||||
def test_payload_uses_temperature_floor(self):
|
||||
client = OpenAICompatClient(
|
||||
ModelConfig(model='minimax/MiniMax-M2.5', temperature=0.0)
|
||||
)
|
||||
payload = client._build_payload( # noqa: SLF001 - verify payload compatibility.
|
||||
messages=[{'role': 'user', 'content': 'hi'}],
|
||||
tools=[],
|
||||
stream=False,
|
||||
output_schema=None,
|
||||
)
|
||||
self.assertEqual(payload['temperature'], 0.01)
|
||||
|
||||
def test_payload_omits_tool_choice_when_no_tools_are_supplied(self):
|
||||
client = OpenAICompatClient(ModelConfig(model='azure_openai/gpt-4o-mini'))
|
||||
payload = client._build_payload( # noqa: SLF001 - verify provider compatibility.
|
||||
messages=[{'role': 'user', 'content': 'hi'}],
|
||||
tools=[],
|
||||
stream=False,
|
||||
output_schema=None,
|
||||
)
|
||||
self.assertNotIn('tools', payload)
|
||||
self.assertNotIn('tool_choice', payload)
|
||||
|
||||
def test_payload_includes_tool_choice_when_tools_are_supplied(self):
|
||||
client = OpenAICompatClient(ModelConfig(model='azure_openai/gpt-4o-mini'))
|
||||
payload = client._build_payload( # noqa: SLF001 - verify provider compatibility.
|
||||
messages=[{'role': 'user', 'content': 'hi'}],
|
||||
tools=[{'type': 'function', 'function': {'name': 'noop', 'parameters': {}}}],
|
||||
stream=False,
|
||||
output_schema=None,
|
||||
)
|
||||
self.assertIn('tools', payload)
|
||||
self.assertEqual(payload['tool_choice'], 'auto')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user