Commit 13e8e80
authored
fix(inference, anthropic): emit tool messages before user text in translation (#6367)
# What does this PR do?
`convert_single_message` flushed a user turn's accumulated text as a
`user` message as soon as it hit a `tool_result` block. When a client
puts its text before the tool result, that lands a `user` message
between the assistant `tool_calls` message and the tool result:
```text
assistant tool_calls=[toolu_123]
user "and New York?"
tool tool_call_id=toolu_123
```
OpenAI rejects that ordering — `Invalid parameter: messages with role
'tool' must be a response to a preceding message with 'tool_calls'` — so
every such request fails once translated, and the caller sees a provider
error rather than an answer.
Anthropic only *recommends* placing `tool_result` blocks first in the
user turn; it accepts them after text, and
`AnthropicCreateMessageRequest` accepts them too. Two `tool_result`
blocks separated by text were worse still: they came out as `tool, user,
tool`.
Tool messages are now emitted first whatever order the blocks arrived
in, and the remainder of the turn follows as a single `user` message.
Text blocks keep their relative order, and images promoted out of
`tool_result` content still land in that trailing user message.
Only the ordering changes. A turn that carries just `tool_result`
blocks, or just text, is untouched.
## Test Plan
Two cases added to
`tests/unit/providers/utils/inference/test_anthropic_translation.py`.
Both fail on `main` and pass here:
```console
$ uv run pytest tests/unit/providers/utils/inference/test_anthropic_translation.py -q
51 passed in 0.09s
# on main, with the two new cases applied:
2 failed, 49 passed in 0.12s
FAILED ...::test_tool_result_after_text_is_emitted_before_the_user_text
FAILED ...::test_tool_results_split_by_text_stay_adjacent
```
Script, `demo.py`:
```python
from ogx.providers.utils.inference.anthropic_translation import anthropic_request_to_openai
from ogx_api.messages.models import (
AnthropicCreateMessageRequest,
AnthropicMessage,
AnthropicTextBlock,
AnthropicToolResultBlock,
AnthropicToolUseBlock,
)
request = AnthropicCreateMessageRequest(
model="m",
max_tokens=100,
messages=[
AnthropicMessage(role="user", content="What is the weather in SF?"),
AnthropicMessage(
role="assistant",
content=[AnthropicToolUseBlock(id="toolu_123", name="get_weather", input={"city": "SF"})],
),
AnthropicMessage(
role="user",
content=[
AnthropicTextBlock(text="and New York?"),
AnthropicToolResultBlock(tool_use_id="toolu_123", content="72F and sunny"),
],
),
],
)
messages = [m.model_dump(exclude_none=True) for m in anthropic_request_to_openai(request).messages]
for i, m in enumerate(messages):
extra = f" tool_call_id={m['tool_call_id']}" if m.get("tool_call_id") else ""
extra += " tool_calls=yes" if m.get("tool_calls") else ""
print(f"{i}: role={m['role']}{extra} content={str(m.get('content'))[:40]!r}")
previous = None
for m in messages:
if m["role"] == "tool":
ok = previous is not None and (
previous["role"] == "tool" or (previous["role"] == "assistant" and previous.get("tool_calls"))
)
if not ok:
raise SystemExit(f"\nINVALID: tool message preceded by role={previous['role'] if previous else None}")
previous = m
print("\nVALID: every tool message follows the assistant tool_calls message or another tool message")
```
On `main`:
```console
$ uv run python demo.py
0: role=user content='What is the weather in SF?'
1: role=assistant tool_calls=yes content='None'
2: role=user content='and New York?'
3: role=tool tool_call_id=toolu_123 content='72F and sunny'
INVALID: tool message preceded by role=user
```
On this branch:
```console
$ uv run python demo.py
0: role=user content='What is the weather in SF?'
1: role=assistant tool_calls=yes content='None'
2: role=tool tool_call_id=toolu_123 content='72F and sunny'
3: role=user content='and New York?'
VALID: every tool message follows the assistant tool_calls message or another tool message
```
Full unit suite, unchanged apart from the two new cases:
```console
$ uv run pytest tests/unit/ -q
1 failed, 3260 passed, 5 skipped, 3 xfailed, 61 errors # this branch
1 failed, 3258 passed, 5 skipped, 3 xfailed, 61 errors # main
```
The one failure and the 61 errors are `tests/unit/providers/vector_io/`
on both sides — `_create_sqlite_connection` cannot load `sqlite_vec` on
this macOS box. Not related to this change.
`ruff check`, `ruff format --check` and `mypy` are clean on both files,
and `pre-commit run --files <both files>` passes every hook.
Signed-off-by: LuShadowX <xshadowlu13@gmail.com>1 parent a7de599 commit 13e8e80
2 files changed
Lines changed: 83 additions & 25 deletions
File tree
- src/ogx/providers/utils/inference
- tests/unit/providers/utils/inference
Lines changed: 21 additions & 25 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
121 | 121 | | |
122 | 122 | | |
123 | 123 | | |
124 | | - | |
125 | | - | |
126 | | - | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
127 | 132 | | |
128 | 133 | | |
129 | 134 | | |
130 | | - | |
131 | | - | |
132 | | - | |
133 | | - | |
134 | | - | |
135 | | - | |
136 | | - | |
137 | | - | |
138 | | - | |
139 | | - | |
140 | | - | |
141 | 135 | | |
142 | | - | |
143 | 136 | | |
144 | 137 | | |
145 | 138 | | |
146 | 139 | | |
147 | 140 | | |
148 | 141 | | |
149 | | - | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
150 | 147 | | |
151 | | - | |
| 148 | + | |
152 | 149 | | |
153 | 150 | | |
154 | 151 | | |
155 | 152 | | |
156 | 153 | | |
157 | 154 | | |
158 | | - | |
159 | | - | |
160 | 155 | | |
161 | | - | |
| 156 | + | |
162 | 157 | | |
163 | | - | |
| 158 | + | |
164 | 159 | | |
165 | | - | |
166 | | - | |
167 | | - | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
168 | 164 | | |
169 | | - | |
| 165 | + | |
170 | 166 | | |
171 | 167 | | |
172 | 168 | | |
| |||
Lines changed: 62 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
260 | 260 | | |
261 | 261 | | |
262 | 262 | | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
263 | 325 | | |
264 | 326 | | |
265 | 327 | | |
| |||
0 commit comments