From dac15f77a336be4843f5b80ddd2326fb8fba7674 Mon Sep 17 00:00:00 2001 From: Alessandro <155005371+3clyp50@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:40:13 +0200 Subject: [PATCH] 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. --- conf/AGENTS.md | 2 +- conf/model_providers.yaml | 5 +++ helpers/litellm_transport.py | 15 ++------- helpers/litellm_transport.py.dox.md | 2 +- plugins/_oauth/AGENTS.md | 2 +- plugins/_oauth/conf/model_providers.yaml | 3 ++ tests/test_model_config_api_keys.py | 42 +++++++----------------- tests/test_responses_architecture.py | 13 ++++++-- tests/test_stream_tool_early_stop.py | 17 +++++++--- 9 files changed, 49 insertions(+), 52 deletions(-) diff --git a/conf/AGENTS.md b/conf/AGENTS.md index f01e9488f..f81c872bf 100644 --- a/conf/AGENTS.md +++ b/conf/AGENTS.md @@ -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 diff --git a/conf/model_providers.yaml b/conf/model_providers.yaml index d1394d708..c7676f0db 100644 --- a/conf/model_providers.yaml +++ b/conf/model_providers.yaml @@ -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 diff --git a/helpers/litellm_transport.py b/helpers/litellm_transport.py index 5491a5fd6..3a88f574e 100644 --- a/helpers/litellm_transport.py +++ b/helpers/litellm_transport.py @@ -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: diff --git a/helpers/litellm_transport.py.dox.md b/helpers/litellm_transport.py.dox.md index 8a80b030e..78f304baf 100644 --- a/helpers/litellm_transport.py.dox.md +++ b/helpers/litellm_transport.py.dox.md @@ -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. diff --git a/plugins/_oauth/AGENTS.md b/plugins/_oauth/AGENTS.md index 0a26286e1..e4d717311 100644 --- a/plugins/_oauth/AGENTS.md +++ b/plugins/_oauth/AGENTS.md @@ -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 diff --git a/plugins/_oauth/conf/model_providers.yaml b/plugins/_oauth/conf/model_providers.yaml index b9b4d65fb..058ad71af 100644 --- a/plugins/_oauth/conf/model_providers.yaml +++ b/plugins/_oauth/conf/model_providers.yaml @@ -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" diff --git a/tests/test_model_config_api_keys.py b/tests/test_model_config_api_keys.py index ee9c1eda5..ab559e7f5 100644 --- a/tests/test_model_config_api_keys.py +++ b/tests/test_model_config_api_keys.py @@ -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): diff --git a/tests/test_responses_architecture.py b/tests/test_responses_architecture.py index 462ed391c..02fa880a7 100644 --- a/tests/test_responses_architecture.py +++ b/tests/test_responses_architecture.py @@ -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): diff --git a/tests/test_stream_tool_early_stop.py b/tests/test_stream_tool_early_stop.py index e688916c0..eb3e5db2c 100644 --- a/tests/test_stream_tool_early_stop.py +++ b/tests/test_stream_tool_early_stop.py @@ -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, )