Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions gateway/platforms/api_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,7 @@ def _create_agent(
self,
ephemeral_system_prompt: Optional[str] = None,
session_id: Optional[str] = None,
model_override: Optional[str] = None,
stream_delta_callback=None,
tool_progress_callback=None,
tool_start_callback=None,
Expand Down Expand Up @@ -879,7 +880,7 @@ def _create_agent(

runtime_kwargs = _resolve_runtime_agent_kwargs()
reasoning_config = GatewayRunner._load_reasoning_config()
model = _resolve_gateway_model()
model = model_override or _resolve_gateway_model()

user_config = _load_gateway_config()
enabled_toolsets = sorted(_get_platform_tools(user_config, "api_server"))
Expand Down Expand Up @@ -911,6 +912,24 @@ def _create_agent(
)
return agent

def _resolve_chat_completion_model(self, body: dict[str, Any]) -> str:
"""Resolve the model for a Chat Completions request.

Request-level ``model`` must take precedence for the OpenAI-compatible
API. If it is absent, fall back to the configured Hermes default. Do not
create a Codex agent with an empty model because the provider then fails
with a less actionable error.
"""
request_model = str(body.get("model") or "").strip()
if request_model:
return request_model
try:
from gateway.run import _resolve_gateway_model

return str(_resolve_gateway_model() or "").strip()
except Exception:
return ""

# ------------------------------------------------------------------
# HTTP Handlers
# ------------------------------------------------------------------
Expand Down Expand Up @@ -1135,7 +1154,16 @@ async def _handle_chat_completions(self, request: "web.Request") -> "web.Respons
# history already set from request body above

completion_id = f"chatcmpl-{uuid.uuid4().hex[:29]}"
model_name = body.get("model", self._model_name)
model_name = self._resolve_chat_completion_model(body)
if not model_name:
return web.json_response(
_openai_error(
"Missing model. Provide request.model or configure Hermes model.default.",
param="model",
code="missing_model",
),
status=400,
)
created = int(time.time())

if stream:
Expand Down Expand Up @@ -1215,6 +1243,7 @@ def _on_tool_complete(tool_call_id, function_name, function_args, function_resul
conversation_history=history,
ephemeral_system_prompt=system_prompt,
session_id=session_id,
model_name=model_name,
stream_delta_callback=_on_delta,
tool_start_callback=_on_tool_start,
tool_complete_callback=_on_tool_complete,
Expand All @@ -1238,6 +1267,7 @@ async def _compute_completion():
conversation_history=history,
ephemeral_system_prompt=system_prompt,
session_id=session_id,
model_name=model_name,
gateway_session_key=gateway_session_key,
)

Expand Down Expand Up @@ -2737,6 +2767,7 @@ async def _run_agent(
conversation_history: List[Dict[str, str]],
ephemeral_system_prompt: Optional[str] = None,
session_id: Optional[str] = None,
model_name: Optional[str] = None,
stream_delta_callback=None,
tool_progress_callback=None,
tool_start_callback=None,
Expand All @@ -2761,6 +2792,7 @@ def _run():
agent = self._create_agent(
ephemeral_system_prompt=ephemeral_system_prompt,
session_id=session_id,
model_override=model_name,
stream_delta_callback=stream_delta_callback,
tool_progress_callback=tool_progress_callback,
tool_start_callback=tool_start_callback,
Expand Down
41 changes: 41 additions & 0 deletions tests/gateway/test_api_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,47 @@ async def test_empty_messages_returns_400(self, adapter):
resp = await cli.post("/v1/chat/completions", json={"model": "test", "messages": []})
assert resp.status == 400

def test_resolve_chat_completion_model_prefers_request_model(self, adapter):
with patch("gateway.run._resolve_gateway_model", return_value="configured-default"):
model = adapter._resolve_chat_completion_model({"model": "gpt-5.3-codex"})

assert model == "gpt-5.3-codex"

@pytest.mark.asyncio
async def test_missing_request_model_without_config_returns_400(self, adapter):
app = _create_app(adapter)
with patch("gateway.run._resolve_gateway_model", return_value=""):
async with TestClient(TestServer(app)) as cli:
resp = await cli.post(
"/v1/chat/completions",
json={"messages": [{"role": "user", "content": "Hello"}]},
)
data = await resp.json()

assert resp.status == 400
assert data["error"]["code"] == "missing_model"
assert "request.model" in data["error"]["message"]

@pytest.mark.asyncio
async def test_missing_request_model_uses_configured_default(self, adapter):
mock_result = {"final_response": "ok", "messages": [], "api_calls": 1}
app = _create_app(adapter)
with (
patch("gateway.run._resolve_gateway_model", return_value="gpt-5.3-codex"),
patch.object(adapter, "_run_agent", new_callable=AsyncMock) as mock_run,
):
mock_run.return_value = (mock_result, {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0})
async with TestClient(TestServer(app)) as cli:
resp = await cli.post(
"/v1/chat/completions",
json={"messages": [{"role": "user", "content": "Hello"}]},
)
data = await resp.json()

assert resp.status == 200
assert data["model"] == "gpt-5.3-codex"
assert mock_run.call_args.kwargs["model_name"] == "gpt-5.3-codex"

@pytest.mark.asyncio
async def test_stream_true_returns_sse(self, adapter):
"""stream=true returns SSE format with the full response."""
Expand Down