Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
math_tutor_dev
public_math_tutor
Commits
cad4524f
You need to sign in or sign up before continuing.
Commit
cad4524f
authored
Jul 03, 2026
by
Kantz
Browse files
OpenAI reasoning intigiert
parent
fa3e49fd
Changes
3
Hide whitespace changes
Inline
Side-by-side
math-tutor/backend/app/deterministic_services/llm_client.py
View file @
cad4524f
...
@@ -6,15 +6,13 @@ from typing import Any, Callable
...
@@ -6,15 +6,13 @@ from typing import Any, Callable
import
ollama
import
ollama
from
mistralai.client
import
Mistral
from
mistralai.client
import
Mistral
from
openai
import
OpenAI
from
openai
import
BadRequestError
,
OpenAI
from
app
import
config
from
app
import
config
from
app.deterministic_services
import
llm_quota
from
app.deterministic_services
import
llm_quota
from
app.deterministic_services.llm_tool_client
import
(
from
app.deterministic_services.llm_tool_client
import
(
append_tool_trace_log
,
append_tool_trace_log
,
resolve_mcp_base_url
,
run_tool_chat_sync
,
run_tool_chat_sync
,
with_mcp_session
,
)
)
...
@@ -71,6 +69,110 @@ def _get_message_field(message: object, field: str) -> str:
...
@@ -71,6 +69,110 @@ def _get_message_field(message: object, field: str) -> str:
return
getattr
(
message
,
field
)
or
""
return
getattr
(
message
,
field
)
or
""
return
""
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
# Quota
...
@@ -152,11 +254,21 @@ def _chat_openai_compatible(
...
@@ -152,11 +254,21 @@ def _chat_openai_compatible(
settings
=
settings
or
_require_openai_chat_settings
()
settings
=
settings
or
_require_openai_chat_settings
()
timeout
=
settings
.
timeout
or
60.0
timeout
=
settings
.
timeout
or
60.0
client
=
OpenAI
(
api_key
=
settings
.
api_key
,
base_url
=
settings
.
base_url
,
timeout
=
timeout
)
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
:
if
settings
.
temperature
is
not
None
:
kwargs
[
"temperature"
]
=
settings
.
temperature
kwargs
[
"temperature"
]
=
settings
.
temperature
response
=
client
.
chat
.
completions
.
create
(
**
kwargs
)
kwargs
[
"reasoning"
]
=
_openai_reasoning
()
message
=
response
.
choices
[
0
].
message
if
response
.
choices
else
{}
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
}
return
{
"raw"
:
response
,
"message"
:
message
}
...
...
math-tutor/backend/app/deterministic_services/llm_tool_client.py
View file @
cad4524f
...
@@ -6,7 +6,7 @@ import httpx
...
@@ -6,7 +6,7 @@ import httpx
import
ollama
import
ollama
from
mcp.client.session
import
ClientSession
from
mcp.client.session
import
ClientSession
from
mcp.client.streamable_http
import
streamable_http_client
from
mcp.client.streamable_http
import
streamable_http_client
from
openai
import
AsyncOpenAI
from
openai
import
AsyncOpenAI
,
BadRequestError
from
app
import
config
from
app
import
config
from
app.deterministic_services
import
tool_log_context
from
app.deterministic_services
import
tool_log_context
...
@@ -14,6 +14,15 @@ 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"
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
:
def
_extract_message
(
response
)
->
object
:
if
isinstance
(
response
,
dict
)
and
"message"
in
response
:
if
isinstance
(
response
,
dict
)
and
"message"
in
response
:
return
response
[
"message"
]
return
response
[
"message"
]
...
@@ -30,6 +39,38 @@ def _extract_tool_calls(message: object) -> list[Any]:
...
@@ -30,6 +39,38 @@ def _extract_tool_calls(message: object) -> list[Any]:
return
[]
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
]:
def
_normalize_message
(
message
:
object
)
->
dict
[
str
,
Any
]:
if
isinstance
(
message
,
dict
):
if
isinstance
(
message
,
dict
):
return
dict
(
message
)
return
dict
(
message
)
...
@@ -44,6 +85,10 @@ def _normalize_message(message: object) -> dict[str, Any]:
...
@@ -44,6 +85,10 @@ def _normalize_message(message: object) -> dict[str, Any]:
def
_extract_tool_call_name
(
tool_call
:
object
)
->
str
:
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
)
function
=
getattr
(
tool_call
,
"function"
,
None
)
if
function
is
not
None
and
hasattr
(
function
,
"name"
):
if
function
is
not
None
and
hasattr
(
function
,
"name"
):
return
getattr
(
function
,
"name"
)
return
getattr
(
function
,
"name"
)
...
@@ -54,11 +99,16 @@ def _extract_tool_call_name(tool_call: object) -> str:
...
@@ -54,11 +99,16 @@ def _extract_tool_call_name(tool_call: object) -> str:
def
_extract_tool_call_arguments
(
tool_call
:
object
)
->
dict
[
str
,
Any
]:
def
_extract_tool_call_arguments
(
tool_call
:
object
)
->
dict
[
str
,
Any
]:
function
=
getattr
(
tool_call
,
"function"
,
None
)
raw_arguments
:
Any
=
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"
)
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
{}
function_dict
=
tool_call
.
get
(
"function"
)
or
{}
raw_arguments
=
function_dict
.
get
(
"arguments"
)
raw_arguments
=
function_dict
.
get
(
"arguments"
)
...
@@ -70,6 +120,10 @@ def _extract_tool_call_arguments(tool_call: object) -> dict[str, Any]:
...
@@ -70,6 +120,10 @@ def _extract_tool_call_arguments(tool_call: object) -> dict[str, Any]:
def
_extract_tool_call_id
(
tool_call
:
object
)
->
str
:
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"
):
if
hasattr
(
tool_call
,
"id"
):
return
str
(
getattr
(
tool_call
,
"id"
)
or
""
)
return
str
(
getattr
(
tool_call
,
"id"
)
or
""
)
if
isinstance
(
tool_call
,
dict
):
if
isinstance
(
tool_call
,
dict
):
...
@@ -92,27 +146,46 @@ def _extract_structured_tool_output(tool_result: Any) -> Any:
...
@@ -92,27 +146,46 @@ def _extract_structured_tool_output(tool_result: Any) -> Any:
def
append_tool_trace_log
(
result
:
dict
,
provider
:
str
)
->
None
:
def
append_tool_trace_log
(
result
:
dict
,
provider
:
str
)
->
None
:
tool_trace
=
result
.
get
(
"tool_trace"
)
tool_trace
=
result
.
get
(
"tool_trace"
)
or
[]
if
not
tool_trace
:
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
return
tool_log
=
tool_log_context
.
get_active_tool_log
()
tool_log
=
tool_log_context
.
get_active_tool_log
()
if
tool_log
is
None
:
if
tool_log
is
None
:
return
return
tool_log
.
append
(
if
output
:
{
message
=
result
.
get
(
"message"
)
"name"
:
"llm_tool_trace"
,
tool_log
.
append
(
"arguments"
:
{
{
"provider"
:
provider
,
"name"
:
"llm_response_metadata"
,
"tool_call_count"
:
len
(
tool_trace
),
"arguments"
:
{
"provider"
:
provider
},
},
"response"
:
{
"response"
:
{
"output_item_types"
:
[
_field
(
item
,
"type"
,
""
)
for
item
in
output
],
"tool_trace"
:
tool_trace
,
"reasoning_summary_chars"
:
len
(
reasoning
),
"response_ids"
:
result
.
get
(
"response_ids"
,
[]),
"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
:
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]:
...
@@ -171,18 +244,49 @@ def _tool_to_chat_tool(tool: Any) -> dict[str, Any]:
}
}
def
_append_openai_tool_message
(
def
_tool_to_response_tool
(
tool
:
Any
)
->
dict
[
str
,
Any
]:
messages
:
list
[
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_call_id
:
str
,
tool_name
:
str
,
structured_output
:
Any
,
structured_output
:
Any
,
)
->
None
:
)
->
None
:
message
s
.
append
(
item
s
.
append
(
{
{
"role"
:
"tool"
,
"type"
:
"function_call_output"
,
"tool_call_id"
:
tool_call_id
,
"call_id"
:
tool_call_id
,
"name"
:
tool_name
,
"output"
:
json
.
dumps
(
structured_output
),
"content"
:
json
.
dumps
(
structured_output
),
}
}
)
)
...
@@ -217,36 +321,42 @@ async def _run_openai_compatible_tool_loop(
...
@@ -217,36 +321,42 @@ async def _run_openai_compatible_tool_loop(
client
=
openai_client
or
AsyncOpenAI
(
**
client_kwargs
)
client
=
openai_client
or
AsyncOpenAI
(
**
client_kwargs
)
async
def
_handler
(
session
,
tools
)
->
dict
:
async
def
_handler
(
session
,
tools
)
->
dict
:
chat
_tools
=
[
_tool_to_
chat
_tool
(
tool
)
for
tool
in
tools
]
response
_tools
=
[
_tool_to_
response
_tool
(
tool
)
for
tool
in
tools
]
request_
messages
=
[
dict
(
message
)
for
message
in
messages
]
request_
input
=
[
dict
(
message
)
for
message
in
messages
]
response_ids
:
list
[
str
]
=
[]
response_ids
:
list
[
str
]
=
[]
tool_trace
:
list
[
dict
[
str
,
Any
]]
=
[]
tool_trace
:
list
[
dict
[
str
,
Any
]]
=
[]
while
True
:
while
True
:
request_kwargs
:
dict
[
str
,
Any
]
=
{
request_kwargs
:
dict
[
str
,
Any
]
=
{
"model"
:
settings
.
model
,
"model"
:
settings
.
model
,
"
messages
"
:
request_
messages
,
"
input
"
:
request_
input
,
"tools"
:
chat
_tools
,
"tools"
:
response
_tools
,
}
}
if
settings
.
temperature
is
not
None
:
if
settings
.
temperature
is
not
None
:
request_kwargs
[
"temperature"
]
=
settings
.
temperature
request_kwargs
[
"temperature"
]
=
settings
.
temperature
request_kwargs
[
"reasoning"
]
=
_openai_reasoning
()
response
=
await
client
.
chat
.
completions
.
create
(
**
request_kwargs
)
response_id
=
getattr
(
response
,
"id"
,
""
)
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
:
if
response_id
:
response_ids
.
append
(
response_id
)
response_ids
.
append
(
response_id
)
message
=
response
.
choices
[
0
].
message
if
response
.
choices
else
{}
tool_calls
=
_extract_response_tool_calls
(
response
)
tool_calls
=
_extract_tool_calls
(
message
)
if
not
tool_calls
:
if
not
tool_calls
:
return
{
return
{
"raw"
:
response
,
"raw"
:
response
,
"message"
:
message
,
"message"
:
_extract_response_message
(
response
)
,
"tool_trace"
:
tool_trace
,
"tool_trace"
:
tool_trace
,
"response_ids"
:
response_ids
,
"response_ids"
:
response_ids
,
}
}
request_
messages
.
append
(
_normalize_message
(
messag
e
))
request_
input
.
extend
(
_response_input_item
(
item
)
for
item
in
_extract_response_output
(
respons
e
))
for
tool_call
in
tool_calls
:
for
tool_call
in
tool_calls
:
tool_name
=
_extract_tool_call_name
(
tool_call
)
tool_name
=
_extract_tool_call_name
(
tool_call
)
arguments
=
_extract_tool_call_arguments
(
tool_call
)
arguments
=
_extract_tool_call_arguments
(
tool_call
)
...
@@ -259,10 +369,9 @@ async def _run_openai_compatible_tool_loop(
...
@@ -259,10 +369,9 @@ async def _run_openai_compatible_tool_loop(
"output"
:
structured_output
,
"output"
:
structured_output
,
}
}
)
)
_append_
openai_tool_message
(
_append_
response_tool_output
(
request_
messages
,
request_
input
,
_extract_tool_call_id
(
tool_call
),
_extract_tool_call_id
(
tool_call
),
tool_name
,
structured_output
,
structured_output
,
)
)
...
...
math-tutor/backend/test/test_llm_provider.py
View file @
cad4524f
...
@@ -25,6 +25,7 @@ ollama_stub.AsyncClient = object
...
@@ -25,6 +25,7 @@ ollama_stub.AsyncClient = object
openai_stub
=
types
.
ModuleType
(
"openai"
)
openai_stub
=
types
.
ModuleType
(
"openai"
)
openai_stub
.
OpenAI
=
object
openai_stub
.
OpenAI
=
object
openai_stub
.
AsyncOpenAI
=
object
openai_stub
.
AsyncOpenAI
=
object
openai_stub
.
BadRequestError
=
RuntimeError
mistralai_stub
=
types
.
ModuleType
(
"mistralai"
)
mistralai_stub
=
types
.
ModuleType
(
"mistralai"
)
mistralai_client_stub
=
types
.
ModuleType
(
"mistralai.client"
)
mistralai_client_stub
=
types
.
ModuleType
(
"mistralai.client"
)
...
@@ -90,50 +91,84 @@ from app.deterministic_services.orchestrators.orchestrator_base import ChatState
...
@@ -90,50 +91,84 @@ from app.deterministic_services.orchestrators.orchestrator_base import ChatState
MESSAGES
=
[{
"role"
:
"user"
,
"content"
:
"Hallo"
}]
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
)
usage
=
types
.
SimpleNamespace
(
total_tokens
=
total_tokens
)
choice
=
types
.
SimpleNamespace
(
message
=
message
)
return
types
.
SimpleNamespace
(
return
types
.
SimpleNamespace
(
id
=
response_id
,
choices
=
[
choice
],
usage
=
usage
)
id
=
response_id
,
output_text
=
output_text
,
output
=
output
or
[],
usage
=
usage
,
)
def
_make_
openai
_tool_call
(
def
_make_
response
_tool_call
(
name
:
str
,
name
:
str
,
arguments
:
str
|
dict
,
arguments
:
str
|
dict
,
tool_call_id
:
str
=
"call-1"
,
tool_call_id
:
str
=
"call-1"
,
):
):
function
=
types
.
SimpleNamespace
(
name
=
name
,
arguments
=
arguments
)
return
types
.
SimpleNamespace
(
return
types
.
SimpleNamespace
(
id
=
tool_call_id
,
function
=
function
)
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
:
def
__init__
(
self
,
responses
:
list
[
object
])
->
None
:
self
.
_responses
=
list
(
responses
)
self
.
_responses
=
list
(
responses
)
self
.
calls
:
list
[
dict
]
=
[]
self
.
calls
:
list
[
dict
]
=
[]
async
def
create
(
self
,
**
kwargs
):
async
def
create
(
self
,
**
kwargs
):
self
.
calls
.
append
(
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
:
class
_FakeOpenAIClient
:
def
__init__
(
self
,
responses
:
list
[
object
])
->
None
:
def
__init__
(
self
,
responses
:
list
[
object
])
->
None
:
completions
=
_FakeOpenAICompletions
(
responses
)
self
.
responses
=
_FakeOpenAIResponses
(
responses
)
self
.
chat
=
types
.
SimpleNamespace
(
completions
=
completions
)
class
_FakeSyncOpenAIClient
:
class
_FakeSyncOpenAIClient
:
def
__init__
(
self
,
response
:
object
)
->
None
:
def
__init__
(
self
,
response
:
object
)
->
None
:
self
.
calls
:
list
[
dict
]
=
[]
self
.
calls
:
list
[
dict
]
=
[]
self
.
chat
=
types
.
SimpleNamespace
(
self
.
responses
=
types
.
SimpleNamespace
(
create
=
self
.
_create
)
completions
=
types
.
SimpleNamespace
(
create
=
self
.
_create
)
)
self
.
_response
=
response
self
.
_response
=
response
def
_create
(
self
,
**
kwargs
):
def
_create
(
self
,
**
kwargs
):
self
.
calls
.
append
(
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
return
self
.
_response
class
_FakeBadRequestError
(
RuntimeError
):
def
__init__
(
self
,
message
:
str
,
param
:
str
|
None
=
None
)
->
None
:
super
().
__init__
(
message
)
self
.
param
=
param
class
_FakeMistralClient
:
class
_FakeMistralClient
:
calls
:
list
[
dict
]
=
[]
calls
:
list
[
dict
]
=
[]
...
@@ -489,14 +524,83 @@ class LLMClientProviderTest(unittest.TestCase):
...
@@ -489,14 +524,83 @@ class LLMClientProviderTest(unittest.TestCase):
timeout
=
30.0
,
timeout
=
30.0
,
temperature
=
None
,
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
(
with
patch
.
dict
(
os
.
environ
,
{
"LLM_THINKING_ENABLED"
:
"true"
}),
patch
.
object
(
llm_client
,
"OpenAI"
,
return_value
=
fake_client
llm_client
,
"OpenAI"
,
return_value
=
fake_client
):
):
llm_client
.
_chat_openai_compatible
(
MESSAGES
,
settings
)
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\n
assistant: 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\n
answer"
)
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
:
def
test_mistral_chat_sets_reasoning_effort_from_env
(
self
)
->
None
:
"""Checks that Mistral chat maps thinking mode to reasoning effort."""
"""Checks that Mistral chat maps thinking mode to reasoning effort."""
...
@@ -663,8 +767,7 @@ class LLMClientToolHelpersTest(unittest.TestCase):
...
@@ -663,8 +767,7 @@ class LLMClientToolHelpersTest(unittest.TestCase):
timeout
=
30.0
,
timeout
=
30.0
,
temperature
=
0.2
,
temperature
=
0.2
,
)
)
final_message
=
types
.
SimpleNamespace
(
role
=
"assistant"
,
content
=
"done"
,
tool_calls
=
None
)
fake_client
=
_FakeOpenAIClient
([
_make_responses_response
(
"done"
)])
fake_client
=
_FakeOpenAIClient
([
_make_openai_response
(
final_message
)])
async
def
run_test
():
async
def
run_test
():
async
def
fake_with_mcp_session
(
mcp_settings
,
handler
,
http_client
=
None
):
async
def
fake_with_mcp_session
(
mcp_settings
,
handler
,
http_client
=
None
):
...
@@ -677,7 +780,7 @@ class LLMClientToolHelpersTest(unittest.TestCase):
...
@@ -677,7 +780,7 @@ class LLMClientToolHelpersTest(unittest.TestCase):
openai_client
=
fake_client
,
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
[
"response_ids"
],
[
"resp-1"
])
self
.
assertEqual
(
result
[
"tool_trace"
],
[])
self
.
assertEqual
(
result
[
"tool_trace"
],
[])
...
@@ -693,17 +796,11 @@ class LLMClientToolHelpersTest(unittest.TestCase):
...
@@ -693,17 +796,11 @@ class LLMClientToolHelpersTest(unittest.TestCase):
timeout
=
30.0
,
timeout
=
30.0
,
temperature
=
0.2
,
temperature
=
0.2
,
)
)
tool_call
=
_make_openai_tool_call
(
"solve"
,
"{
\"
x
\"
: 2}"
)
tool_call
=
_make_response_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
)
fake_client
=
_FakeOpenAIClient
(
fake_client
=
_FakeOpenAIClient
(
[
[
_make_
openai
_response
(
tool_message
,
response_id
=
"resp-1"
),
_make_
responses
_response
(
output
=
[
tool_call
]
,
response_id
=
"resp-1"
),
_make_
openai_
response
(
final_message
,
response_id
=
"resp-2"
),
_make_response
s_response
(
"x=2"
,
response_id
=
"resp-2"
),
]
]
)
)
fake_tool
=
types
.
SimpleNamespace
(
fake_tool
=
types
.
SimpleNamespace
(
...
@@ -724,16 +821,81 @@ class LLMClientToolHelpersTest(unittest.TestCase):
...
@@ -724,16 +821,81 @@ class LLMClientToolHelpersTest(unittest.TestCase):
openai_client
=
fake_client
,
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
[
"response_ids"
],
[
"resp-1"
,
"resp-2"
])
self
.
assertEqual
(
self
.
assertEqual
(
result
[
"tool_trace"
],
result
[
"tool_trace"
],
[{
"name"
:
"solve"
,
"arguments"
:
{
"x"
:
2
},
"output"
:
{
"result"
:
2
}}],
[{
"name"
:
"solve"
,
"arguments"
:
{
"x"
:
2
},
"output"
:
{
"result"
:
2
}}],
)
)
self
.
assertEqual
(
session
.
calls
,
[(
"solve"
,
{
"x"
:
2
})])
self
.
assertEqual
(
session
.
calls
,
[(
"solve"
,
{
"x"
:
2
})])
second_call_messages
=
fake_client
.
chat
.
completions
.
calls
[
1
][
"messages"
]
second_call_input
=
fake_client
.
responses
.
calls
[
1
][
"input"
]
self
.
assertEqual
(
second_call_messages
[
-
1
][
"role"
],
"tool"
)
self
.
assertEqual
(
second_call_input
[
-
1
][
"type"
],
"function_call_output"
)
self
.
assertEqual
(
second_call_messages
[
-
1
][
"tool_call_id"
],
"call-1"
)
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\n
done"
)
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
())
asyncio_result
=
__import__
(
"asyncio"
).
run
(
run_test
())
self
.
assertIsNone
(
asyncio_result
)
self
.
assertIsNone
(
asyncio_result
)
...
@@ -748,15 +910,13 @@ class LLMClientToolHelpersTest(unittest.TestCase):
...
@@ -748,15 +910,13 @@ class LLMClientToolHelpersTest(unittest.TestCase):
temperature
=
0.2
,
temperature
=
0.2
,
)
)
tool_calls
=
[
tool_calls
=
[
_make_
openai
_tool_call
(
"first"
,
{
"a"
:
1
},
tool_call_id
=
"call-1"
),
_make_
response
_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
(
"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
(
fake_client
=
_FakeOpenAIClient
(
[
[
_make_
openai
_response
(
tool_message
,
response_id
=
"resp-1"
),
_make_
responses
_response
(
output
=
tool_calls
,
response_id
=
"resp-1"
),
_make_
openai
_response
(
final_message
,
response_id
=
"resp-2"
),
_make_
responses
_response
(
"done"
,
response_id
=
"resp-2"
),
]
]
)
)
fake_tools
=
[
fake_tools
=
[
...
@@ -781,7 +941,7 @@ class LLMClientToolHelpersTest(unittest.TestCase):
...
@@ -781,7 +941,7 @@ class LLMClientToolHelpersTest(unittest.TestCase):
openai_client
=
fake_client
,
openai_client
=
fake_client
,
)
)
self
.
assertEqual
(
result
[
"message"
]
.
content
,
"done"
)
self
.
assertEqual
(
result
[
"message"
]
[
"
content
"
]
,
"done"
)
self
.
assertEqual
(
self
.
assertEqual
(
result
[
"tool_trace"
],
result
[
"tool_trace"
],
[
[
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment