From 27f9ddc8e0683189aded279f18163d2d6119d88b Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 3 Jun 2026 14:56:28 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20address=20PR=20review=20=E2=80=94=20Resp?= =?UTF-8?q?onses=20input=20encoding=20+=20finish=20Azure=20removal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replay prior-turn assistant text as input_text, not output_text: a Responses easy-input message only accepts input_* content parts, so the old shape would 400 on the second turn of every conversation (default store=false resends prior turns inline). - Always request include=["reasoning.encrypted_content"] so in-turn reasoning carryover works whether or not the response is also stored server-side (OPENAI_RESPONSES_STORE). - Add detail="auto" to input_image parts. - Remove the now-dead azure_openai option from setup.sh / setup.ps1 and the docs, completing the AzureOpenAILLM removal. - Tests: correct the input_text / image-detail / include assertions and add parallel tool-call and stream-error coverage. --- application/llm/openai.py | 32 ++++++++----- docs/content/Deploying/DocsGPT-Settings.mdx | 3 +- docs/content/Models/cloud-providers.mdx | 1 - setup.ps1 | 30 ++---------- setup.sh | 28 ++--------- tests/llm/test_openai_responses.py | 53 +++++++++++++++++++-- 6 files changed, 80 insertions(+), 67 deletions(-) diff --git a/application/llm/openai.py b/application/llm/openai.py index 7942c220..ddbe5e3d 100644 --- a/application/llm/openai.py +++ b/application/llm/openai.py @@ -461,8 +461,15 @@ class OpenAILLM(BaseLLM): @staticmethod def _responses_content_parts(role, content): """Translate a cleaned chat ``content`` value into Responses content - parts (``input_text``/``output_text``/``input_image``/``input_file``).""" - text_type = "output_text" if role == "assistant" else "input_text" + parts (``input_text``/``input_image``/``input_file``). + + Assistant text from prior turns is replayed as ``input_text`` too: a + Responses easy-input message only accepts the input_* content types + regardless of role (``output_text`` is reserved for full + output-message items, which carry id/status/annotations). + """ + _ = role + text_type = "input_text" parts = [] if content is None: return parts @@ -480,7 +487,11 @@ class OpenAILLM(BaseLLM): elif itype == "image_url": url = (item.get("image_url") or {}).get("url") if url: - parts.append({"type": "input_image", "image_url": url}) + parts.append({ + "type": "input_image", + "image_url": url, + "detail": "auto", + }) elif itype == "file": file_obj = item.get("file") or {} file_part = {"type": "input_file"} @@ -640,14 +651,13 @@ class OpenAILLM(BaseLLM): store = bool(settings.OPENAI_RESPONSES_STORE) params["store"] = store - if store: - if previous_response_id: - params["previous_response_id"] = previous_response_id - else: - # Stateless: ask the API to return encrypted reasoning so it can - # be replayed across the in-turn tool loop without server-side - # retention of the conversation. - params["include"] = ["reasoning.encrypted_content"] + if store and previous_response_id: + params["previous_response_id"] = previous_response_id + # Always request encrypted reasoning content so reasoning items can be + # replayed by value across the in-turn tool loop — this keeps + # carryover working whether or not the response is also retained + # server-side (store=true). + params["include"] = ["reasoning.encrypted_content"] return params @staticmethod diff --git a/docs/content/Deploying/DocsGPT-Settings.mdx b/docs/content/Deploying/DocsGPT-Settings.mdx index 373e56cb..ea4227e9 100644 --- a/docs/content/Deploying/DocsGPT-Settings.mdx +++ b/docs/content/Deploying/DocsGPT-Settings.mdx @@ -48,7 +48,6 @@ Here are some of the most fundamental settings you'll likely want to configure: - `anthropic`: Use Anthropic's Claude models. - `groq`: Use Groq's models. - `huggingface`: Use HuggingFace Inference API. - - `azure_openai`: Use Azure OpenAI Service. - `openai` (when using local inference engines like Ollama, Llama.cpp, TGI, etc.): This signals DocsGPT to use an OpenAI-compatible API format, even if the actual LLM is running locally. - **`LLM_NAME`**: Specifies the specific model to use from the chosen LLM provider. The available models depend on the `LLM_PROVIDER` you've selected. @@ -103,7 +102,7 @@ In this case, even though you are using Ollama locally, `LLM_PROVIDER` is set to DocsGPT ships with a built-in catalog of models for the providers it supports out of the box (OpenAI, Anthropic, Google, Groq, OpenRouter, -Novita, Azure OpenAI, Hugging Face, DocsGPT). To add **your own +Novita, Hugging Face, DocsGPT). To add **your own models** without forking the repo — for example, a Mistral or Together account, a self-hosted vLLM endpoint, or any other OpenAI-compatible API — point `MODELS_CONFIG_DIR` at a directory of YAML files. diff --git a/docs/content/Models/cloud-providers.mdx b/docs/content/Models/cloud-providers.mdx index cc4018b3..23d0fd44 100644 --- a/docs/content/Models/cloud-providers.mdx +++ b/docs/content/Models/cloud-providers.mdx @@ -29,7 +29,6 @@ DocsGPT offers direct, streamlined support for the following cloud LLM providers | Anthropic (Claude) | `anthropic` | `claude-3-5-sonnet-20241022`| | Groq | `groq` | `llama-3.3-70b-versatile` | | HuggingFace Inference API | `huggingface` | `meta-llama/Llama-3.1-8B-Instruct` | -| Azure OpenAI | `azure_openai` | `azure-gpt-4` | | Prem AI | `premai` | (See Prem AI docs) | | AWS SageMaker | `sagemaker` | (See SageMaker docs) | | Novita AI | `novita` | (See Novita docs) | diff --git a/setup.ps1 b/setup.ps1 index 75a8011f..f3d43d31 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -266,11 +266,10 @@ function Prompt-CloudAPIProviderOptions { Write-ColorText "3) Anthropic (Claude)" -ForegroundColor "Yellow" Write-ColorText "4) Groq" -ForegroundColor "Yellow" Write-ColorText "5) HuggingFace Inference API" -ForegroundColor "Yellow" - Write-ColorText "6) Azure OpenAI" -ForegroundColor "Yellow" - Write-ColorText "7) Novita" -ForegroundColor "Yellow" + Write-ColorText "6) Novita" -ForegroundColor "Yellow" Write-ColorText "b) Back to Main Menu" -ForegroundColor "Yellow" Write-Host "" - $script:provider_choice = Read-Host "Choose option (1-7, or b)" + $script:provider_choice = Read-Host "Choose option (1-6, or b)" } # Function to prompt for Ollama CPU/GPU options @@ -974,20 +973,7 @@ function Connect-CloudAPIProvider { Get-APIKey break } - "6" { # Azure OpenAI - $script:provider_name = "Azure OpenAI" - $script:llm_name = "azure_openai" - $script:model_name = "gpt-4o" - Get-APIKey - Write-Host "" - Write-ColorText "Azure OpenAI requires additional configuration:" -ForegroundColor "White" -Bold - $script:azure_api_base = Read-Host "Enter Azure OpenAI API base URL (e.g. https://your-resource.openai.azure.com/)" - $script:azure_api_version = Read-Host "Enter Azure OpenAI API version (e.g. 2024-02-15-preview)" - $script:azure_deployment = Read-Host "Enter Azure deployment name for chat" - $script:azure_emb_deployment = Read-Host "Enter Azure deployment name for embeddings (leave empty to skip)" - break - } - "7" { # Novita + "6" { # Novita $script:provider_name = "Novita" $script:llm_provider = "novita" $script:model_name = "moonshotai/kimi-k2.5" @@ -998,7 +984,7 @@ function Connect-CloudAPIProvider { "B" { Clear-Host; return } default { Write-Host "" - Write-ColorText "Invalid choice. Please choose 1-7, or b." -ForegroundColor "Red" + Write-ColorText "Invalid choice. Please choose 1-6, or b." -ForegroundColor "Red" Start-Sleep -Seconds 1 } } @@ -1017,14 +1003,6 @@ function Connect-CloudAPIProvider { "LLM_NAME=$model_name" | Add-Content -Path $ENV_FILE -Encoding utf8 "VITE_API_STREAMING=true" | Add-Content -Path $ENV_FILE -Encoding utf8 - # Azure OpenAI additional settings - if ($llm_name -eq "azure_openai") { - if ($azure_api_base) { "OPENAI_API_BASE=$azure_api_base" | Add-Content -Path $ENV_FILE -Encoding utf8 } - if ($azure_api_version) { "OPENAI_API_VERSION=$azure_api_version" | Add-Content -Path $ENV_FILE -Encoding utf8 } - if ($azure_deployment) { "AZURE_DEPLOYMENT_NAME=$azure_deployment" | Add-Content -Path $ENV_FILE -Encoding utf8 } - if ($azure_emb_deployment) { "AZURE_EMBEDDINGS_DEPLOYMENT_NAME=$azure_emb_deployment" | Add-Content -Path $ENV_FILE -Encoding utf8 } - } - Write-ColorText ".env file configured for $provider_name." -ForegroundColor "Green" Prompt-AdvancedSettings diff --git a/setup.sh b/setup.sh index 7ae1d3cc..e98a6e0b 100755 --- a/setup.sh +++ b/setup.sh @@ -152,11 +152,10 @@ prompt_cloud_api_provider_options() { echo -e "${YELLOW}3) Anthropic (Claude)${NC}" echo -e "${YELLOW}4) Groq${NC}" echo -e "${YELLOW}5) HuggingFace Inference API${NC}" - echo -e "${YELLOW}6) Azure OpenAI${NC}" - echo -e "${YELLOW}7) Novita${NC}" + echo -e "${YELLOW}6) Novita${NC}" echo -e "${YELLOW}b) Back to Main Menu${NC}" echo - read -p "$(echo -e "${DEFAULT_FG}Choose option (1-7, or b): ${NC}")" provider_choice + read -p "$(echo -e "${DEFAULT_FG}Choose option (1-6, or b): ${NC}")" provider_choice } # Function to prompt for Ollama CPU/GPU options @@ -700,25 +699,14 @@ connect_cloud_api_provider() { model_name="meta-llama/Llama-3.1-8B-Instruct" get_api_key break ;; - 6) # Azure OpenAI - provider_name="Azure OpenAI" - llm_provider="azure_openai" - model_name="gpt-4o" - get_api_key - echo -e "\n${DEFAULT_FG}${BOLD}Azure OpenAI requires additional configuration:${NC}" - read -p "$(echo -e "${DEFAULT_FG}Enter Azure OpenAI API base URL (e.g. https://your-resource.openai.azure.com/): ${NC}")" azure_api_base - read -p "$(echo -e "${DEFAULT_FG}Enter Azure OpenAI API version (e.g. 2024-02-15-preview): ${NC}")" azure_api_version - read -p "$(echo -e "${DEFAULT_FG}Enter Azure deployment name for chat: ${NC}")" azure_deployment - read -p "$(echo -e "${DEFAULT_FG}Enter Azure deployment name for embeddings (leave empty to skip): ${NC}")" azure_emb_deployment - break ;; - 7) # Novita + 6) # Novita provider_name="Novita" llm_provider="novita" model_name="moonshotai/kimi-k2.5" get_api_key break ;; b|B) clear; return 1 ;; # Clear screen and Back to Main Menu - *) echo -e "\n${RED}Invalid choice. Please choose 1-7, or b.${NC}" ; sleep 1 ;; + *) echo -e "\n${RED}Invalid choice. Please choose 1-6, or b.${NC}" ; sleep 1 ;; esac done @@ -728,14 +716,6 @@ connect_cloud_api_provider() { echo "LLM_NAME=$model_name" >> "$ENV_FILE" echo "VITE_API_STREAMING=true" >> "$ENV_FILE" - # Azure OpenAI additional settings - if [ "$llm_provider" = "azure_openai" ]; then - [ -n "$azure_api_base" ] && echo "OPENAI_API_BASE=$azure_api_base" >> "$ENV_FILE" - [ -n "$azure_api_version" ] && echo "OPENAI_API_VERSION=$azure_api_version" >> "$ENV_FILE" - [ -n "$azure_deployment" ] && echo "AZURE_DEPLOYMENT_NAME=$azure_deployment" >> "$ENV_FILE" - [ -n "$azure_emb_deployment" ] && echo "AZURE_EMBEDDINGS_DEPLOYMENT_NAME=$azure_emb_deployment" >> "$ENV_FILE" - fi - echo -e "${GREEN}.env file configured for ${BOLD}${provider_name}${NC}${GREEN}.${NC}" prompt_advanced_settings diff --git a/tests/llm/test_openai_responses.py b/tests/llm/test_openai_responses.py index a7505a49..67a021d8 100644 --- a/tests/llm/test_openai_responses.py +++ b/tests/llm/test_openai_responses.py @@ -102,7 +102,7 @@ def test_to_responses_input_tool_roundtrip(monkeypatch): "arguments": '{"q":"x"}', }, {"type": "function_call_output", "call_id": "call_1", "output": "result"}, - {"role": "assistant", "content": [{"type": "output_text", "text": "final"}]}, + {"role": "assistant", "content": [{"type": "input_text", "text": "final"}]}, ] @@ -145,7 +145,11 @@ def test_to_responses_input_multimodal_image(monkeypatch): "role": "user", "content": [ {"type": "input_text", "text": "look"}, - {"type": "input_image", "image_url": "data:image/png;base64,xx"}, + { + "type": "input_image", + "image_url": "data:image/png;base64,xx", + "detail": "auto", + }, ], }] @@ -230,7 +234,9 @@ def test_build_responses_params_store_with_previous_id(monkeypatch): ) assert params["store"] is True assert params["previous_response_id"] == "resp_abc" - assert "include" not in params + # Encrypted reasoning is always requested so in-turn carryover works + # regardless of server-side retention. + assert params["include"] == ["reasoning.encrypted_content"] # ── streaming normalization into the existing handler contract ─────────────── @@ -294,6 +300,47 @@ def test_responses_gen_stream_text_only(monkeypatch): assert llm._last_response_id == "resp_2" +@pytest.mark.unit +def test_responses_gen_stream_parallel_tool_calls(monkeypatch): + from application.llm.handlers.openai import OpenAILLMHandler + + llm = _make_llm(monkeypatch, _responses_caps()) + events = [ + _ns( + type="response.output_item.added", output_index=0, + item=_ns(type="function_call", call_id="call_a", name="t1", id="fc_a"), + ), + _ns( + type="response.output_item.added", output_index=1, + item=_ns(type="function_call", call_id="call_b", name="t2", id="fc_b"), + ), + _ns(type="response.function_call_arguments.delta", output_index=0, delta='{"a":'), + _ns(type="response.function_call_arguments.done", output_index=0, arguments='{"a":1}'), + _ns(type="response.function_call_arguments.done", output_index=1, arguments='{"b":2}'), + _ns(type="response.completed", response=_ns(id="resp_p")), + ] + llm.client.responses.create = MagicMock(return_value=events) + out = list(llm._responses_gen_stream("gpt-5.5", [{"role": "user", "content": "hi"}], tools=[{"type": "function", "function": {"name": "t1", "parameters": {}}}])) + parsed = OpenAILLMHandler().parse_response(out[-1]) + assert parsed.finish_reason == "tool_calls" + assert [tc.id for tc in parsed.tool_calls] == ["call_a", "call_b"] + assert [tc.index for tc in parsed.tool_calls] == [0, 1] + assert parsed.tool_calls[0].arguments == '{"a":1}' + assert parsed.tool_calls[1].arguments == '{"b":2}' + + +@pytest.mark.unit +def test_responses_gen_stream_error_event_raises(monkeypatch): + llm = _make_llm(monkeypatch, _responses_caps()) + events = [ + _ns(type="response.output_text.delta", delta="partial"), + _ns(type="response.failed", response=_ns(error="boom")), + ] + llm.client.responses.create = MagicMock(return_value=events) + with pytest.raises(RuntimeError): + list(llm._responses_gen_stream("gpt-5.5", [{"role": "user", "content": "hi"}], tools=None)) + + @pytest.mark.unit def test_responses_gen_nonstream_tools(monkeypatch): from application.llm.handlers.openai import OpenAILLMHandler