mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-06 17:38:35 +00:00
Merge remote-tracking branch 'origin/main' into pr-3379
This commit is contained in:
commit
79247545ac
@ -446,10 +446,11 @@ class OpenAICompatProvider(LLMProvider):
|
|||||||
reasoning_effort: str | None,
|
reasoning_effort: str | None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Use Responses API only for direct OpenAI requests that benefit from it."""
|
"""Use Responses API only for direct OpenAI requests that benefit from it."""
|
||||||
if self._spec and self._spec.name != "openai":
|
if self._spec and self._spec.name not in ("openai", "github_copilot"):
|
||||||
return False
|
|
||||||
if not _is_direct_openai_base(self._effective_base):
|
|
||||||
return False
|
return False
|
||||||
|
if self._spec is None or self._spec.name != "github_copilot":
|
||||||
|
if not _is_direct_openai_base(self._effective_base):
|
||||||
|
return False
|
||||||
|
|
||||||
model_name = (model or self.default_model).lower()
|
model_name = (model or self.default_model).lower()
|
||||||
wants = False
|
wants = False
|
||||||
@ -527,6 +528,8 @@ class OpenAICompatProvider(LLMProvider):
|
|||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Build a Responses API body for direct OpenAI requests."""
|
"""Build a Responses API body for direct OpenAI requests."""
|
||||||
model_name = model or self.default_model
|
model_name = model or self.default_model
|
||||||
|
if self._spec and self._spec.strip_model_prefix:
|
||||||
|
model_name = model_name.split("/")[-1]
|
||||||
sanitized_messages = self._sanitize_messages(self._sanitize_empty_content(messages))
|
sanitized_messages = self._sanitize_messages(self._sanitize_empty_content(messages))
|
||||||
instructions, input_items = convert_messages(sanitized_messages)
|
instructions, input_items = convert_messages(sanitized_messages)
|
||||||
|
|
||||||
@ -987,6 +990,11 @@ class OpenAICompatProvider(LLMProvider):
|
|||||||
self._record_responses_success(model, reasoning_effort)
|
self._record_responses_success(model, reasoning_effort)
|
||||||
return result
|
return result
|
||||||
except Exception as responses_error:
|
except Exception as responses_error:
|
||||||
|
if self._spec and self._spec.name == "github_copilot":
|
||||||
|
# Copilot gateway exposes GPT-5/o-series only via /responses;
|
||||||
|
# falling back to /chat/completions cannot succeed and would
|
||||||
|
# hide the real error.
|
||||||
|
raise
|
||||||
if not self._should_fallback_from_responses_error(responses_error):
|
if not self._should_fallback_from_responses_error(responses_error):
|
||||||
raise
|
raise
|
||||||
self._record_responses_failure(model, reasoning_effort)
|
self._record_responses_failure(model, reasoning_effort)
|
||||||
@ -1045,6 +1053,11 @@ class OpenAICompatProvider(LLMProvider):
|
|||||||
reasoning_content=reasoning_content,
|
reasoning_content=reasoning_content,
|
||||||
)
|
)
|
||||||
except Exception as responses_error:
|
except Exception as responses_error:
|
||||||
|
if self._spec and self._spec.name == "github_copilot":
|
||||||
|
# Copilot gateway exposes GPT-5/o-series only via /responses;
|
||||||
|
# falling back to /chat/completions cannot succeed and would
|
||||||
|
# hide the real error.
|
||||||
|
raise
|
||||||
if not self._should_fallback_from_responses_error(responses_error):
|
if not self._should_fallback_from_responses_error(responses_error):
|
||||||
raise
|
raise
|
||||||
self._record_responses_failure(model, reasoning_effort)
|
self._record_responses_failure(model, reasoning_effort)
|
||||||
|
|||||||
@ -223,6 +223,7 @@ PROVIDERS: tuple[ProviderSpec, ...] = (
|
|||||||
default_api_base="https://api.githubcopilot.com",
|
default_api_base="https://api.githubcopilot.com",
|
||||||
strip_model_prefix=True,
|
strip_model_prefix=True,
|
||||||
is_oauth=True,
|
is_oauth=True,
|
||||||
|
supports_max_completion_tokens=True,
|
||||||
),
|
),
|
||||||
# DeepSeek: OpenAI-compatible at api.deepseek.com
|
# DeepSeek: OpenAI-compatible at api.deepseek.com
|
||||||
ProviderSpec(
|
ProviderSpec(
|
||||||
|
|||||||
@ -421,13 +421,13 @@ async def test_github_copilot_provider_refreshes_client_api_key_before_chat():
|
|||||||
})
|
})
|
||||||
|
|
||||||
with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI", return_value=mock_client):
|
with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI", return_value=mock_client):
|
||||||
provider = GitHubCopilotProvider(default_model="github-copilot/gpt-5.1")
|
provider = GitHubCopilotProvider(default_model="github-copilot/gpt-4")
|
||||||
|
|
||||||
provider._get_copilot_access_token = AsyncMock(return_value="copilot-access-token")
|
provider._get_copilot_access_token = AsyncMock(return_value="copilot-access-token")
|
||||||
|
|
||||||
response = await provider.chat(
|
response = await provider.chat(
|
||||||
messages=[{"role": "user", "content": "hi"}],
|
messages=[{"role": "user", "content": "hi"}],
|
||||||
model="github-copilot/gpt-5.1",
|
model="github-copilot/gpt-4",
|
||||||
max_tokens=16,
|
max_tokens=16,
|
||||||
temperature=0.1,
|
temperature=0.1,
|
||||||
)
|
)
|
||||||
|
|||||||
79
tests/providers/test_github_copilot_routing.py
Normal file
79
tests/providers/test_github_copilot_routing.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
"""Regression tests for GitHub Copilot /responses routing.
|
||||||
|
|
||||||
|
Covers the Copilot-specific branches added to route GPT-5 / o-series models
|
||||||
|
through the /responses endpoint without falling back to /chat/completions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from nanobot.providers.openai_compat_provider import OpenAICompatProvider
|
||||||
|
from nanobot.providers.registry import find_by_name
|
||||||
|
|
||||||
|
|
||||||
|
def _make_copilot_provider() -> OpenAICompatProvider:
|
||||||
|
"""Build a bare provider with the real github_copilot spec (no network)."""
|
||||||
|
p = OpenAICompatProvider.__new__(OpenAICompatProvider)
|
||||||
|
p.default_model = "github_copilot/gpt-5.4-mini"
|
||||||
|
p._spec = find_by_name("github_copilot")
|
||||||
|
p._effective_base = "https://api.githubcopilot.com"
|
||||||
|
p._responses_failures = {}
|
||||||
|
p._responses_tripped_at = {}
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_use_responses_api_allows_github_copilot_non_openai_base():
|
||||||
|
"""github_copilot bypasses the direct-OpenAI base check and still opts in for GPT-5."""
|
||||||
|
provider = _make_copilot_provider()
|
||||||
|
assert provider._should_use_responses_api("github_copilot/gpt-5.4-mini", None) is True
|
||||||
|
assert provider._should_use_responses_api("github_copilot/o3", None) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_responses_body_strips_github_copilot_prefix():
|
||||||
|
"""/responses body must send the bare model name; gateway rejects routing prefixes."""
|
||||||
|
provider = _make_copilot_provider()
|
||||||
|
body = provider._build_responses_body(
|
||||||
|
messages=[{"role": "user", "content": "hi"}],
|
||||||
|
tools=None,
|
||||||
|
model="github_copilot/gpt-5.4-mini",
|
||||||
|
max_tokens=16,
|
||||||
|
temperature=0.1,
|
||||||
|
reasoning_effort=None,
|
||||||
|
tool_choice=None,
|
||||||
|
)
|
||||||
|
assert body["model"] == "gpt-5.4-mini"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_github_copilot_does_not_fall_back_from_responses_error():
|
||||||
|
"""On /responses failure, github_copilot must re-raise instead of hitting /chat/completions."""
|
||||||
|
from nanobot.providers.github_copilot_provider import GitHubCopilotProvider
|
||||||
|
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client.api_key = "no-key"
|
||||||
|
|
||||||
|
class _CompatError(Exception):
|
||||||
|
"""Looks like a fallback-eligible error on other providers."""
|
||||||
|
status_code = 400
|
||||||
|
body = "Unsupported parameter responses api"
|
||||||
|
|
||||||
|
mock_client.responses.create = AsyncMock(side_effect=_CompatError("boom"))
|
||||||
|
mock_client.chat.completions.create = AsyncMock()
|
||||||
|
|
||||||
|
with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI", return_value=mock_client):
|
||||||
|
provider = GitHubCopilotProvider(default_model="github_copilot/gpt-5.4-mini")
|
||||||
|
provider._get_copilot_access_token = AsyncMock(return_value="copilot-access-token")
|
||||||
|
|
||||||
|
response = await provider.chat(
|
||||||
|
messages=[{"role": "user", "content": "hi"}],
|
||||||
|
model="github_copilot/gpt-5.4-mini",
|
||||||
|
max_tokens=16,
|
||||||
|
temperature=0.1,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.finish_reason == "error"
|
||||||
|
mock_client.responses.create.assert_awaited_once()
|
||||||
|
mock_client.chat.completions.create.assert_not_awaited()
|
||||||
Loading…
x
Reference in New Issue
Block a user