Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions openrag/api/schemas/user/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@


class OpenAIMessage(BaseModel):
# Same passthrough policy as the request below, applied per message. An
# OpenAI message carries more than role/content — `name`, `tool_calls`,
# `function_call`, `tool_call_id` — and pydantic's default `extra="ignore"`
# dropped them here, before the router dumped the payload, so they never
# reached the LLM. `QueryService._sanitize_messages` already branches on
# `tool_calls`/`function_call`, which until now could not survive parsing.
model_config = ConfigDict(extra="allow")
Comment thread
coderabbitai[bot] marked this conversation as resolved.

role: Literal["user", "assistant", "system"]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
content: str

Expand Down Expand Up @@ -61,6 +69,11 @@ def _ignore_top_logprobs_without_logprobs(self) -> "OpenAIChatCompletionRequest"


class OpenAICompletionRequest(BaseModel):
# Mirrors OpenAIChatCompletionRequest: forward vendor-specific params rather
# than silently dropping them. The bounds below still apply — `extra="allow"`
# only admits *undeclared* keys, so `n`/`best_of` stay validated.
model_config = ConfigDict(extra="allow")

model: str | None = Field(None, description="model name")
prompt: str
# Bound n/best_of: each multiplies generation cost, so leaving them unbounded
Expand Down
58 changes: 58 additions & 0 deletions tests/unit/api/schemas/test_api_schema_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,64 @@ def test_chat_request_passes_through_extra_openai_params():
assert dump["seed"] == 42


def test_chat_message_passes_through_extra_openai_fields():
"""An OpenAI message is more than role/content: `name` disambiguates speakers
and `tool_calls`/`tool_call_id` carry function calling. Dropping them here
silently truncated the history sent to the LLM
"""
request = OpenAIChatCompletionRequest.model_validate(
{
"messages": [
{"role": "user", "content": "hi", "name": "alice"},
{
"role": "assistant",
"content": "",
"tool_calls": [{"id": "c1", "type": "function", "function": {"name": "f", "arguments": "{}"}}],
},
]
}
)
messages = request.model_dump(exclude_none=True)["messages"]

assert messages[0]["name"] == "alice"
assert messages[1]["tool_calls"][0]["id"] == "c1"
Comment on lines +117 to +137

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files matching target:"
git ls-files 'tests/unit/api/schemas/test_api_schema_imports.py' || true

if [ -f tests/unit/api/schemas/test_api_schema_imports.py ]; then
  echo
  echo "Outline/target section:"
  wc -l tests/unit/api/schemas/test_api_schema_imports.py
  sed -n '1,220p' tests/unit/api/schemas/test_api_schema_imports.py | cat -n
fi

Repository: linagora/openrag

Length of output: 10310


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Chat schema files:"
fd -a 'chat\.py$' openrag api tests | sed 's#^\./##' | sort

echo
for f in $(fd 'chat\.py$' openrag | sort); do
  echo "--- $f ($(wc -l < "$f") lines) ---"
  cat -n "$f"
done

echo
echo "References to OpenAIMessage / tool_call_id / function_call in api.schemas:"
rg -n "OpenAIMessage|tool_call_id|function_call|name|extra|model_config|ConfigDict" api/schemas -g '*.py'

Repository: linagora/openrag

Length of output: 37832


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Pydantic availability/version:"
python3 - <<'PY'
try:
    import pydantic
    import pydantic.version
    print(pydantic.__version__, pydantic.version.version_info())
except Exception as e:
    print("pydantic import failed:", type(e).__name__, str(e))
PY

echo
if python3 - <<'PY'
import pydantic
print(pydantic.__version__)
PY
then
  python3 - <<'PY'
from pydantic import BaseModel, ConfigDict
import json

class Inner(BaseModel):
    model_config = ConfigDict(extra="allow")
    role: str
    content: str

class Request(BaseModel):
    model_config = ConfigDict(extra="allow")
    messages: list[Inner]

data = {
    "messages": [
        {"role": "user", "content": "hi", "name": "alice"},
        {
            "role": "assistant",
            "content": "",
            "tool_calls": [
                {
                    "id": "c1",
                    "type": "function",
                    "tool_call_id": "t1",
                    "function": {"name": "f", "arguments": "{}"}
                }
            ],
            "function_call": {"name": "g"}
        },
        {"role": "tool", "tool_call_id": "t1", "content": "response"},
    ]
}

request = Request.model_validate(data)
dump = request.model_dump(exclude_none=True)
print(json.dumps(dump, indent=2))

checks = {
    "name": dump["messages"][0].get("name"),
    "tool_call_id": dump["messages"][1]["tool_calls"][0].get("tool_call_id"),
    "function_call": dump["messages"][1].get("function_call"),
    "nested_name": dump["messages"][1]["tool_calls"][0]["function"].get("name"),
    "nested_arguments": dump["messages"][1]["tool_calls"][0]["function"].get("arguments"),
    "tool_role_call_id": dump["messages"][2].get("tool_call_id"),
}
print("checks:", checks)
PY
fi

Repository: linagora/openrag

Length of output: 378


🌐 Web query:

OpenAI chat completion assistant message tool_calls tool_call_id function_call tool role messages parameter

💡 Result:

In the OpenAI Chat Completions API, the interaction between tool calls and assistant messages is managed through specific parameters and message roles [1][2]. The tool_calls parameter is an array within an assistant message (role: "assistant") that contains the tool calls the model has requested to execute [1][3]. Each object in this array typically includes an id (tool_call_id), which acts as a unique identifier for that specific call, and the details of the function or tool to be invoked (e.g., function name and arguments) [4][5][6]. When the model generates a tool_calls request, the developer must execute the corresponding functions and return the results to the model [5]. This is done by appending a new message with the role "tool" to the message history [5]. This "tool" message must include: 1. tool_call_id: Must match the id from the original tool_calls request [5][7]. 2. content: The output of the function execution [4][5]. The older function_call parameter is deprecated in favor of tool_calls and tools [1][3][6]. While function_call was once the primary way to handle function execution, tool_calls provides a more robust and flexible system, allowing for multiple tool calls in a single turn [1][6]. A complete message flow typically looks like this: 1. User sends a prompt. 2. Assistant replies with a message containing tool_calls (an array of calls with unique tool_call_ids) [1]. 3. Developer appends this assistant message to the history and then appends one "tool" message for each tool call, matching the tool_call_id [5]. 4. Developer calls the Chat Completions API again with the updated message history, allowing the model to process the tool results and formulate a final response [5]. If you send an assistant message containing tool_calls, the API requires you to provide corresponding "tool" messages to satisfy the tool_call_id requirements, or it will return an error [7].

Citations:


🌐 Web query:

site:openai.github.io/openai-python ChatCompletionMessageParam tool role

💡 Result:

ChatCompletionMessageParam can include a message with role: "tool" for tool results — i.e. the output returned after the assistant makes a tool call. In the OpenAI Python SDK internals, tool messages are paired using tool_call_id, and the model expects the tool-result message to reference the original tool call ID. (openai.github.io)

If you want, I can show the exact JSON shape for a tool message.

Citations:


🌐 Web query:

OpenAI Python chat completions tool role messages role tool tool_call_id required

💡 Result:

In the OpenAI Python Chat Completions API, when providing the result of a tool call back to the model, you must use a message with the role set to "tool" [1][2]. For this "tool" role message, the following fields are required: 1. role: Must be the literal string "tool" [1][3]. 2. tool_call_id: A string matching the id of the specific tool call you are responding to [1][2][4]. 3. content: The output of the tool execution (as a string) [1][2]. Additionally, while not strictly listed as a required parameter in some schema definitions compared to the three above, it is standard practice to include the name field (the name of the tool function that was called) in your message dictionary to ensure correct identification by the model [2][5][4]. Example structure for appending a tool result to your messages list: messages.append({ "role": "tool", "tool_call_id": tool_call.id, "name": tool_call.function.name, "content": json.dumps(function_result) }) It is important to note that a message with the role "tool" must directly follow the assistant's message that requested the tool call, and the tool_call_id must strictly correspond to the id provided in that assistant message [2][4]. Attempting to reuse an object type like ChatCompletionMessage (which is typically for assistant responses) for this "tool" role message will often result in a validation error, as the role "tool" is specifically required [5].

Citations:


Cover the full message passthrough contract.

This test only checks name and tool_calls[0]["id"], leaving nested fields such as tool_call_id, function_call, and nested function.name/function.arguments unguarded. Add assertions for those fields in this case or an equivalent tool-result message case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/api/schemas/test_api_schema_imports.py` around lines 117 - 137,
Extend test_chat_message_passes_through_extra_openai_fields to assert
preservation of tool_calls[0].function.name and function.arguments, plus
tool_call_id and function_call using the corresponding message fixtures. Keep
the existing name and tool-call ID assertions, and ensure nested OpenAI fields
are verified after model_dump(exclude_none=True).



def test_sanitize_messages_keeps_tool_calls_reaching_it():
"""_sanitize_messages leaves a content-free assistant turn alone when it
carries tool_calls — reachable only now that the schema forwards the field
"""
from services.orchestrators.query_service import QueryService

request = OpenAIChatCompletionRequest.model_validate(
{
"messages": [
{
"role": "assistant",
"content": "",
"tool_calls": [{"id": "c1", "type": "function", "function": {"name": "f", "arguments": "{}"}}],
}
]
}
)
sanitized = QueryService._sanitize_messages(request.model_dump(exclude_none=True)["messages"])

assert sanitized[0]["content"] == ""


def test_completion_request_passes_through_extra_openai_params():
"""Legacy /completions mirrors the chat request: undeclared vendor params are
forwarded, while the declared bounds on n/best_of still apply
"""
request = OpenAICompletionRequest.model_validate({"prompt": "hi", "suffix": "!", "user": "alice"})
dump = request.model_dump(exclude_none=True)

assert dump["suffix"] == "!"
assert dump["user"] == "alice"
with pytest.raises(ValidationError):
OpenAICompletionRequest.model_validate({"prompt": "hi", "n": 9, "user": "alice"})


def test_completion_request_omits_unset_nulls():
"""The /completions router dumps with exclude_none=True (matching chat), so
optional params left unset are not sent as explicit null to strict providers
Expand Down
Loading