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, )