fix: address PR review — Responses input encoding + finish Azure removal

- 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.
This commit is contained in:
Alex committed 2026-06-03 14:56:28 +01:00
1 parent 02d1458467
commit 27f9ddc8e0
6 files changed
+80 -67

No files matched your search

+21 -11
View File
@@ -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
+1 -2
View File
@@ -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.
-1
View File
@@ -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) |
+4 -26
View File
@@ -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
+4 -24
View File
@@ -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
+50 -3
View File
@@ -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