Default model transports to Chat Completions

Route OpenAI, Azure, GitHub Copilot, and GitHub Copilot OAuth explicitly through Chat Completions. Keep Codex/ChatGPT and xAI Grok OAuth explicitly on Responses.

Make Chat Completions the LiteLLM transport default for omitted, blank, default, auto, or unknown modes so new providers avoid unsupported Responses endpoints unless they explicitly opt in.
This commit is contained in:
Alessandro 2026-08-27 12:40:13 +02:00
parent dcf5c1500e
commit dac15f77a3
9 changed files with 49 additions and 52 deletions

View file

@ -16,7 +16,7 @@
- Do not commit API keys, provider secrets, local account identifiers, or private endpoints.
- Keep provider IDs and settings keys stable unless all loaders, UI references, migrations, and tests are updated.
- Defaults must work in a clean checkout and in Docker.
- Providers without a native Responses path in the supported LiteLLM runtime, or intentionally standardized on Chat Completions, must set `a0_api_mode: chat`; native Responses providers rely on the Responses default.
- Chat Completions is the transport default; providers intentionally using Responses must set `a0_api_mode: responses` explicitly.
- Templates must avoid accidentally unignoring private runtime content.
## Work Guidance

View file

@ -71,6 +71,7 @@ chat:
name: GitHub Copilot
litellm_provider: github_copilot
kwargs:
a0_api_mode: chat
extra_headers:
"Editor-Version": "vscode/1.85.1"
"Copilot-Integration-Id": "vscode-chat"
@ -180,6 +181,8 @@ chat:
litellm_provider: openai
models_list:
endpoint_url: "https://api.openai.com/v1/models"
kwargs:
a0_api_mode: chat
azure:
name: OpenAI Azure
litellm_provider: azure
@ -187,6 +190,8 @@ chat:
endpoint_url: "/openai/models"
params:
api-version: "2024-10-21"
kwargs:
a0_api_mode: chat
bedrock:
name: AWS Bedrock
litellm_provider: bedrock

View file

@ -35,14 +35,7 @@ class TransportRecovery(Enum):
FALLBACK_TO_CHAT = "fallback_to_chat"
CHAT_COMPLETIONS_ALIASES = {
"chat",
"chat_completion",
"chat_completions",
"completion",
"completions",
}
RESPONSES_ALIASES = {"", "auto", "default", "response", "responses", "responses_api"}
RESPONSES_ALIASES = {"response", "responses", "responses_api"}
RESPONSES_REASONING_EFFORTS = {"minimal", "low", "medium", "high"}
RESPONSES_REASONING_FALLBACK_EFFORT = "high"
NO_REASONING_EFFORT_ALIASES = {"", "0", "false", "no", "none", "off", "disabled"}
@ -146,12 +139,10 @@ class TransportPolicy:
@staticmethod
def _pop_mode(kwargs: dict[str, Any]) -> TransportMode:
value = str(kwargs.pop("a0_api_mode", "responses") or "").lower().strip()
if value in CHAT_COMPLETIONS_ALIASES:
return TransportMode.CHAT_COMPLETIONS
value = str(kwargs.pop("a0_api_mode", "") or "").lower().strip()
if value in RESPONSES_ALIASES:
return TransportMode.RESPONSES
return TransportMode.RESPONSES
return TransportMode.CHAT_COMPLETIONS
@property
def using_responses(self) -> bool:

View file

@ -27,7 +27,7 @@
- Do not send orphan tool controls when no tools are present; strict OpenAI-compatible servers can reject empty `tools` arrays.
- When Agent Zero function tools are present, default Responses requests to one required native call; explicit request-level `tool_choice` and `parallel_tool_calls` values still win.
- Normalize function tool parameter schemas with an explicit object `properties` field before Responses requests so OpenAI-compatible chat backends reached through LiteLLM can validate them.
- Prefer Responses API when configured, but fallback to Chat Completions when the provider does not support Responses.
- Default to Chat Completions; use Responses only when `a0_api_mode` explicitly selects it, with fallback to Chat Completions when unsupported.
- Fall back to Chat Completions when a Responses request is rejected before any output by an endpoint-specific or shape-specific Bad Request indicating the provider cannot parse Responses payloads.
- Treat opaque type-discrimination errors such as `cannot determine type` from OpenAI-compatible Responses endpoints as shape-specific rejections.
- Fall back to Chat Completions when a Responses endpoint fails before output with an endpoint-specific server error, proxy path-unavailable error, or LiteLLM proxy-extra import error.

View file

@ -48,7 +48,7 @@
- Codex Responses proxy defaults for reasoning effort, reasoning summary, and text verbosity come from the `codex` plugin config; explicit native request values take precedence.
- Codex request shaping tightens an already-advertised native `response` tool to a strict required `text` schema; it must not add tools omitted by the framework tool policy.
- Non-streaming Codex proxy responses must retain completed SSE output items when the final `response.completed` envelope omits them.
- OAuth providers without upstream Responses support must set `a0_api_mode: chat`; native Responses providers rely on the default, since a local proxy route alone does not prove upstream support.
- OAuth providers intentionally using Responses must set `a0_api_mode: responses`; all others inherit the Chat Completions default, since a local proxy route alone does not prove upstream support.
## Work Guidance

View file

@ -6,6 +6,7 @@ chat:
models_list:
endpoint_url: "/models"
kwargs:
a0_api_mode: responses
api_base: "http://127.0.0.1/oauth/codex/v1"
github_copilot_oauth:
name: GitHub Copilot Account
@ -14,6 +15,7 @@ chat:
models_list:
endpoint_url: "/models"
kwargs:
a0_api_mode: chat
api_base: "http://127.0.0.1/oauth/github-copilot/v1"
gemini_api_oauth:
name: Google Cloud Gemini Account
@ -31,4 +33,5 @@ chat:
models_list:
endpoint_url: "/models"
kwargs:
a0_api_mode: responses
api_base: "http://127.0.0.1/oauth/xai-grok/v1"

View file

@ -531,39 +531,19 @@ def test_provider_api_mode_defaults_use_intended_transport():
).read_text(encoding="utf-8")
)
chat_providers = (
"anthropic",
"cometapi",
"deepseek",
"google",
"groq",
"huggingface",
"mistral",
"moonshot",
"nebius",
"nvidia_nim",
"bedrock",
"openrouter",
"sambanova",
"xai",
"zai",
"zai_coding",
)
responses_providers = ("azure", "github_copilot", "openai")
for provider in provider_config["chat"].values():
assert provider.get("kwargs", {}).get("a0_api_mode", "chat") == "chat"
for provider in chat_providers:
assert provider_config["chat"][provider]["kwargs"]["a0_api_mode"] == "chat"
responses_providers = {
provider
for provider, config in oauth_provider_config["chat"].items()
if config.get("kwargs", {}).get("a0_api_mode") == "responses"
}
assert responses_providers == {"codex_oauth", "xai_grok_oauth"}
for provider in responses_providers:
assert "a0_api_mode" not in provider_config["chat"][provider].get("kwargs", {})
assert (
oauth_provider_config["chat"]["gemini_api_oauth"]["kwargs"]["a0_api_mode"]
== "chat"
)
for provider in ("codex_oauth", "github_copilot_oauth", "xai_grok_oauth"):
assert "a0_api_mode" not in oauth_provider_config["chat"][provider]["kwargs"]
for provider, config in oauth_provider_config["chat"].items():
if provider not in responses_providers:
assert config.get("kwargs", {}).get("a0_api_mode", "chat") == "chat"
def test_missing_api_key_banner_does_not_include_auto_modal_metadata(monkeypatch):

View file

@ -236,6 +236,7 @@ async def test_transport_retries_provider_state_as_local_replay(monkeypatch):
model="openai/gpt-5.4",
messages=[{"role": "user", "content": "new"}],
kwargs={
"a0_api_mode": "responses",
"previous_response_id": "resp_1",
"responses_input_items": [{"role": "user", "content": "new"}],
"responses_local_input_items": [{"role": "user", "content": "full"}],
@ -276,7 +277,10 @@ async def test_transport_downgrades_unsupported_builtin_tools(monkeypatch):
transport = litellm_transport.LiteLLMTransport(
model="openai/gpt-5.4",
messages=[{"role": "user", "content": "new"}],
kwargs={"responses_builtin_tools": [{"type": "web_search"}]},
kwargs={
"a0_api_mode": "responses",
"responses_builtin_tools": [{"type": "web_search"}],
},
)
parsed = await transport.acomplete()
@ -291,7 +295,10 @@ async def test_transport_downgrades_unsupported_builtin_tools(monkeypatch):
next_transport = litellm_transport.LiteLLMTransport(
model="openai/gpt-5.4",
messages=[{"role": "user", "content": "again"}],
kwargs={"responses_builtin_tools": [{"type": "web_search"}]},
kwargs={
"a0_api_mode": "responses",
"responses_builtin_tools": [{"type": "web_search"}],
},
)
request = next_transport._responses_request(stream=False)
assert "tools" not in request
@ -344,6 +351,7 @@ async def test_unified_turn_keeps_streamed_call_when_completion_omits_output(
model="test-model",
provider="openai",
model_config=None,
a0_api_mode="responses",
)
async def response_callback(chunk: str, full: str):
@ -430,6 +438,7 @@ async def test_unified_turn_waits_for_completed_native_responses_calls(monkeypat
model="test-model",
provider="openai",
model_config=None,
a0_api_mode="responses",
)
async def response_callback(chunk: str, full: str):

View file

@ -386,6 +386,7 @@ async def test_unified_call_closes_responses_stream_when_callback_raises(monkeyp
model="test-model",
provider="openai",
model_config=None,
a0_api_mode="responses",
)
async def response_callback(chunk: str, full: str):
@ -401,7 +402,7 @@ async def test_unified_call_closes_responses_stream_when_callback_raises(monkeyp
@pytest.mark.asyncio
async def test_chat_completions_escape_hatch_still_uses_acompletion(monkeypatch):
async def test_chat_completions_default_uses_acompletion(monkeypatch):
stream = _AsyncChunkStream([_chunk("hello")])
calls: list[str] = []
@ -425,7 +426,6 @@ async def test_chat_completions_escape_hatch_still_uses_acompletion(monkeypatch)
model="test-model",
provider="openai",
model_config=None,
a0_api_mode="chat_completions",
)
async def response_callback(chunk: str, full: str):
@ -508,6 +508,7 @@ async def test_unified_call_retries_responses_with_high_reasoning(monkeypatch):
model="gpt-5.4",
provider="openai",
model_config=None,
a0_api_mode="responses",
)
async def response_callback(chunk: str, full: str):
@ -558,6 +559,7 @@ async def test_unified_call_falls_back_to_chat_when_responses_endpoint_missing(
model="claude-opus-4.7",
provider="openai",
model_config=None,
a0_api_mode="responses",
tool_choice="auto",
parallel_tool_calls=True,
)
@ -616,6 +618,7 @@ async def test_unified_call_falls_back_when_litellm_hides_responses_404_url(
model="claude-opus-4.7",
provider="openai",
model_config=None,
a0_api_mode="responses",
)
async def response_callback(chunk: str, full: str):
@ -672,6 +675,7 @@ async def test_unified_call_falls_back_for_proxy_responses_failures(
model="test-model",
provider="openai",
model_config=None,
a0_api_mode="responses",
)
async def response_callback(chunk: str, full: str):
@ -720,6 +724,7 @@ async def test_unified_call_falls_back_when_responses_mock_reads_sse_as_json(
model="omniroute/test-model",
provider="openai",
model_config=None,
a0_api_mode="responses",
)
async def response_callback(chunk: str, full: str):
@ -769,6 +774,7 @@ async def test_unified_call_falls_back_when_responses_bad_request_rejects_shape(
model="venice-model",
provider="openai",
model_config=None,
a0_api_mode="responses",
)
async def response_callback(chunk: str, full: str):
@ -822,6 +828,7 @@ async def test_unified_call_raises_generic_responses_bad_request(monkeypatch):
model="test-model",
provider="openai",
model_config=None,
a0_api_mode="responses",
)
async def response_callback(chunk: str, full: str):
@ -873,6 +880,7 @@ async def test_unified_call_preserves_cache_control_with_chat_for_non_native_res
model="claude-sonnet-4-5",
provider="anthropic",
model_config=None,
a0_api_mode="responses",
)
async def response_callback(chunk: str, full: str):
@ -1157,6 +1165,7 @@ def test_complete_falls_back_to_chat_when_responses_shim_sends_empty_tools(
model="hosted_vllm/qwen",
messages=[{"role": "user", "content": "hi"}],
kwargs={
"a0_api_mode": "responses",
"tools": [],
"tool_choice": "auto",
"parallel_tool_calls": True,
@ -1358,12 +1367,12 @@ def test_cache_control_policy_keeps_native_responses_first():
openai_policy = litellm_transport.TransportPolicy.from_request(
"openai/gpt-5.4",
{},
{"a0_api_mode": "responses"},
messages=messages,
)
anthropic_policy = litellm_transport.TransportPolicy.from_request(
"anthropic/claude-sonnet-4-5",
{},
{"a0_api_mode": "responses"},
messages=messages,
)