from pathlib import Path import sys from unittest.mock import patch import httpx import pytest sys.path.insert(0, str(Path(__file__).resolve().parents[2])) with patch( "pydantic_settings.sources.providers.dotenv.DotEnvSettingsSource._read_env_files", return_value={}, ): from aiprovider.provider_service import ProviderService from aiprovider.schemas import SituationalAnalysisRequest from app.api.v1.settings import _runtime_config_from_ai_payload def test_custom_openai_gateway_keeps_its_configured_protocol(): config = _runtime_config_from_ai_payload( { "default_provider": "openai", "providers": { "openai": { "base_url": "https://gateway.test/v1", "provider_api": "openai-completions", "model": "gpt-6-astra", }, }, } )["llm_config"] service = ProviderService({**config, "api_key": "test-key"}) assert service._resolve_model_provider_api("gpt-6-astra") == "openai-completions" @pytest.mark.asyncio @pytest.mark.parametrize( "model,path", [("minimax-m3", "/messages"), ("qwen3.8-max", "/messages"), ("gpt-5.6-luna", "/responses")], ) async def test_runtime_model_routes_use_current_protocols(monkeypatch, model, path): llm = _runtime_config_from_ai_payload( { "default_provider": "opencode-go", "providers": { "opencode-go": { "model": model, "api_key": "test-key", "model_provider_apis": {model: "openai-completions"}, } }, } )["llm_config"] service = ProviderService(llm) seen = [] async def post(**request): seen.append(request) assert request["path"] == path assert request["request_body"]["model"] == model if path == "/responses": assert request["request_body"]["store"] is False assert "max_tokens" not in request["request_body"] return { "output": [ {"type": "reasoning", "summary": [{"text": "reason"}]}, {"type": "message", "content": [{"type": "output_text", "text": "OK"}]}, ] } return {"content": [{"type": "text", "text": "OK"}]} monkeypatch.setattr(service, "_post", post) result = await service.analyze(SituationalAnalysisRequest(title="test", objective="reply OK")) assert len(seen) == 1 assert result.content == "OK" @pytest.mark.asyncio async def test_minimax_messages_url_accepts_both_documented_base_forms(monkeypatch): seen = [] client_type = httpx.AsyncClient def handle(request): seen.append(str(request.url)) return httpx.Response(200, json={"content": [{"type": "text", "text": "OK"}]}) transport = httpx.MockTransport(handle) monkeypatch.setattr(httpx, "AsyncClient", lambda **kw: client_type(transport=transport, **kw)) for base in ["https://api.minimaxi.com/anthropic", "https://api.minimaxi.com/anthropic/v1"]: service = ProviderService( { "provider": "minimax", "provider_api": "anthropic-messages", "base_url": base, "api_key": "test-key", "model": "MiniMax-M3", } ) result = await service.analyze( SituationalAnalysisRequest(title="test", objective="reply OK") ) assert result.content == "OK" assert seen == ["https://api.minimaxi.com/anthropic/v1/messages"] * 2 @pytest.mark.asyncio async def test_m3_playground_thinking_and_opencode_session_headers(monkeypatch): seen = [] client_type = httpx.AsyncClient def handle(request): import json seen.append(request) assert json.loads(request.content)["thinking"] == {"type": "adaptive"} assert request.headers["user-agent"] == "Planet/1.0" return httpx.Response(200, json={"content": [{"type": "text", "text": "OK"}]}) transport = httpx.MockTransport(handle) monkeypatch.setattr(httpx, "AsyncClient", lambda **kw: client_type(transport=transport, **kw)) for session_id in ["conversation-one", "conversation-one", "conversation-two"]: service = ProviderService( { "provider": "opencode-go", "provider_api": "anthropic-messages", "base_url": "https://opencode.ai/zen/go/v1", "api_key": "test-key", "model": "minimax-m3", } ) await service.analyze( SituationalAnalysisRequest( title="test", objective="reply OK", thinking={"type": "enabled"}, context={"session_id": session_id}, ) ) session_headers = [request.headers["x-opencode-session"] for request in seen] assert session_headers[0] == session_headers[1] assert session_headers[0] != session_headers[2]