项目文件夹

文件
Ali Khokhar af12e7b2bb Coordinate provider recovery under concurrent load (#1205)
## Problem

Concurrent transient failures could start independent retry, replay,
continuation, and repair loops while holding provider concurrency slots.
This multiplied upstream attempts and could delay or strand terminal
errors under fan-out.

## Changes

| Before | After |
| --- | --- |
| Retry paths owned separate attempt budgets. | One logical-execution
session caps all upstream work at five attempts. |
| Concurrent failures backed off independently. | One provider-owned
recovery episode elects a single half-open probe while followers
coalesce. |
| Backoff occupied stream concurrency. | Concurrency is held only while
an upstream operation or stream is active. |
| Provider catalog calls and stream creation used separate admission
paths. | Every upstream operation uses one provider-generation admission
controller. |
| Cancellation could leave recovery ownership or follower state
unresolved. | Cancellation releases permits, transfers probe ownership,
and unregisters waiting followers. |
| Late in-flight failures could cross an exhausted episode boundary. |
Every coalesced execution retains that generation's terminal outcome. |
| Replay tests allowed loose lifecycle assertions. | Exact SSE contracts
prove retries and continuations emit one unduplicated response. |
| Recovery wrappers could mask final diagnostics. | Final responses and
traces retain the raw provider failure and request ID. |

<!-- greptile_comment -->

<details open><summary><h3>Greptile Summary</h3></summary>

This PR coordinates provider recovery and retry work under concurrent
load. The main changes are:

- One five-attempt budget for each logical execution.
- Provider-wide recovery episodes with one elected probe.
- Shared admission for streams, catalog calls, rate limits, and
concurrency.
- Concurrency permits held only during active upstream work.
- Cancellation-safe probe ownership and preserved final diagnostics.
</details>

<h3>Confidence Score: 5/5</h3>

This looks safe to merge.

No blocking issues found in the changed code.

<details><summary><h3><a href="https://www.greptile.com/trex"><img
alt="T-Rex"
src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg"
height="20" align="absmiddle"></a> T-Rex Logs</h3></summary>

**What T-Rex did**
- Reviewed the coordinated-recovery-01-before.log to understand how the
exhausted generation outcome was not preserved in a late in-flight
failure.
- Reviewed the coordinated-recovery-02-after.log to confirm that the
updated implementation preserves the exhausted generation outcome for
the same focused contract set.
- Validated that the provider-admission-full-current.log shows the
complete requested test file passed under Python 3.14 with uv run pytest
-n 0.

<a
href="https://app.greptile.com/trex/runs/15050270/artifacts"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifactsDark.svg?v=4"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=4"><img
alt="View all artifacts"
src="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=4"></picture></a>

<sub><a href="https://www.greptile.com/trex"><img alt="T-Rex"
src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg"
height="14" align="absmiddle"></a> Ran code and verified through
T-Rex</sub>
</details>

<details open><summary><h3>Important Files Changed</h3></summary>

| Filename | Overview |
|----------|----------|
| src/free_claude_code/providers/admission.py | Adds shared admission,
retry budgets, recovery episodes, probe election, and cancellation
handling. |
| src/free_claude_code/providers/openai_chat/provider.py | Moves stream
creation, replay, continuation, and repair onto one admission-owned
retry session. |
| src/free_claude_code/providers/stream_recovery.py | Selects replay,
continuation, repair, or final failure using the remaining shared
attempt budget. |
| src/free_claude_code/providers/failure_policy.py | Adds recovery
exhaustion handling and preserves the underlying provider error for
final classification. |
| src/free_claude_code/providers/runtime/factory.py | Creates one
admission controller per provider generation and passes it through
provider factories. |

</details>

<details open><summary><h3>Sequence Diagram</h3></summary>

<a href="#gh-light-mode-only">

```mermaid
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant E as Execution
participant A as Admission controller
participant P as Provider
participant F as Concurrent follower

E->>A: Open attempt
A->>P: Send upstream request
P-->>E: Retryable failure
E->>A: Open recovery episode
F->>A: Request admission
A-->>F: Coalesce and wait
E->>A: Claim probe
A->>P: Send half-open probe
alt Probe succeeds
    P-->>E: Valid response
    E->>A: Close recovery episode
    A-->>F: Release waiter
else Probe fails
    P-->>E: Retryable failure
    E->>A: Schedule next probe or finalize error
end
```

</a>
<a href="#gh-dark-mode-only">

```mermaid
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant E as Execution
participant A as Admission controller
participant P as Provider
participant F as Concurrent follower

E->>A: Open attempt
A->>P: Send upstream request
P-->>E: Retryable failure
E->>A: Open recovery episode
F->>A: Request admission
A-->>F: Coalesce and wait
E->>A: Claim probe
A->>P: Send half-open probe
alt Probe succeeds
    P-->>E: Valid response
    E->>A: Close recovery episode
    A-->>F: Release waiter
else Probe fails
    P-->>E: Retryable failure
    E->>A: Schedule next probe or finalize error
end
```

</a>
</details>

<sub>Reviews (2): Last reviewed commit: ["Harden coordinated retry
lifecycle
invar..."](https://github.com/alishahryar1/free-claude-code/commit/2e871c8649d148b5eb71d21f80bf870ae2d11708)
| [Re-trigger
Greptile](https://app.greptile.com/api/retrigger?id=45554917)</sub>

<!-- /greptile_comment -->
2026-07-19 22:43:29 -07:00

960 行
30 KiB
Python

import json
from unittest.mock import AsyncMock, MagicMock, patch
import openai
import pytest
from httpx import Request, Response
from free_claude_code.config.nim import NimSettings
from free_claude_code.config.provider_catalog import NVIDIA_NIM_DEFAULT_BASE
from free_claude_code.core.failures import ExecutionFailure
from free_claude_code.core.reasoning import ReasoningEffort, ReasoningPolicy
from free_claude_code.providers.admission import UPSTREAM_TRANSIENT_TOTAL_ATTEMPTS
from free_claude_code.providers.nvidia_nim import NvidiaNimProvider
from free_claude_code.providers.nvidia_nim.tool_schema import (
NIM_TOOL_ARGUMENT_ALIASES_KEY,
)
from tests.providers.request_factory import make_messages_request
from tests.providers.support import (
REASONING_OFF,
REASONING_ON,
immediate_admission,
reasoning_for,
)
def message(role, content):
return {"role": role, "content": content}
def tool(name, description, input_schema):
return {"name": name, "description": description, "input_schema": input_schema}
def block(**fields):
return fields
def make_request(**overrides):
model = overrides.pop("model", "test-model")
overrides.setdefault("stop_sequences", ["STOP"])
return make_messages_request(model, **overrides)
def _input_json_deltas(events):
deltas = []
for event in events:
if "event: content_block_delta" not in event:
continue
for line in event.splitlines():
if not line.startswith("data: "):
continue
payload = json.loads(line[6:])
delta = payload.get("delta", {})
if delta.get("type") == "input_json_delta":
deltas.append(delta.get("partial_json", ""))
return deltas
def _tool_call_chunk(
*,
name,
arguments,
tool_id="call_1",
index=0,
finish_reason=None,
):
mock_tc = MagicMock()
mock_tc.index = index
mock_tc.id = tool_id
mock_tc.function.name = name
mock_tc.function.arguments = arguments
mock_chunk = MagicMock()
mock_chunk.choices = [
MagicMock(
delta=MagicMock(content=None, reasoning_content="", tool_calls=[mock_tc]),
finish_reason=finish_reason,
)
]
mock_chunk.usage = None
return mock_chunk
def _make_bad_request_error(message: str) -> openai.BadRequestError:
response = Response(
status_code=400,
request=Request("POST", f"{NVIDIA_NIM_DEFAULT_BASE}/chat/completions"),
)
body = {"error": {"message": message, "type": "BadRequestError", "code": 400}}
return openai.BadRequestError(message, response=response, body=body)
def _make_internal_server_error(message: str) -> openai.InternalServerError:
response = Response(
status_code=500,
request=Request("POST", f"{NVIDIA_NIM_DEFAULT_BASE}/chat/completions"),
)
body = {
"error": {
"message": message,
"type": "internal_server_error",
"code": 500,
}
}
return openai.InternalServerError(message, response=response, body=body)
@pytest.mark.asyncio
async def test_init(provider_config):
"""Test provider initialization."""
with patch(
"free_claude_code.providers.openai_chat.provider.AsyncOpenAI"
) as mock_openai:
provider = NvidiaNimProvider(
provider_config,
nim_settings=NimSettings(),
admission=immediate_admission(),
)
assert provider._api_key == "test_key"
assert provider._base_url == "https://test.api.nvidia.com/v1"
mock_openai.assert_called_once()
@pytest.mark.asyncio
async def test_init_uses_configurable_timeouts():
"""Test that provider passes configurable read/write/connect timeouts to client."""
from free_claude_code.providers.base import ProviderConfig
config = ProviderConfig(
api_key="test_key",
base_url="https://test.api.nvidia.com/v1",
http_read_timeout=600.0,
http_write_timeout=15.0,
http_connect_timeout=5.0,
)
with patch(
"free_claude_code.providers.openai_chat.provider.AsyncOpenAI"
) as mock_openai:
NvidiaNimProvider(
config, nim_settings=NimSettings(), admission=immediate_admission()
)
call_kwargs = mock_openai.call_args[1]
timeout = call_kwargs["timeout"]
assert timeout.read == 600.0
assert timeout.write == 15.0
assert timeout.connect == 5.0
@pytest.mark.asyncio
async def test_build_request_body(provider_config):
"""Test request body construction."""
provider = NvidiaNimProvider(
provider_config,
nim_settings=NimSettings(),
admission=immediate_admission(),
)
req = make_request()
body = provider._build_request_body(req, reasoning=reasoning_for(req))
assert body["model"] == "test-model"
assert body["temperature"] == 0.5
assert len(body["messages"]) == 2 # System + User
assert body["messages"][0]["role"] == "system"
assert body["messages"][0]["content"] == "System prompt"
assert "extra_body" in body
ctk = body["extra_body"]["chat_template_kwargs"]
assert ctk["thinking"] is True
assert ctk["enable_thinking"] is True
assert "reasoning_budget" not in ctk
assert "reasoning_budget" not in body["extra_body"]
@pytest.mark.asyncio
async def test_build_request_body_encodes_explicit_reasoning_off(
provider_config,
):
provider = NvidiaNimProvider(
provider_config,
nim_settings=NimSettings(),
admission=immediate_admission(),
)
req = make_request()
body = provider._build_request_body(req, reasoning=REASONING_OFF)
extra = body.get("extra_body", {})
assert extra["chat_template_kwargs"] == {
"thinking": False,
"enable_thinking": False,
}
assert "reasoning_budget" not in extra
@pytest.mark.asyncio
async def test_build_request_body_omits_reasoning_when_request_disables_thinking(
provider_config,
):
provider = NvidiaNimProvider(
provider_config,
nim_settings=NimSettings(),
admission=immediate_admission(),
)
req = make_request()
req.thinking.enabled = False
body = provider._build_request_body(req)
extra = body.get("extra_body", {})
assert "chat_template_kwargs" not in extra
assert "reasoning_budget" not in extra
def test_preflight_and_build_request_issue_206_post_tool_text(nim_provider):
"""Regression: assistant message with tool_use then text plus tool results (GitHub #206)."""
tool_id = "toolu_issue_206"
req = make_request(
messages=[
message("user", "Use echo once."),
message(
"assistant",
[
block(
type="tool_use",
id=tool_id,
name="echo_smoke",
input={"value": "FCC_206"},
),
block(
type="text",
text="Commentary after the tool row.",
),
],
),
message(
"user",
[
block(type="tool_result", tool_use_id=tool_id, content="FCC_206"),
block(type="text", text="What was echoed?"),
],
),
],
)
nim_provider.preflight_stream(req, reasoning=REASONING_OFF)
body = nim_provider._build_request_body(req, reasoning=REASONING_OFF)
assert "messages" in body
assert any(m.get("role") == "tool" for m in body["messages"])
@pytest.mark.asyncio
async def test_stream_response_text(nim_provider):
"""Test streaming text response."""
req = make_request()
# Create mock chunks
mock_chunk1 = MagicMock()
mock_chunk1.choices = [
MagicMock(
delta=MagicMock(content="Hello", reasoning_content=""), finish_reason=None
)
]
mock_chunk1.usage = None
mock_chunk2 = MagicMock()
mock_chunk2.choices = [
MagicMock(
delta=MagicMock(content=" World", reasoning_content=""),
finish_reason="stop",
)
]
mock_chunk2.usage = MagicMock(completion_tokens=10)
async def mock_stream():
yield mock_chunk1
yield mock_chunk2
with patch.object(
nim_provider._client.chat.completions, "create", new_callable=AsyncMock
) as mock_create:
mock_create.return_value = mock_stream()
events = [e async for e in nim_provider.stream_response(req)]
assert len(events) > 0
assert "event: message_start" in events[0]
text_content = ""
for e in events:
if "event: content_block_delta" in e and '"text_delta"' in e:
for line in e.splitlines():
if line.startswith("data: "):
data = json.loads(line[6:])
if "delta" in data and "text" in data["delta"]:
text_content += data["delta"]["text"]
assert "Hello World" in text_content
@pytest.mark.asyncio
async def test_stream_response_thinking_reasoning_content(nim_provider):
"""Test streaming with native reasoning_content."""
req = make_request()
mock_chunk = MagicMock()
mock_chunk.choices = [
MagicMock(
delta=MagicMock(content=None, reasoning_content="Thinking..."),
finish_reason=None,
)
]
mock_chunk.usage = None
stop_chunk = MagicMock()
stop_chunk.choices = [
MagicMock(
delta=MagicMock(content=None, reasoning_content=None, tool_calls=None),
finish_reason="stop",
)
]
stop_chunk.usage = None
async def mock_stream():
yield mock_chunk
yield stop_chunk
with patch.object(
nim_provider._client.chat.completions, "create", new_callable=AsyncMock
) as mock_create:
mock_create.return_value = mock_stream()
events = [e async for e in nim_provider.stream_response(req)]
# Check for thinking_delta
found_thinking = False
for e in events:
if (
"event: content_block_delta" in e
and '"thinking_delta"' in e
and "Thinking..." in e
):
found_thinking = True
assert found_thinking
@pytest.mark.asyncio
async def test_stream_response_suppresses_thinking_when_disabled(provider_config):
provider = NvidiaNimProvider(
provider_config,
nim_settings=NimSettings(),
admission=immediate_admission(),
)
req = make_request()
mock_chunk = MagicMock()
mock_chunk.choices = [
MagicMock(
delta=MagicMock(
content="<think>secret</think>Answer", reasoning_content="Thinking..."
),
finish_reason="stop",
)
]
mock_chunk.usage = None
async def mock_stream():
yield mock_chunk
with patch.object(
provider._client.chat.completions, "create", new_callable=AsyncMock
) as mock_create:
mock_create.return_value = mock_stream()
events = [
e async for e in provider.stream_response(req, reasoning=REASONING_OFF)
]
event_text = "".join(events)
assert "thinking_delta" not in event_text
assert "Thinking..." not in event_text
assert "secret" not in event_text
assert "Answer" in event_text
def _make_bad_request_error(message: str) -> openai.BadRequestError:
response = Response(status_code=400, request=Request("POST", "http://test"))
body = {"error": {"message": message}}
return openai.BadRequestError(message, response=response, body=body)
@pytest.mark.asyncio
async def test_stream_response_retries_without_chat_template(provider_config):
provider = NvidiaNimProvider(
provider_config,
nim_settings=NimSettings(chat_template="custom_template"),
admission=immediate_admission(),
)
req = make_request(model="mistralai/mixtral-8x7b-instruct-v0.1")
mock_chunk = MagicMock()
mock_chunk.choices = [
MagicMock(
delta=MagicMock(content="OK", reasoning_content=""),
finish_reason="stop",
)
]
mock_chunk.usage = MagicMock(completion_tokens=2)
async def mock_stream():
yield mock_chunk
first_error = _make_bad_request_error(
"chat_template is not supported for Mistral tokenizers."
)
with patch.object(
provider._client.chat.completions, "create", new_callable=AsyncMock
) as mock_create:
mock_create.side_effect = [first_error, mock_stream()]
events = [
e async for e in provider.stream_response(req, reasoning=REASONING_ON)
]
assert mock_create.await_count == 2
first_extra = mock_create.call_args_list[0].kwargs["extra_body"]
second_extra = mock_create.call_args_list[1].kwargs["extra_body"]
assert first_extra["chat_template"] == "custom_template"
assert first_extra["chat_template_kwargs"] == {
"thinking": True,
"enable_thinking": True,
}
assert "reasoning_budget" not in first_extra
assert "chat_template" not in second_extra
assert "chat_template_kwargs" not in second_extra
assert "reasoning_budget" not in second_extra
event_text = "".join(events)
assert "event: error" not in event_text
assert "OK" in event_text
@pytest.mark.asyncio
async def test_stream_response_retries_without_chat_template_kwargs_issue_993(
provider_config,
):
provider = NvidiaNimProvider(
provider_config,
nim_settings=NimSettings(),
admission=immediate_admission(),
)
req = make_request(model="mistralai/mistral-small-4-119b-2603")
mock_chunk = MagicMock()
mock_chunk.choices = [
MagicMock(
delta=MagicMock(content="OK", reasoning_content=""),
finish_reason="stop",
)
]
mock_chunk.usage = MagicMock(completion_tokens=2)
async def mock_stream():
yield mock_chunk
first_error = _make_bad_request_error(
"chat_template is not supported for Mistral tokenizers."
)
with patch.object(
provider._client.chat.completions, "create", new_callable=AsyncMock
) as mock_create:
mock_create.side_effect = [first_error, mock_stream()]
events = [
e async for e in provider.stream_response(req, reasoning=REASONING_ON)
]
assert mock_create.await_count == 2
first_extra = mock_create.call_args_list[0].kwargs["extra_body"]
second_kwargs = mock_create.call_args_list[1].kwargs
assert "chat_template" not in first_extra
assert first_extra["chat_template_kwargs"] == {
"thinking": True,
"enable_thinking": True,
}
second_extra = second_kwargs.get("extra_body") or {}
assert "chat_template" not in second_extra
assert "chat_template_kwargs" not in second_extra
event_text = "".join(events)
assert "event: error" not in event_text
assert "OK" in event_text
@pytest.mark.asyncio
async def test_stream_response_does_not_retry_unrelated_bad_request(provider_config):
provider = NvidiaNimProvider(
provider_config,
nim_settings=NimSettings(chat_template="custom_template"),
admission=immediate_admission(),
)
req = make_request(model="mistralai/mixtral-8x7b-instruct-v0.1")
with patch.object(
provider._client.chat.completions, "create", new_callable=AsyncMock
) as mock_create:
mock_create.side_effect = _make_bad_request_error("unrelated bad request")
with pytest.raises(ExecutionFailure) as exc_info:
[e async for e in provider.stream_response(req)]
assert mock_create.await_count == 1
assert "Invalid request sent to provider" in exc_info.value.message
@pytest.mark.asyncio
async def test_tool_call_stream(nim_provider):
"""Test streaming tool calls."""
req = make_request()
# Mock tool call delta
mock_tc = MagicMock()
mock_tc.index = 0
mock_tc.id = "call_1"
mock_tc.function.name = "search"
mock_tc.function.arguments = '{"q": "test"}'
mock_chunk = MagicMock()
mock_chunk.choices = [
MagicMock(
delta=MagicMock(content=None, reasoning_content="", tool_calls=[mock_tc]),
finish_reason=None,
)
]
mock_chunk.usage = None
async def mock_stream():
yield mock_chunk
with patch.object(
nim_provider._client.chat.completions, "create", new_callable=AsyncMock
) as mock_create:
mock_create.return_value = mock_stream()
events = [e async for e in nim_provider.stream_response(req)]
starts = [
e for e in events if "event: content_block_start" in e and '"tool_use"' in e
]
assert len(starts) == 1
assert "search" in starts[0]
@pytest.mark.asyncio
async def test_stream_response_restores_aliased_tool_arguments(nim_provider):
"""NIM-safe argument aliases are restored before Anthropic SSE emission."""
req = make_request(
tools=[
tool(
"Grep",
"Search file contents",
{
"type": "object",
"properties": {
"pattern": {"type": "string"},
"-A": {"type": "number"},
"type": {"type": "string"},
},
"required": ["pattern"],
},
)
]
)
mock_chunk = _tool_call_chunk(
name="Grep",
arguments=json.dumps({"pattern": "needle", "-A": 2, "_fcc_arg_type": "py"}),
)
async def mock_stream():
yield mock_chunk
with patch.object(
nim_provider._client.chat.completions, "create", new_callable=AsyncMock
) as mock_create:
mock_create.return_value = mock_stream()
events = [e async for e in nim_provider.stream_response(req)]
await_args = mock_create.await_args
assert await_args is not None
create_kwargs = await_args.kwargs
assert NIM_TOOL_ARGUMENT_ALIASES_KEY not in create_kwargs
properties = create_kwargs["tools"][0]["function"]["parameters"]["properties"]
assert "-A" in properties
assert "type" not in properties
assert "_fcc_arg_A" not in properties
assert "_fcc_arg_type" in properties
deltas = _input_json_deltas(events)
assert len(deltas) == 1
assert json.loads(deltas[0]) == {"pattern": "needle", "-A": 2, "type": "py"}
assert "_fcc_arg_type" not in deltas[0]
@pytest.mark.asyncio
async def test_stream_response_buffers_chunked_aliased_tool_arguments(nim_provider):
"""Chunked aliased args are emitted once as restored Claude Code args."""
req = make_request(
tools=[
tool(
"Grep",
"Search file contents",
{
"type": "object",
"properties": {
"pattern": {"type": "string"},
"type": {"type": "string"},
},
"required": ["pattern"],
},
)
]
)
first_chunk = _tool_call_chunk(
name="Grep",
arguments='{"pattern": "needle", ',
tool_id="call_chunked",
)
second_chunk = _tool_call_chunk(
name=None,
arguments='"_fcc_arg_type": "py"}',
tool_id="call_chunked",
)
async def mock_stream():
yield first_chunk
yield second_chunk
with patch.object(
nim_provider._client.chat.completions, "create", new_callable=AsyncMock
) as mock_create:
mock_create.return_value = mock_stream()
events = [e async for e in nim_provider.stream_response(req)]
deltas = _input_json_deltas(events)
assert len(deltas) == 1
assert json.loads(deltas[0]) == {"pattern": "needle", "type": "py"}
@pytest.mark.asyncio
async def test_stream_response_restores_nested_aliased_tool_arguments(nim_provider):
req = make_request(
tools=[
tool(
"NotionLike",
"Nested type schema",
{
"type": "object",
"properties": {
"parent": {
"type": "object",
"properties": {
"type": {"type": "string"},
"id": {"type": "string"},
},
"required": ["type", "id"],
}
},
"required": ["parent"],
},
)
]
)
mock_chunk = _tool_call_chunk(
name="NotionLike",
arguments=json.dumps(
{"parent": {"_fcc_arg_type": "page_id", "id": "page_123"}}
),
)
async def mock_stream():
yield mock_chunk
with patch.object(
nim_provider._client.chat.completions, "create", new_callable=AsyncMock
) as mock_create:
mock_create.return_value = mock_stream()
events = [e async for e in nim_provider.stream_response(req)]
deltas = _input_json_deltas(events)
assert len(deltas) == 1
assert json.loads(deltas[0]) == {"parent": {"type": "page_id", "id": "page_123"}}
@pytest.mark.asyncio
async def test_stream_response_task_tool_still_forces_background_false(nim_provider):
req = make_request(
tools=[
tool(
"Task",
"Run a subagent",
{
"type": "object",
"properties": {
"description": {"type": "string"},
"prompt": {"type": "string"},
"run_in_background": {"type": "boolean"},
},
"required": ["description", "prompt"],
},
)
]
)
mock_chunk = _tool_call_chunk(
name="Task",
arguments=json.dumps(
{
"description": "Inspect",
"prompt": "Read the marker",
"run_in_background": True,
}
),
tool_id="call_task",
)
async def mock_stream():
yield mock_chunk
with patch.object(
nim_provider._client.chat.completions, "create", new_callable=AsyncMock
) as mock_create:
mock_create.return_value = mock_stream()
events = [e async for e in nim_provider.stream_response(req)]
deltas = _input_json_deltas(events)
assert len(deltas) == 1
assert json.loads(deltas[0])["run_in_background"] is False
@pytest.mark.asyncio
async def test_stream_response_retries_without_reasoning_budget(nim_provider):
req = make_request()
mock_chunk = MagicMock()
mock_chunk.choices = [
MagicMock(
delta=MagicMock(content="Recovered", reasoning_content=""),
finish_reason="stop",
)
]
mock_chunk.usage = MagicMock(completion_tokens=5)
async def mock_stream():
yield mock_chunk
error = _make_bad_request_error("Unsupported field: reasoning_budget")
with patch.object(
nim_provider._client.chat.completions, "create", new_callable=AsyncMock
) as mock_create:
mock_create.side_effect = [error, mock_stream()]
events = [
e
async for e in nim_provider.stream_response(
req,
reasoning=ReasoningPolicy.on(effort=ReasoningEffort.XHIGH),
)
]
assert mock_create.await_count == 2
first_call = mock_create.await_args_list[0].kwargs
second_call = mock_create.await_args_list[1].kwargs
assert first_call["extra_body"]["chat_template_kwargs"]["reasoning_budget"] == 4096
assert "reasoning_budget" not in second_call["extra_body"]
assert "reasoning_budget" not in second_call["extra_body"]["chat_template_kwargs"]
assert second_call["extra_body"]["chat_template_kwargs"]["enable_thinking"] is True
assert any("Recovered" in event for event in events)
assert any("message_stop" in event for event in events)
@pytest.mark.asyncio
async def test_stream_response_retries_without_budget_for_thinking_token_error(
nim_provider,
):
req = make_request(model="meta/llama-3.3-70b-instruct")
mock_chunk = MagicMock()
mock_chunk.choices = [
MagicMock(
delta=MagicMock(content="Recovered", reasoning_content=""),
finish_reason="stop",
)
]
mock_chunk.usage = MagicMock(completion_tokens=5)
async def mock_stream():
yield mock_chunk
error = _make_internal_server_error(
"ValueError: thinking_token_budget is set but reasoning_config is not "
"configured. Please set --reasoning-config to use thinking_token_budget."
)
with patch.object(
nim_provider._client.chat.completions, "create", new_callable=AsyncMock
) as mock_create:
mock_create.side_effect = [error, mock_stream()]
events = [
e
async for e in nim_provider.stream_response(
req, reasoning=ReasoningPolicy.on(budget_tokens=77)
)
]
assert mock_create.await_count == 2
first_call = mock_create.await_args_list[0].kwargs
second_call = mock_create.await_args_list[1].kwargs
assert first_call["extra_body"]["chat_template_kwargs"]["reasoning_budget"] == 77
assert "reasoning_budget" not in second_call["extra_body"]
assert "reasoning_budget" not in second_call["extra_body"]["chat_template_kwargs"]
assert second_call["extra_body"]["chat_template_kwargs"]["thinking"] is True
assert second_call["extra_body"]["chat_template_kwargs"]["enable_thinking"] is True
assert any("Recovered" in event for event in events)
assert any("message_stop" in event for event in events)
@pytest.mark.asyncio
async def test_stream_response_retries_without_reasoning_content(nim_provider):
req = make_request(
system=None,
messages=[
message(
"assistant",
[
block(type="thinking", thinking="Need the tool."),
block(
type="tool_use",
id="toolu_reasoning",
name="echo_smoke",
input={"value": "FCC_TOOL"},
),
],
),
message(
"user",
[
block(
type="tool_result",
tool_use_id="toolu_reasoning",
content="result",
)
],
),
],
)
mock_chunk = MagicMock()
mock_chunk.choices = [
MagicMock(
delta=MagicMock(content="Recovered", reasoning_content=""),
finish_reason="stop",
)
]
mock_chunk.usage = MagicMock(completion_tokens=5)
async def mock_stream():
yield mock_chunk
error = _make_bad_request_error("Unsupported field: reasoning_content")
with patch.object(
nim_provider._client.chat.completions, "create", new_callable=AsyncMock
) as mock_create:
mock_create.side_effect = [error, mock_stream()]
events = [e async for e in nim_provider.stream_response(req)]
assert mock_create.await_count == 2
first_call = mock_create.await_args_list[0].kwargs
second_call = mock_create.await_args_list[1].kwargs
assert first_call["messages"][0]["reasoning_content"] == "Need the tool."
assert "reasoning_content" not in second_call["messages"][0]
assert second_call["messages"][0]["tool_calls"][0]["id"] == "toolu_reasoning"
assert any("Recovered" in event for event in events)
assert any("message_stop" in event for event in events)
@pytest.mark.asyncio
async def test_stream_response_bad_request_without_reasoning_budget_does_not_retry(
nim_provider,
):
req = make_request()
error = _make_bad_request_error("Unsupported field: top_k")
with patch.object(
nim_provider._client.chat.completions, "create", new_callable=AsyncMock
) as mock_create:
mock_create.side_effect = error
with pytest.raises(ExecutionFailure) as exc_info:
[e async for e in nim_provider.stream_response(req)]
assert mock_create.await_count == 1
assert "Invalid request sent to provider" in exc_info.value.message
@pytest.mark.asyncio
async def test_stream_response_unrelated_internal_error_does_not_downgrade(
nim_provider,
):
req = make_request()
error = _make_internal_server_error("unrelated internal provider failure")
with patch.object(
nim_provider._client.chat.completions, "create", new_callable=AsyncMock
) as mock_create:
mock_create.side_effect = error
with pytest.raises(ExecutionFailure) as exc_info:
[e async for e in nim_provider.stream_response(req)]
assert mock_create.await_count == UPSTREAM_TRANSIENT_TOTAL_ATTEMPTS
assert all(
call.kwargs == mock_create.await_args_list[0].kwargs
for call in mock_create.await_args_list
)
assert "Provider API request failed" in exc_info.value.message
@pytest.mark.asyncio
async def test_stream_response_internal_reasoning_content_error_does_not_downgrade(
nim_provider,
):
req = make_request()
error = _make_internal_server_error(
"reasoning_content could not be processed by the upstream model"
)
with patch.object(
nim_provider._client.chat.completions, "create", new_callable=AsyncMock
) as mock_create:
mock_create.side_effect = error
with pytest.raises(ExecutionFailure) as exc_info:
[e async for e in nim_provider.stream_response(req)]
assert mock_create.await_count == UPSTREAM_TRANSIENT_TOTAL_ATTEMPTS
assert all(
call.kwargs == mock_create.await_args_list[0].kwargs
for call in mock_create.await_args_list
)
assert "Provider API request failed" in exc_info.value.message