alishahryar1--free-claude-code
65a342ede4
## Problem FCC preserved named reasoning effort but numeric-budget providers received no intensity unless a client supplied an exact token budget. NIM and llama.cpp therefore treated Low through Max like the same provider default. ## Changes | Before | After | | --- | --- | | Named effort had no FCC-owned numeric meaning. | FCC maps Low, Medium, High, X-High, and Max to 512, 1,024, 2,048, 4,096, and 8,192 tokens. | | NIM received only thinking booleans for named effort. | [NIM](https://docs.nvidia.com/nim/large-language-models/1.15.0/thinking-budget-control.html) receives thinking booleans plus the mapped `reasoning_budget`, retaining its existing retry without rejected budget control. | | llama.cpp forwarded only exact client budgets. | [llama.cpp](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md) receives the mapped `thinking_budget_tokens` value. | | Named, boolean, and provider-default adapters shared no explicit numeric contract. | Named adapters keep words, boolean adapters keep on/off, and only numeric-budget adapters consume the FCC scale. | | Documentation prohibited every named-effort budget conversion. | Documentation defines the product scale, exact-budget precedence, and model-independent ownership boundary. | | Version 4.8.0 exposed tiers that collapsed on numeric providers. | Version 4.8.1 completes the tiers; all five local CI checks pass with 2,383 tests passed and 40 skipped. | <!-- greptile_comment --> <details open><summary><h3>Greptile Summary</h3></summary> This PR gives named reasoning tiers numeric budgets for providers that require token counts. The main changes are: - Adds one shared tier-to-token scale with exact-budget precedence. - Sends mapped budgets to NVIDIA NIM and llama.cpp. - Removes conflicting client-supplied NIM budget fields. - Adds focused provider and policy tests. - Updates documentation and bumps the package to 4.8.1. </details> <h3>Confidence Score: 5/5</h3> This looks safe to merge. The NIM fix removes both conflicting budget locations while preserving unrelated nested options. No blocking issue remains in the changed paths. <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** - T-Rex ran a pre-change focused validation against the budget logic and observed 16 failures and 2 passes, establishing the baseline. - T-Rex ran a post-change focused validation (head run) and confirmed all 18 cases passed, including the mocked NIM retry paths. - T-Rex executed the full focused validation after the change and produced a verbose log showing 91/91 passing. <a href="https://app.greptile.com/trex/runs/14800553/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/core/reasoning.py | Adds the shared effort-to-token scale and preserves exact client budgets as the higher-priority value. | | src/free_claude_code/providers/nvidia_nim/request_options.py | Removes top-level and nested client budgets before inserting one policy-derived NIM budget. | | src/free_claude_code/providers/openai_chat/reasoning.py | Extends llama.cpp request encoding to send mapped named-effort budgets. | </details> <sub>Reviews (2): Last reviewed commit: ["fix: canonicalize NIM reasoning budgets"](https://github.com/alishahryar1/free-claude-code/commit/368f1a88d3f1404fcab2365703cca89f1077f1ea) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=44998377)</sub> <!-- /greptile_comment -->
639 行
22 KiB
Python
639 行
22 KiB
Python
"""Tests for NVIDIA NIM request policy helpers."""
|
|
|
|
from copy import deepcopy
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from free_claude_code.config.nim import NimSettings
|
|
from free_claude_code.core.anthropic import set_if_not_none
|
|
from free_claude_code.core.anthropic.models import MessagesRequest, Tool
|
|
from free_claude_code.core.reasoning import ReasoningEffort, ReasoningPolicy
|
|
from free_claude_code.providers.nvidia_nim.request_options import (
|
|
_set_extra,
|
|
)
|
|
from free_claude_code.providers.nvidia_nim.request_options import (
|
|
build_nim_request_body as build_request_body,
|
|
)
|
|
from free_claude_code.providers.nvidia_nim.retry import (
|
|
clone_body_without_chat_template,
|
|
clone_body_without_reasoning_content,
|
|
)
|
|
from free_claude_code.providers.nvidia_nim.tool_schema import (
|
|
NIM_TOOL_ARGUMENT_ALIASES_KEY,
|
|
body_without_nim_tool_argument_aliases,
|
|
nim_tool_argument_aliases_from_body,
|
|
)
|
|
from tests.providers.request_factory import make_messages_request
|
|
from tests.providers.support import REASONING_OFF, REASONING_ON
|
|
|
|
GREP_SCHEMA_FROM_SERVER_LOG: dict[str, Any] = {
|
|
"type": "object",
|
|
"properties": {
|
|
"pattern": {"type": "string", "description": "The regular expression"},
|
|
"path": {"type": "string", "description": "File or directory to search"},
|
|
"glob": {"type": "string", "description": "Glob to filter files"},
|
|
"output_mode": {
|
|
"type": "string",
|
|
"enum": ["content", "files_with_matches", "count"],
|
|
},
|
|
"-A": {"type": "number", "description": "Lines after match"},
|
|
"-B": {"type": "number", "description": "Lines before match"},
|
|
"-C": {"type": "number", "description": "Lines around match"},
|
|
"-i": {"type": "boolean", "description": "Case insensitive"},
|
|
"-n": {"type": "boolean", "description": "Show line numbers"},
|
|
"type": {"type": "string", "description": "File type to search"},
|
|
},
|
|
"additionalProperties": False,
|
|
"required": ["pattern"],
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def req() -> MessagesRequest:
|
|
return make_messages_request(
|
|
model="test",
|
|
messages=[{"role": "user", "content": "hi"}],
|
|
max_tokens=100,
|
|
system=None,
|
|
temperature=None,
|
|
top_p=None,
|
|
stop_sequences=None,
|
|
tools=None,
|
|
extra_body=None,
|
|
top_k=None,
|
|
thinking=None,
|
|
)
|
|
|
|
|
|
class TestSetIfNotNone:
|
|
def test_value_not_none_sets(self):
|
|
body = {}
|
|
set_if_not_none(body, "key", "value")
|
|
assert body["key"] == "value"
|
|
|
|
def test_value_none_skips(self):
|
|
body = {}
|
|
set_if_not_none(body, "key", None)
|
|
assert "key" not in body
|
|
|
|
|
|
class TestSetExtra:
|
|
def test_key_in_extra_body_skips(self):
|
|
extra = {"top_k": 42}
|
|
_set_extra(extra, "top_k", 10)
|
|
assert extra["top_k"] == 42
|
|
|
|
def test_value_none_skips(self):
|
|
extra = {}
|
|
_set_extra(extra, "top_k", None)
|
|
assert "top_k" not in extra
|
|
|
|
def test_value_equals_ignore_value_skips(self):
|
|
extra = {}
|
|
_set_extra(extra, "top_k", -1, ignore_value=-1)
|
|
assert "top_k" not in extra
|
|
|
|
def test_value_set_when_valid(self):
|
|
extra = {}
|
|
_set_extra(extra, "top_k", 10, ignore_value=-1)
|
|
assert extra["top_k"] == 10
|
|
|
|
|
|
class TestBuildRequestBody:
|
|
@pytest.mark.parametrize(
|
|
("effort", "expected_budget"),
|
|
(
|
|
(ReasoningEffort.MINIMAL, 512),
|
|
(ReasoningEffort.LOW, 512),
|
|
(ReasoningEffort.MEDIUM, 1_024),
|
|
(ReasoningEffort.HIGH, 2_048),
|
|
(ReasoningEffort.XHIGH, 4_096),
|
|
(ReasoningEffort.MAX, 8_192),
|
|
),
|
|
)
|
|
def test_named_effort_enables_thinking_with_numeric_budget(
|
|
self,
|
|
req,
|
|
effort: ReasoningEffort,
|
|
expected_budget: int,
|
|
):
|
|
policy = ReasoningPolicy(effort=effort)
|
|
|
|
body = build_request_body(req, NimSettings(), reasoning=policy)
|
|
|
|
assert body["extra_body"]["chat_template_kwargs"] == {
|
|
"thinking": True,
|
|
"enable_thinking": True,
|
|
"reasoning_budget": expected_budget,
|
|
}
|
|
|
|
def test_named_effort_replaces_client_reasoning_budgets(self):
|
|
req = make_messages_request(
|
|
model="test",
|
|
thinking=None,
|
|
extra_body={
|
|
"reasoning_budget": 99,
|
|
"chat_template_kwargs": {
|
|
"reasoning_budget": 100,
|
|
"custom": "value",
|
|
},
|
|
},
|
|
)
|
|
|
|
body = build_request_body(
|
|
req,
|
|
NimSettings(),
|
|
reasoning=ReasoningPolicy(effort=ReasoningEffort.HIGH),
|
|
)
|
|
|
|
extra_body = body["extra_body"]
|
|
assert "reasoning_budget" not in extra_body
|
|
assert extra_body["chat_template_kwargs"] == {
|
|
"custom": "value",
|
|
"thinking": True,
|
|
"enable_thinking": True,
|
|
"reasoning_budget": 2048,
|
|
}
|
|
|
|
def test_max_tokens_capped_by_nim(self, req):
|
|
req.max_tokens = 100000
|
|
nim = NimSettings(max_tokens=4096)
|
|
body = build_request_body(req, nim, reasoning=REASONING_ON)
|
|
assert body["max_tokens"] == 4096
|
|
|
|
def test_presence_penalty_included_when_nonzero(self, req):
|
|
nim = NimSettings(presence_penalty=0.5)
|
|
body = build_request_body(req, nim, reasoning=REASONING_ON)
|
|
assert body["presence_penalty"] == 0.5
|
|
|
|
def test_include_stop_str_in_output_not_sent(self, req):
|
|
body = build_request_body(req, NimSettings(), reasoning=REASONING_ON)
|
|
assert "include_stop_str_in_output" not in body.get("extra_body", {})
|
|
|
|
def test_parallel_tool_calls_included(self, req):
|
|
nim = NimSettings(parallel_tool_calls=False)
|
|
body = build_request_body(req, nim, reasoning=REASONING_ON)
|
|
assert body["parallel_tool_calls"] is False
|
|
|
|
def test_tool_schema_boolean_subschemas_are_removed_without_mutating_request(
|
|
self, req
|
|
):
|
|
tool_schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
"query": {"type": "string", "default": False},
|
|
"blocked": False,
|
|
"nested": {"type": "object", "additionalProperties": False},
|
|
"choice": {"anyOf": [False, {"type": "string"}]},
|
|
},
|
|
"additionalProperties": False,
|
|
"required": ["query"],
|
|
}
|
|
req.tools = [
|
|
Tool(
|
|
name="search",
|
|
description="search",
|
|
input_schema=tool_schema,
|
|
)
|
|
]
|
|
|
|
body = build_request_body(req, NimSettings(), reasoning=REASONING_OFF)
|
|
|
|
parameters = body["tools"][0]["function"]["parameters"]
|
|
properties = parameters["properties"]
|
|
assert "additionalProperties" not in parameters
|
|
assert "blocked" not in properties
|
|
assert "additionalProperties" not in properties["nested"]
|
|
assert properties["choice"]["anyOf"] == [{"type": "string"}]
|
|
assert properties["query"]["default"] is False
|
|
assert tool_schema["additionalProperties"] is False
|
|
assert tool_schema["properties"]["nested"]["additionalProperties"] is False
|
|
|
|
def test_grep_schema_type_parameter_is_aliased_without_mutating_request(self, req):
|
|
tool_schema = deepcopy(GREP_SCHEMA_FROM_SERVER_LOG)
|
|
tool_schema["properties"]["_fcc_arg_type"] = {
|
|
"type": "string",
|
|
"description": "Existing safe property that collides with the alias",
|
|
}
|
|
tool_schema["required"] = ["pattern", "-A", "_fcc_arg_type"]
|
|
original_schema = deepcopy(tool_schema)
|
|
req.tools = [
|
|
Tool(
|
|
name="Grep",
|
|
description="Search file contents",
|
|
input_schema=tool_schema,
|
|
)
|
|
]
|
|
|
|
body = build_request_body(req, NimSettings(), reasoning=REASONING_OFF)
|
|
|
|
parameters = body["tools"][0]["function"]["parameters"]
|
|
properties = parameters["properties"]
|
|
aliases = body[NIM_TOOL_ARGUMENT_ALIASES_KEY]["Grep"]
|
|
assert "additionalProperties" not in parameters
|
|
assert properties["-A"] == original_schema["properties"]["-A"]
|
|
assert properties["-B"] == original_schema["properties"]["-B"]
|
|
assert properties["-C"] == original_schema["properties"]["-C"]
|
|
assert properties["-i"] == original_schema["properties"]["-i"]
|
|
assert properties["-n"] == original_schema["properties"]["-n"]
|
|
assert "type" not in properties
|
|
assert properties["pattern"] == original_schema["properties"]["pattern"]
|
|
assert properties["output_mode"]["enum"] == [
|
|
"content",
|
|
"files_with_matches",
|
|
"count",
|
|
]
|
|
assert (
|
|
properties["_fcc_arg_type"]
|
|
== original_schema["properties"]["_fcc_arg_type"]
|
|
)
|
|
assert aliases == {"_fcc_arg_type_2": "type"}
|
|
assert properties["_fcc_arg_type_2"] == original_schema["properties"]["type"]
|
|
assert "-A" in parameters["required"]
|
|
assert "_fcc_arg_type" in parameters["required"]
|
|
assert tool_schema == original_schema
|
|
|
|
def test_safe_tool_schema_does_not_add_alias_metadata(self, req):
|
|
tool_schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
"pattern": {"type": "string"},
|
|
"path": {"type": "string"},
|
|
"output_mode": {"type": "string", "enum": ["content", "count"]},
|
|
},
|
|
"required": ["pattern"],
|
|
}
|
|
req.tools = [
|
|
Tool(
|
|
name="Glob",
|
|
description="Find files",
|
|
input_schema=tool_schema,
|
|
)
|
|
]
|
|
|
|
body = build_request_body(req, NimSettings(), reasoning=REASONING_OFF)
|
|
|
|
assert NIM_TOOL_ARGUMENT_ALIASES_KEY not in body
|
|
parameters = body["tools"][0]["function"]["parameters"]
|
|
assert parameters["properties"] == tool_schema["properties"]
|
|
assert parameters["required"] == ["pattern"]
|
|
|
|
def test_nested_schema_keyword_properties_are_aliased_without_mutating_request(
|
|
self, req
|
|
):
|
|
tool_schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
"parent": {
|
|
"type": "object",
|
|
"properties": {
|
|
"type": {"type": "string", "enum": ["page_id"]},
|
|
"id": {"type": "string"},
|
|
},
|
|
"required": ["type", "id"],
|
|
}
|
|
},
|
|
"required": ["parent"],
|
|
}
|
|
original_schema = deepcopy(tool_schema)
|
|
req.tools = [
|
|
Tool(
|
|
name="NotionLike",
|
|
description="Nested type schema",
|
|
input_schema=tool_schema,
|
|
)
|
|
]
|
|
|
|
body = build_request_body(req, NimSettings(), reasoning=REASONING_OFF)
|
|
|
|
aliases = body[NIM_TOOL_ARGUMENT_ALIASES_KEY]["NotionLike"]
|
|
parent = body["tools"][0]["function"]["parameters"]["properties"]["parent"]
|
|
parent_properties = parent["properties"]
|
|
assert "type" not in parent_properties
|
|
assert parent_properties["_fcc_arg_type"] == {
|
|
"type": "string",
|
|
"enum": ["page_id"],
|
|
}
|
|
assert parent["required"] == ["_fcc_arg_type", "id"]
|
|
assert aliases == {"_fcc_arg_type": "type"}
|
|
assert tool_schema == original_schema
|
|
|
|
def test_private_alias_metadata_is_stripped_without_mutating_body(self):
|
|
body = {
|
|
"model": "test",
|
|
NIM_TOOL_ARGUMENT_ALIASES_KEY: {"Grep": {"_fcc_arg_A": "-A"}},
|
|
}
|
|
|
|
upstream_body = body_without_nim_tool_argument_aliases(body)
|
|
|
|
assert NIM_TOOL_ARGUMENT_ALIASES_KEY not in upstream_body
|
|
assert body[NIM_TOOL_ARGUMENT_ALIASES_KEY] == {"Grep": {"_fcc_arg_A": "-A"}}
|
|
assert nim_tool_argument_aliases_from_body(body) == {
|
|
"Grep": {"_fcc_arg_A": "-A"}
|
|
}
|
|
|
|
def test_reasoning_params_in_extra_body(self):
|
|
req = make_messages_request(
|
|
model="test",
|
|
messages=[{"role": "user", "content": "hi"}],
|
|
max_tokens=100,
|
|
system=None,
|
|
temperature=None,
|
|
top_p=None,
|
|
stop_sequences=None,
|
|
tools=None,
|
|
tool_choice=None,
|
|
extra_body=None,
|
|
top_k=None,
|
|
thinking=None,
|
|
)
|
|
|
|
nim = NimSettings()
|
|
body = build_request_body(req, nim, reasoning=REASONING_ON)
|
|
extra = body["extra_body"]
|
|
assert extra["chat_template_kwargs"] == {
|
|
"thinking": True,
|
|
"enable_thinking": True,
|
|
}
|
|
assert "reasoning_budget" not in extra
|
|
|
|
def test_canonicalization_removes_empty_client_reasoning_envelope(self):
|
|
req = make_messages_request(
|
|
model="test",
|
|
extra_body={
|
|
"chat_template_kwargs": {
|
|
"thinking": True,
|
|
"enable_thinking": True,
|
|
"reasoning_budget": 100,
|
|
}
|
|
},
|
|
)
|
|
|
|
body = build_request_body(
|
|
req,
|
|
NimSettings(),
|
|
reasoning=ReasoningPolicy.provider_default(),
|
|
)
|
|
|
|
assert "chat_template_kwargs" not in body["extra_body"]
|
|
|
|
def test_clone_body_without_chat_template(self):
|
|
body = {
|
|
"model": "test",
|
|
"extra_body": {
|
|
"chat_template": "custom_template",
|
|
"chat_template_kwargs": {
|
|
"thinking": True,
|
|
"enable_thinking": True,
|
|
"reasoning_budget": 100,
|
|
},
|
|
"ignore_eos": False,
|
|
},
|
|
}
|
|
|
|
cloned = clone_body_without_chat_template(body)
|
|
|
|
assert cloned is not None
|
|
assert "chat_template" not in cloned["extra_body"]
|
|
assert "chat_template_kwargs" not in cloned["extra_body"]
|
|
assert cloned["extra_body"]["ignore_eos"] is False
|
|
assert body["extra_body"]["chat_template"] == "custom_template"
|
|
assert body["extra_body"]["chat_template_kwargs"] == {
|
|
"thinking": True,
|
|
"enable_thinking": True,
|
|
"reasoning_budget": 100,
|
|
}
|
|
|
|
def test_clone_body_without_chat_template_kwargs_only(self):
|
|
body = {
|
|
"model": "test",
|
|
"extra_body": {
|
|
"chat_template_kwargs": {
|
|
"thinking": True,
|
|
"enable_thinking": True,
|
|
"reasoning_budget": 100,
|
|
},
|
|
"ignore_eos": False,
|
|
},
|
|
}
|
|
|
|
cloned = clone_body_without_chat_template(body)
|
|
|
|
assert cloned is not None
|
|
assert "chat_template" not in cloned["extra_body"]
|
|
assert "chat_template_kwargs" not in cloned["extra_body"]
|
|
assert cloned["extra_body"]["ignore_eos"] is False
|
|
|
|
def test_clone_body_without_chat_template_returns_none_when_unchanged(self):
|
|
body = {"model": "test", "extra_body": {"ignore_eos": False}}
|
|
|
|
assert clone_body_without_chat_template(body) is None
|
|
|
|
def test_no_chat_template_kwargs_when_thinking_disabled(self):
|
|
req = make_messages_request(
|
|
model="test",
|
|
messages=[{"role": "user", "content": "hi"}],
|
|
max_tokens=100,
|
|
system=None,
|
|
temperature=None,
|
|
top_p=None,
|
|
stop_sequences=None,
|
|
tools=None,
|
|
tool_choice=None,
|
|
extra_body=None,
|
|
top_k=None,
|
|
thinking=None,
|
|
)
|
|
|
|
nim = NimSettings()
|
|
body = build_request_body(req, nim, reasoning=REASONING_OFF)
|
|
extra = body.get("extra_body", {})
|
|
assert extra["chat_template_kwargs"] == {
|
|
"thinking": False,
|
|
"enable_thinking": False,
|
|
}
|
|
assert "reasoning_budget" not in extra
|
|
|
|
def test_reasoning_budget_respects_existing_chat_template_kwargs(self):
|
|
req = make_messages_request(
|
|
model="test",
|
|
messages=[{"role": "user", "content": "hi"}],
|
|
max_tokens=100,
|
|
system=None,
|
|
temperature=None,
|
|
top_p=None,
|
|
stop_sequences=None,
|
|
tools=None,
|
|
tool_choice=None,
|
|
top_k=None,
|
|
extra_body={
|
|
"chat_template_kwargs": {
|
|
"enable_thinking": False,
|
|
"custom": "value",
|
|
}
|
|
},
|
|
thinking=None,
|
|
)
|
|
|
|
body = build_request_body(req, NimSettings(), reasoning=REASONING_ON)
|
|
assert body["extra_body"]["chat_template_kwargs"] == {
|
|
"enable_thinking": True,
|
|
"custom": "value",
|
|
"thinking": True,
|
|
}
|
|
|
|
def test_chat_template_fields_are_provider_wide(self):
|
|
req = make_messages_request(
|
|
model="mistralai/mixtral-8x7b-instruct-v0.1",
|
|
messages=[{"role": "user", "content": "hi"}],
|
|
max_tokens=100,
|
|
system=None,
|
|
temperature=None,
|
|
top_p=None,
|
|
stop_sequences=None,
|
|
tools=None,
|
|
tool_choice=None,
|
|
extra_body=None,
|
|
top_k=None,
|
|
thinking=None,
|
|
)
|
|
|
|
nim = NimSettings(chat_template="custom_template")
|
|
body = build_request_body(req, nim, reasoning=REASONING_ON)
|
|
extra = body.get("extra_body", {})
|
|
assert extra["chat_template_kwargs"] == {
|
|
"thinking": True,
|
|
"enable_thinking": True,
|
|
}
|
|
assert extra["chat_template"] == "custom_template"
|
|
|
|
def test_no_reasoning_params_in_extra_body(self):
|
|
req = make_messages_request(
|
|
model="test",
|
|
messages=[{"role": "user", "content": "hi"}],
|
|
max_tokens=100,
|
|
system=None,
|
|
temperature=None,
|
|
top_p=None,
|
|
stop_sequences=None,
|
|
tools=None,
|
|
tool_choice=None,
|
|
extra_body=None,
|
|
top_k=None,
|
|
thinking=None,
|
|
)
|
|
|
|
nim = NimSettings()
|
|
body = build_request_body(req, nim, reasoning=REASONING_OFF)
|
|
extra = body.get("extra_body", {})
|
|
for param in (
|
|
"thinking",
|
|
"reasoning_split",
|
|
"return_tokens_as_token_ids",
|
|
"include_reasoning",
|
|
"reasoning_effort",
|
|
):
|
|
assert param not in extra
|
|
assert extra["chat_template_kwargs"] == {
|
|
"thinking": False,
|
|
"enable_thinking": False,
|
|
}
|
|
|
|
def test_explicit_reasoning_budget_is_preserved_exactly(self):
|
|
req = make_messages_request(model="test", thinking=None)
|
|
|
|
body = build_request_body(
|
|
req,
|
|
NimSettings(),
|
|
reasoning=ReasoningPolicy.on(budget_tokens=321),
|
|
)
|
|
|
|
assert body["extra_body"]["chat_template_kwargs"] == {
|
|
"thinking": True,
|
|
"enable_thinking": True,
|
|
"reasoning_budget": 321,
|
|
}
|
|
|
|
def test_assistant_thinking_blocks_removed_when_disabled(self):
|
|
req = make_messages_request(
|
|
model="test",
|
|
messages=[
|
|
{
|
|
"role": "assistant",
|
|
"content": [
|
|
{"type": "thinking", "thinking": "secret"},
|
|
{"type": "text", "text": "answer"},
|
|
],
|
|
}
|
|
],
|
|
max_tokens=100,
|
|
system=None,
|
|
temperature=None,
|
|
top_p=None,
|
|
stop_sequences=None,
|
|
tools=None,
|
|
tool_choice=None,
|
|
extra_body=None,
|
|
top_k=None,
|
|
thinking=None,
|
|
)
|
|
|
|
body = build_request_body(req, NimSettings(), reasoning=REASONING_OFF)
|
|
assert "<think>" not in body["messages"][0]["content"]
|
|
assert "answer" in body["messages"][0]["content"]
|
|
|
|
def test_assistant_thinking_replayed_as_reasoning_content_when_enabled(self):
|
|
req = make_messages_request(
|
|
model="test",
|
|
messages=[
|
|
{
|
|
"role": "assistant",
|
|
"content": [
|
|
{"type": "thinking", "thinking": "secret"},
|
|
{"type": "text", "text": "answer"},
|
|
],
|
|
}
|
|
],
|
|
max_tokens=100,
|
|
system=None,
|
|
temperature=None,
|
|
top_p=None,
|
|
stop_sequences=None,
|
|
tools=None,
|
|
tool_choice=None,
|
|
extra_body=None,
|
|
top_k=None,
|
|
thinking=None,
|
|
)
|
|
|
|
body = build_request_body(req, NimSettings(), reasoning=REASONING_ON)
|
|
assistant = body["messages"][0]
|
|
assert assistant["reasoning_content"] == "secret"
|
|
assert assistant["content"] == "answer"
|
|
assert "<think>" not in assistant["content"]
|
|
|
|
def test_clone_body_without_reasoning_content(self):
|
|
body = {
|
|
"model": "test",
|
|
"messages": [
|
|
{"role": "user", "content": "hi"},
|
|
{
|
|
"role": "assistant",
|
|
"content": "answer",
|
|
"reasoning_content": "secret",
|
|
},
|
|
],
|
|
}
|
|
|
|
cloned = clone_body_without_reasoning_content(body)
|
|
|
|
assert cloned is not None
|
|
assert "reasoning_content" not in cloned["messages"][1]
|
|
assert body["messages"][1]["reasoning_content"] == "secret"
|
|
|
|
def test_clone_body_without_reasoning_content_returns_none_when_unchanged(self):
|
|
body = {"model": "test", "messages": [{"role": "user", "content": "hi"}]}
|
|
|
|
assert clone_body_without_reasoning_content(body) is None
|