Commit cad4524f authored by Kantz's avatar Kantz
Browse files

OpenAI reasoning intigiert

parent fa3e49fd
......@@ -6,15 +6,13 @@ from typing import Any, Callable
import ollama
from mistralai.client import Mistral
from openai import OpenAI
from openai import BadRequestError, OpenAI
from app import config
from app.deterministic_services import llm_quota
from app.deterministic_services.llm_tool_client import (
append_tool_trace_log,
resolve_mcp_base_url,
run_tool_chat_sync,
with_mcp_session,
)
......@@ -71,6 +69,110 @@ def _get_message_field(message: object, field: str) -> str:
return getattr(message, field) or ""
return ""
# ---
# OpenAI response extraction helpers
# ---
def _field(value: object, name: str, default=None):
if isinstance(value, dict):
return value.get(name, default)
return getattr(value, name, default)
def _extract_block_text(blocks: list[object]) -> str:
return "".join(str(text) for block in blocks or [] if (text := _field(block, "text")))
def _extract_response_thinking(output: list[object]) -> str:
parts: list[str] = []
for item in output:
if _field(item, "type") != "reasoning":
continue
parts.append(_extract_block_text(_field(item, "summary", []) or []))
parts.append(_extract_block_text(_field(item, "content", []) or []))
return "\n".join(part for part in parts if part)
def _extract_response_openai(response) -> object:
output = _field(response, "output", []) or []
thinking = _extract_response_thinking(output)
if isinstance(response, dict):
output_text = response.get("output_text")
if output_text is not None:
message = {"role": "assistant", "content": output_text}
if thinking:
message["thinking"] = thinking
return message
else:
output_text = getattr(response, "output_text", None)
if output_text is not None:
message = {"role": "assistant", "content": output_text}
if thinking:
message["thinking"] = thinking
return message
parts: list[str] = []
for item in output:
content = item.get("content", []) if isinstance(item, dict) else getattr(item, "content", []) or []
for block in content or []:
block_type = block.get("type") if isinstance(block, dict) else getattr(block, "type", "")
text = block.get("text") if isinstance(block, dict) else getattr(block, "text", None)
if block_type in {"output_text", "text"} and text:
parts.append(str(text))
message = {"role": "assistant", "content": "".join(parts)}
if thinking:
message["thinking"] = thinking
return message
def _openai_reasoning() -> dict[str, str]:
if config.get_llm_thinking_enabled():
return {"effort": "high", "summary": "detailed"}
return {"effort": "none"}
def _message_content_text(content: object) -> str:
if isinstance(content, str):
return content
if isinstance(content, list):
parts = []
for item in content:
text = _field(item, "text")
if text:
parts.append(str(text))
return "\n".join(parts)
return str(content or "")
def _messages_to_responses_input(messages: list[dict]) -> tuple[str | None, str]:
instructions = "\n\n".join(
_message_content_text(message.get("content"))
for message in messages
if message.get("role") == "system"
).strip() or None
turns = []
for message in messages:
role = message.get("role")
if role == "system":
continue
content = _message_content_text(message.get("content")).strip()
if content:
turns.append(f"{role or 'user'}: {content}")
return instructions, "\n\n".join(turns)
def _is_unsupported_temperature_error(exc: Exception) -> bool:
text = str(exc)
return (
isinstance(exc, BadRequestError)
and (getattr(exc, "param", None) == "temperature" or "temperature" in text)
and "Unsupported parameter" in text
)
# ---
# Quota
......@@ -152,11 +254,21 @@ def _chat_openai_compatible(
settings = settings or _require_openai_chat_settings()
timeout = settings.timeout or 60.0
client = OpenAI(api_key=settings.api_key, base_url=settings.base_url, timeout=timeout)
kwargs = {"messages": messages, "model": settings.model}
instructions, response_input = _messages_to_responses_input(messages)
kwargs = {"input": response_input, "model": settings.model}
if instructions:
kwargs["instructions"] = instructions
if settings.temperature is not None:
kwargs["temperature"] = settings.temperature
response = client.chat.completions.create(**kwargs)
message = response.choices[0].message if response.choices else {}
kwargs["reasoning"] = _openai_reasoning()
try:
response = client.responses.create(**kwargs)
except BadRequestError as exc:
if "temperature" not in kwargs or not _is_unsupported_temperature_error(exc):
raise
kwargs.pop("temperature")
response = client.responses.create(**kwargs)
message = _extract_response_openai(response)
return {"raw": response, "message": message}
......
......@@ -6,7 +6,7 @@ import httpx
import ollama
from mcp.client.session import ClientSession
from mcp.client.streamable_http import streamable_http_client
from openai import AsyncOpenAI
from openai import AsyncOpenAI, BadRequestError
from app import config
from app.deterministic_services import tool_log_context
......@@ -14,6 +14,15 @@ from app.deterministic_services import tool_log_context
AUTH_HEADER = "X-MCP-Shared-Secret"
def _is_unsupported_temperature_error(exc: Exception) -> bool:
text = str(exc)
return (
isinstance(exc, BadRequestError)
and (getattr(exc, "param", None) == "temperature" or "temperature" in text)
and "Unsupported parameter" in text
)
def _extract_message(response) -> object:
if isinstance(response, dict) and "message" in response:
return response["message"]
......@@ -30,6 +39,38 @@ def _extract_tool_calls(message: object) -> list[Any]:
return []
def _extract_response_tool_calls(response: object) -> list[Any]:
output = response.get("output", []) if isinstance(response, dict) else getattr(response, "output", []) or []
return [
item for item in output
if (item.get("type") if isinstance(item, dict) else getattr(item, "type", "")) == "function_call"
]
def _extract_response_output(response: object) -> list[Any]:
return response.get("output", []) if isinstance(response, dict) else getattr(response, "output", []) or []
def _field(value: object, name: str, default=None):
if isinstance(value, dict):
return value.get(name, default)
return getattr(value, name, default)
def _extract_block_text(blocks: list[object]) -> str:
return "".join(str(text) for block in blocks or [] if (text := _field(block, "text")))
def _extract_response_thinking(response: object) -> str:
parts: list[str] = []
for item in _extract_response_output(response):
if _field(item, "type") != "reasoning":
continue
parts.append(_extract_block_text(_field(item, "summary", []) or []))
parts.append(_extract_block_text(_field(item, "content", []) or []))
return "\n".join(part for part in parts if part)
def _normalize_message(message: object) -> dict[str, Any]:
if isinstance(message, dict):
return dict(message)
......@@ -44,6 +85,10 @@ def _normalize_message(message: object) -> dict[str, Any]:
def _extract_tool_call_name(tool_call: object) -> str:
if hasattr(tool_call, "name"):
return str(getattr(tool_call, "name") or "")
if isinstance(tool_call, dict) and "name" in tool_call:
return str(tool_call.get("name") or "")
function = getattr(tool_call, "function", None)
if function is not None and hasattr(function, "name"):
return getattr(function, "name")
......@@ -54,11 +99,16 @@ def _extract_tool_call_name(tool_call: object) -> str:
def _extract_tool_call_arguments(tool_call: object) -> dict[str, Any]:
function = getattr(tool_call, "function", None)
raw_arguments: Any = None
if function is not None and hasattr(function, "arguments"):
if hasattr(tool_call, "arguments"):
raw_arguments = getattr(tool_call, "arguments")
elif isinstance(tool_call, dict) and "arguments" in tool_call:
raw_arguments = tool_call.get("arguments")
function = getattr(tool_call, "function", None)
if raw_arguments is None and function is not None and hasattr(function, "arguments"):
raw_arguments = getattr(function, "arguments")
elif isinstance(tool_call, dict):
elif raw_arguments is None and isinstance(tool_call, dict):
function_dict = tool_call.get("function") or {}
raw_arguments = function_dict.get("arguments")
......@@ -70,6 +120,10 @@ def _extract_tool_call_arguments(tool_call: object) -> dict[str, Any]:
def _extract_tool_call_id(tool_call: object) -> str:
if hasattr(tool_call, "call_id"):
return str(getattr(tool_call, "call_id") or "")
if isinstance(tool_call, dict) and "call_id" in tool_call:
return str(tool_call.get("call_id") or "")
if hasattr(tool_call, "id"):
return str(getattr(tool_call, "id") or "")
if isinstance(tool_call, dict):
......@@ -92,27 +146,46 @@ def _extract_structured_tool_output(tool_result: Any) -> Any:
def append_tool_trace_log(result: dict, provider: str) -> None:
tool_trace = result.get("tool_trace")
if not tool_trace:
tool_trace = result.get("tool_trace") or []
raw = result.get("raw")
output = _extract_response_output(raw) if provider in {"openai", "gwdg"} else []
reasoning = _extract_response_thinking(raw) if output else ""
if not tool_trace and not output:
return
tool_log = tool_log_context.get_active_tool_log()
if tool_log is None:
return
tool_log.append(
{
"name": "llm_tool_trace",
"arguments": {
"provider": provider,
"tool_call_count": len(tool_trace),
},
"response": {
"tool_trace": tool_trace,
"response_ids": result.get("response_ids", []),
},
}
)
if output:
message = result.get("message")
tool_log.append(
{
"name": "llm_response_metadata",
"arguments": {"provider": provider},
"response": {
"output_item_types": [_field(item, "type", "") for item in output],
"reasoning_summary_chars": len(reasoning),
"has_message_thinking": isinstance(message, dict) and bool(message.get("thinking")),
},
}
)
if tool_trace:
tool_log.append(
{
"name": "llm_tool_trace",
"arguments": {
"provider": provider,
"tool_call_count": len(tool_trace),
},
"response": {
"tool_trace": tool_trace,
"response_ids": result.get("response_ids", []),
},
}
)
def resolve_mcp_base_url(mcp_settings: dict[str, Any]) -> str:
......@@ -171,18 +244,49 @@ def _tool_to_chat_tool(tool: Any) -> dict[str, Any]:
}
def _append_openai_tool_message(
messages: list[dict[str, Any]],
def _tool_to_response_tool(tool: Any) -> dict[str, Any]:
return {
"type": "function",
"name": _tool_attr(tool, "name", "name"),
"description": _tool_attr(tool, "description", "description") or "",
"parameters": _tool_attr(tool, "input_schema", "inputSchema"),
"strict": False,
}
def _extract_response_message(response: object) -> dict[str, str]:
output_text = response.get("output_text") if isinstance(response, dict) else getattr(response, "output_text", None)
message = {"role": "assistant", "content": output_text or ""}
thinking = _extract_response_thinking(response)
if thinking:
message["thinking"] = thinking
return message
def _openai_reasoning() -> dict[str, str]:
if config.get_llm_thinking_enabled():
return {"effort": "high", "summary": "detailed"}
return {"effort": "none"}
def _response_input_item(item: Any) -> Any:
if isinstance(item, dict):
return item
if hasattr(item, "model_dump"):
return item.model_dump(exclude_none=True)
return item
def _append_response_tool_output(
items: list[dict[str, Any]],
tool_call_id: str,
tool_name: str,
structured_output: Any,
) -> None:
messages.append(
items.append(
{
"role": "tool",
"tool_call_id": tool_call_id,
"name": tool_name,
"content": json.dumps(structured_output),
"type": "function_call_output",
"call_id": tool_call_id,
"output": json.dumps(structured_output),
}
)
......@@ -217,36 +321,42 @@ async def _run_openai_compatible_tool_loop(
client = openai_client or AsyncOpenAI(**client_kwargs)
async def _handler(session, tools) -> dict:
chat_tools = [_tool_to_chat_tool(tool) for tool in tools]
request_messages = [dict(message) for message in messages]
response_tools = [_tool_to_response_tool(tool) for tool in tools]
request_input = [dict(message) for message in messages]
response_ids: list[str] = []
tool_trace: list[dict[str, Any]] = []
while True:
request_kwargs: dict[str, Any] = {
"model": settings.model,
"messages": request_messages,
"tools": chat_tools,
"input": request_input,
"tools": response_tools,
}
if settings.temperature is not None:
request_kwargs["temperature"] = settings.temperature
response = await client.chat.completions.create(**request_kwargs)
response_id = getattr(response, "id", "")
request_kwargs["reasoning"] = _openai_reasoning()
try:
response = await client.responses.create(**request_kwargs)
except BadRequestError as exc:
if "temperature" not in request_kwargs or not _is_unsupported_temperature_error(exc):
raise
request_kwargs.pop("temperature")
response = await client.responses.create(**request_kwargs)
response_id = response.get("id", "") if isinstance(response, dict) else getattr(response, "id", "")
if response_id:
response_ids.append(response_id)
message = response.choices[0].message if response.choices else {}
tool_calls = _extract_tool_calls(message)
tool_calls = _extract_response_tool_calls(response)
if not tool_calls:
return {
"raw": response,
"message": message,
"message": _extract_response_message(response),
"tool_trace": tool_trace,
"response_ids": response_ids,
}
request_messages.append(_normalize_message(message))
request_input.extend(_response_input_item(item) for item in _extract_response_output(response))
for tool_call in tool_calls:
tool_name = _extract_tool_call_name(tool_call)
arguments = _extract_tool_call_arguments(tool_call)
......@@ -259,10 +369,9 @@ async def _run_openai_compatible_tool_loop(
"output": structured_output,
}
)
_append_openai_tool_message(
request_messages,
_append_response_tool_output(
request_input,
_extract_tool_call_id(tool_call),
tool_name,
structured_output,
)
......
......@@ -25,6 +25,7 @@ ollama_stub.AsyncClient = object
openai_stub = types.ModuleType("openai")
openai_stub.OpenAI = object
openai_stub.AsyncOpenAI = object
openai_stub.BadRequestError = RuntimeError
mistralai_stub = types.ModuleType("mistralai")
mistralai_client_stub = types.ModuleType("mistralai.client")
......@@ -90,50 +91,84 @@ from app.deterministic_services.orchestrators.orchestrator_base import ChatState
MESSAGES = [{"role": "user", "content": "Hallo"}]
def _make_openai_response(message, response_id: str = "resp-1", total_tokens: int = 7):
def _make_responses_response(
output_text: str = "",
*,
output: list[object] | None = None,
response_id: str = "resp-1",
total_tokens: int = 7,
):
usage = types.SimpleNamespace(total_tokens=total_tokens)
choice = types.SimpleNamespace(message=message)
return types.SimpleNamespace(id=response_id, choices=[choice], usage=usage)
return types.SimpleNamespace(
id=response_id,
output_text=output_text,
output=output or [],
usage=usage,
)
def _make_openai_tool_call(
def _make_response_tool_call(
name: str,
arguments: str | dict,
tool_call_id: str = "call-1",
):
function = types.SimpleNamespace(name=name, arguments=arguments)
return types.SimpleNamespace(id=tool_call_id, function=function)
return types.SimpleNamespace(
type="function_call",
call_id=tool_call_id,
name=name,
arguments=arguments,
)
def _make_reasoning_item(text: str):
return types.SimpleNamespace(
type="reasoning",
summary=[types.SimpleNamespace(type="summary_text", text=text)],
)
class _FakeOpenAICompletions:
class _FakeOpenAIResponses:
def __init__(self, responses: list[object]) -> None:
self._responses = list(responses)
self.calls: list[dict] = []
async def create(self, **kwargs):
self.calls.append(kwargs)
return self._responses.pop(0)
response = self._responses.pop(0)
if isinstance(response, Exception):
raise response
return response
class _FakeOpenAIClient:
def __init__(self, responses: list[object]) -> None:
completions = _FakeOpenAICompletions(responses)
self.chat = types.SimpleNamespace(completions=completions)
self.responses = _FakeOpenAIResponses(responses)
class _FakeSyncOpenAIClient:
def __init__(self, response: object) -> None:
self.calls: list[dict] = []
self.chat = types.SimpleNamespace(
completions=types.SimpleNamespace(create=self._create)
)
self.responses = types.SimpleNamespace(create=self._create)
self._response = response
def _create(self, **kwargs):
self.calls.append(kwargs)
if isinstance(self._response, list):
response = self._response.pop(0)
if isinstance(response, Exception):
raise response
return response
if isinstance(self._response, Exception):
raise self._response
return self._response
class _FakeBadRequestError(RuntimeError):
def __init__(self, message: str, param: str | None = None) -> None:
super().__init__(message)
self.param = param
class _FakeMistralClient:
calls: list[dict] = []
......@@ -489,14 +524,83 @@ class LLMClientProviderTest(unittest.TestCase):
timeout=30.0,
temperature=None,
)
fake_client = _FakeSyncOpenAIClient(_make_openai_response({"content": "done"}))
fake_client = _FakeSyncOpenAIClient(_make_responses_response("done"))
with patch.dict(os.environ, {"LLM_THINKING_ENABLED": "true"}), patch.object(
llm_client, "OpenAI", return_value=fake_client
):
llm_client._chat_openai_compatible(MESSAGES, settings)
self.assertEqual(fake_client.calls[0]["reasoning_effort"], "medium")
self.assertEqual(fake_client.calls[0]["reasoning"], {"effort": "high", "summary": "detailed"})
def test_openai_compatible_chat_sends_system_as_instructions(self) -> None:
"""Checks that plain Responses input uses text so reasoning summaries are returned."""
settings = config.OpenAIChatSettings(
base_url="https://example.com/v1",
api_key="key",
model="model",
timeout=30.0,
temperature=None,
)
fake_client = _FakeSyncOpenAIClient(_make_responses_response("done"))
with patch.object(llm_client, "OpenAI", return_value=fake_client):
llm_client._chat_openai_compatible(
[
{"role": "system", "content": "Be precise."},
{"role": "user", "content": "Hallo"},
{"role": "assistant", "content": "Hi"},
],
settings,
)
self.assertEqual(fake_client.calls[0]["instructions"], "Be precise.")
self.assertEqual(fake_client.calls[0]["input"], "user: Hallo\n\nassistant: Hi")
def test_openai_compatible_chat_formats_reasoning_as_thinking(self) -> None:
"""Checks that Responses reasoning summaries use the existing thinking format."""
settings = config.OpenAIChatSettings(
base_url="https://example.com/v1",
api_key="key",
model="model",
timeout=30.0,
temperature=None,
)
fake_client = _FakeSyncOpenAIClient(
_make_responses_response("answer", output=[_make_reasoning_item("work")])
)
with patch.dict(os.environ, {"LLM_THINKING_ENABLED": "true"}), patch.object(
llm_client, "OpenAI", return_value=fake_client
):
result = llm_client._chat_openai_compatible(MESSAGES, settings)
self.assertEqual(llm_client.get_message_content(result), "work</think>\n\nanswer")
def test_openai_compatible_chat_retries_without_unsupported_temperature(self) -> None:
"""Checks that Responses models which reject temperature still work."""
settings = config.OpenAIChatSettings(
base_url="https://example.com/v1",
api_key="key",
model="model",
timeout=30.0,
temperature=0.2,
)
fake_client = _FakeSyncOpenAIClient(
[
_FakeBadRequestError("Unsupported parameter: 'temperature'", "temperature"),
_make_responses_response("done"),
]
)
with patch.object(llm_client, "BadRequestError", _FakeBadRequestError), patch.object(
llm_client, "OpenAI", return_value=fake_client
):
result = llm_client._chat_openai_compatible(MESSAGES, settings)
self.assertEqual(result["message"]["content"], "done")
self.assertEqual(fake_client.calls[0]["temperature"], 0.2)
self.assertNotIn("temperature", fake_client.calls[1])
def test_mistral_chat_sets_reasoning_effort_from_env(self) -> None:
"""Checks that Mistral chat maps thinking mode to reasoning effort."""
......@@ -663,8 +767,7 @@ class LLMClientToolHelpersTest(unittest.TestCase):
timeout=30.0,
temperature=0.2,
)
final_message = types.SimpleNamespace(role="assistant", content="done", tool_calls=None)
fake_client = _FakeOpenAIClient([_make_openai_response(final_message)])
fake_client = _FakeOpenAIClient([_make_responses_response("done")])
async def run_test():
async def fake_with_mcp_session(mcp_settings, handler, http_client=None):
......@@ -677,7 +780,7 @@ class LLMClientToolHelpersTest(unittest.TestCase):
openai_client=fake_client,
)
self.assertEqual(result["message"].content, "done")
self.assertEqual(result["message"]["content"], "done")
self.assertEqual(result["response_ids"], ["resp-1"])
self.assertEqual(result["tool_trace"], [])
......@@ -693,17 +796,11 @@ class LLMClientToolHelpersTest(unittest.TestCase):
timeout=30.0,
temperature=0.2,
)
tool_call = _make_openai_tool_call("solve", "{\"x\": 2}")
tool_message = types.SimpleNamespace(
role="assistant",
content="",
tool_calls=[tool_call],
)
final_message = types.SimpleNamespace(role="assistant", content="x=2", tool_calls=None)
tool_call = _make_response_tool_call("solve", "{\"x\": 2}")
fake_client = _FakeOpenAIClient(
[
_make_openai_response(tool_message, response_id="resp-1"),
_make_openai_response(final_message, response_id="resp-2"),
_make_responses_response(output=[tool_call], response_id="resp-1"),
_make_responses_response("x=2", response_id="resp-2"),
]
)
fake_tool = types.SimpleNamespace(
......@@ -724,16 +821,81 @@ class LLMClientToolHelpersTest(unittest.TestCase):
openai_client=fake_client,
)
self.assertEqual(result["message"].content, "x=2")
self.assertEqual(result["message"]["content"], "x=2")
self.assertEqual(result["response_ids"], ["resp-1", "resp-2"])
self.assertEqual(
result["tool_trace"],
[{"name": "solve", "arguments": {"x": 2}, "output": {"result": 2}}],
)
self.assertEqual(session.calls, [("solve", {"x": 2})])
second_call_messages = fake_client.chat.completions.calls[1]["messages"]
self.assertEqual(second_call_messages[-1]["role"], "tool")
self.assertEqual(second_call_messages[-1]["tool_call_id"], "call-1")
second_call_input = fake_client.responses.calls[1]["input"]
self.assertEqual(second_call_input[-1]["type"], "function_call_output")
self.assertEqual(second_call_input[-1]["call_id"], "call-1")
asyncio_result = __import__("asyncio").run(run_test())
self.assertIsNone(asyncio_result)
def test_openai_tool_loop_formats_reasoning_as_thinking(self) -> None:
"""Checks that tool-mode Responses reasoning summaries are preserved."""
settings = config.OpenAIChatSettings(
base_url="https://example.com/v1",
api_key="key",
model="model",
timeout=30.0,
temperature=0.2,
)
fake_client = _FakeOpenAIClient(
[_make_responses_response("done", output=[_make_reasoning_item("checked")])]
)
async def run_test():
async def fake_with_mcp_session(mcp_settings, handler, http_client=None):
return await handler(_FakeSession([]), [])
with patch.object(llm_tool_client, "with_mcp_session", side_effect=fake_with_mcp_session):
result = await llm_tool_client._run_openai_compatible_tool_loop(
MESSAGES,
settings,
openai_client=fake_client,
)
self.assertEqual(llm_client.get_message_content(result), "checked</think>\n\ndone")
asyncio_result = __import__("asyncio").run(run_test())
self.assertIsNone(asyncio_result)
def test_openai_tool_loop_retries_without_unsupported_temperature(self) -> None:
"""Checks that the tool path also tolerates models rejecting temperature."""
settings = config.OpenAIChatSettings(
base_url="https://example.com/v1",
api_key="key",
model="model",
timeout=30.0,
temperature=0.2,
)
fake_client = _FakeOpenAIClient(
[
_FakeBadRequestError("Unsupported parameter: 'temperature'", "temperature"),
_make_responses_response("done"),
]
)
async def run_test():
async def fake_with_mcp_session(mcp_settings, handler, http_client=None):
return await handler(_FakeSession([]), [])
with patch.object(llm_tool_client, "BadRequestError", _FakeBadRequestError), patch.object(
llm_tool_client, "with_mcp_session", side_effect=fake_with_mcp_session
):
result = await llm_tool_client._run_openai_compatible_tool_loop(
MESSAGES,
settings,
openai_client=fake_client,
)
self.assertEqual(result["message"]["content"], "done")
self.assertEqual(fake_client.responses.calls[0]["temperature"], 0.2)
self.assertNotIn("temperature", fake_client.responses.calls[1])
asyncio_result = __import__("asyncio").run(run_test())
self.assertIsNone(asyncio_result)
......@@ -748,15 +910,13 @@ class LLMClientToolHelpersTest(unittest.TestCase):
temperature=0.2,
)
tool_calls = [
_make_openai_tool_call("first", {"a": 1}, tool_call_id="call-1"),
_make_openai_tool_call("second", "{\"b\": 2}", tool_call_id="call-2"),
_make_response_tool_call("first", {"a": 1}, tool_call_id="call-1"),
_make_response_tool_call("second", "{\"b\": 2}", tool_call_id="call-2"),
]
tool_message = types.SimpleNamespace(role="assistant", content="", tool_calls=tool_calls)
final_message = types.SimpleNamespace(role="assistant", content="done", tool_calls=None)
fake_client = _FakeOpenAIClient(
[
_make_openai_response(tool_message, response_id="resp-1"),
_make_openai_response(final_message, response_id="resp-2"),
_make_responses_response(output=tool_calls, response_id="resp-1"),
_make_responses_response("done", response_id="resp-2"),
]
)
fake_tools = [
......@@ -781,7 +941,7 @@ class LLMClientToolHelpersTest(unittest.TestCase):
openai_client=fake_client,
)
self.assertEqual(result["message"].content, "done")
self.assertEqual(result["message"]["content"], "done")
self.assertEqual(
result["tool_trace"],
[
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment