feat: support custom Responses endpoints

This commit is contained in:
fanziqing
2026-08-14 16:53:50 +08:00
parent 9702c86c7f
commit d0103947c3
2 changed files with 50 additions and 7 deletions
+13 -7
View File
@@ -1,4 +1,4 @@
"""OpenAI Responses provider — native OpenAI models via `/v1/responses`.
"""OpenAI Responses provider — native and compatible models via `/responses`.
Chat Completions rejects function tools combined with any `reasoning_effort` other than
`none` on GPT-5.6+ ("use /v1/responses"), which had reasoning pinned OFF for native OpenAI
@@ -8,9 +8,10 @@ reasoning + tools at real effort levels, streamed reasoning summaries (→ the s
chain-of-thought continuity across tool round-trips via `store: false` +
`include: ["reasoning.encrypted_content"]` — nothing retained server-side.
Routing: the `openai` provider entry with NO custom base_url builds this class; a custom
endpoint (Azure, vLLM, any OpenAI-compatible gateway) and every compat vendor keep the
Chat Completions `OpenAIProvider` (registry.py).
Routing: the `openai` provider entry with NO custom base_url builds this class. Most custom
endpoints (Azure, vLLM, and the existing compat vendors) keep the Chat Completions
`OpenAIProvider`; vendors that explicitly implement the Responses wire can opt into this
class with their own base URL (registry.py).
Like the other native providers, this is mostly a pair of pure converters from the
canonical OpenAI-chat-shaped history to Responses `input` items. What the converters
@@ -289,14 +290,16 @@ class OpenAIResponsesProvider(ProviderClient):
default_model: str = "gpt-5.6-sol",
api_key: Optional[str] = None,
secrets: Any = None,
base_url: Optional[str] = None,
):
# Same deferred-client contract as OpenAIProvider: built lazily so an engine can be
# assembled before any key exists; key resolves at call time (explicit → env →
# SecretStore). Tests inject a `client` directly. No base_url — a custom endpoint
# routes to the Chat Completions provider instead (registry.py).
# SecretStore). Tests inject a `client` directly. `base_url` is opt-in: stock OpenAI
# leaves it unset, while Responses-compatible vendors can supply their own endpoint.
self._client = client
self._api_key = api_key
self._secrets = secrets
self._base_url = (base_url or "").strip().rstrip("/") or None
self.default_model = default_model
def _ensure_client(self) -> Any:
@@ -310,7 +313,10 @@ class OpenAIResponsesProvider(ProviderClient):
"No model API key configured. Set OPENAI_API_KEY in the environment, "
"or add your key in Manage → Settings."
)
self._client = OpenAI(api_key=key)
kwargs = {"api_key": key}
if self._base_url:
kwargs["base_url"] = self._base_url
self._client = OpenAI(**kwargs)
return self._client
def _request_kwargs(
+37
View File
@@ -17,6 +17,43 @@ from coworker.providers.openai_responses import (
convert_tools,
)
def test_responses_custom_base_url_reaches_sdk(monkeypatch):
captured: dict = {}
def fake_openai(**kwargs):
captured.update(kwargs)
return SimpleNamespace()
monkeypatch.setattr("openai.OpenAI", fake_openai)
provider = OpenAIResponsesProvider(
api_key="ark-key",
base_url="https://ark.example/api/v3/",
)
provider._ensure_client()
assert captured == {
"api_key": "ark-key",
"base_url": "https://ark.example/api/v3",
}
def test_stock_openai_responses_path_unchanged(monkeypatch):
"""Lockdown: stock OpenAI must not receive a vendor base URL."""
captured: dict = {}
def fake_openai(**kwargs):
captured.update(kwargs)
return SimpleNamespace()
monkeypatch.setattr("openai.OpenAI", fake_openai)
provider = OpenAIResponsesProvider(api_key="openai-key")
provider._ensure_client()
assert captured == {"api_key": "openai-key"}
# -- fakes ------------------------------------------------------------------------