Disable DeepSeek thinking when a tool is forced.
Auto-resolve of passives was 400ing because thinking mode cannot be combined with named tool_choice. Also strip BOM descriptions after an em dash before DigiKey search. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -105,6 +105,10 @@ def mpn_query_variants(mpn: str) -> list[str]:
|
||||
add(raw)
|
||||
add(raw.replace("_", "/"))
|
||||
add(raw.replace("/", "_"))
|
||||
for sep in (" — ", " – ", " - "):
|
||||
if sep in raw:
|
||||
add(raw.split(sep, 1)[0])
|
||||
break
|
||||
if "," in raw:
|
||||
add(raw.split(",", 1)[0])
|
||||
if len(_alnum(raw)) > 8 and raw[-1] in "Rr" and raw[-2].isalnum():
|
||||
|
||||
@@ -113,11 +113,19 @@ def _find_product(mpn: str, products: list[dict]) -> dict | None:
|
||||
|
||||
Prefers punctuation-insensitive equality, then packing suffixes, then a
|
||||
longer orderable code that starts with the BOM MPN. Does not fall back
|
||||
to ``products[0]``.
|
||||
to ``products[0]``. Tries BOM spelling variants (underscore, reel, extra
|
||||
description after an em dash).
|
||||
"""
|
||||
if not products:
|
||||
return None
|
||||
for query in mpn_query_variants(mpn):
|
||||
hit = _find_product_one(query, products)
|
||||
if hit:
|
||||
return hit
|
||||
return None
|
||||
|
||||
|
||||
def _find_product_one(mpn: str, products: list[dict]) -> dict | None:
|
||||
exact = None
|
||||
loose = None
|
||||
family = None
|
||||
@@ -325,7 +333,16 @@ async def fetch_params(mpn: str) -> ParamsFetchResult:
|
||||
return ParamsFetchResult(mpn, error="DigiKey API not configured")
|
||||
|
||||
try:
|
||||
products = await _keyword_search(mpn)
|
||||
products: list[dict] = []
|
||||
tried: set[str] = set()
|
||||
for keyword in mpn_query_variants(mpn):
|
||||
key = keyword.upper()
|
||||
if key in tried:
|
||||
continue
|
||||
tried.add(key)
|
||||
products = await _keyword_search(keyword)
|
||||
if _find_product(mpn, products):
|
||||
break
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.warning("DigiKey search failed for %s: %s", mpn, e)
|
||||
return ParamsFetchResult(mpn, error=f"DigiKey search failed ({e.response.status_code})")
|
||||
|
||||
@@ -253,10 +253,16 @@ class DeepSeekSession(LLMSession):
|
||||
]
|
||||
oai_messages.extend(messages_to_openai(messages, vision=self._vision))
|
||||
|
||||
# DeepSeek rejects forced tool_choice while thinking is on
|
||||
# ("Thinking mode does not support this tool_choice"). Auto-resolve
|
||||
# and extraction always force a save_* tool, so drop thinking there.
|
||||
forced_tool = isinstance(tool_choice, dict) and "name" in tool_choice
|
||||
thinking = self._thinking and not forced_tool
|
||||
|
||||
extra_body: dict[str, Any] = {
|
||||
"thinking": {"type": "enabled" if self._thinking else "disabled"},
|
||||
"thinking": {"type": "enabled" if thinking else "disabled"},
|
||||
}
|
||||
if self._thinking:
|
||||
if thinking:
|
||||
extra_body["reasoning_effort"] = self._reasoning_effort
|
||||
kwargs: dict[str, Any] = {
|
||||
"model": self.model,
|
||||
|
||||
@@ -39,8 +39,8 @@ def test_mpn_catalog_match_orderable_suffix():
|
||||
def test_mpn_query_variants_underscore_and_reel():
|
||||
variants = mpn_query_variants("25AA1024-I_SM")
|
||||
assert "25AA1024-I/SM" in variants
|
||||
variants = mpn_query_variants("ADAU1467WBCPZ300R")
|
||||
assert "ADAU1467WBCPZ300" in variants
|
||||
variants = mpn_query_variants("RC0805FR-0733RL — 33 Ω — 1% — 0805")
|
||||
assert "RC0805FR-0733RL" in variants
|
||||
|
||||
|
||||
def test_pick_lcsc_family_orderable():
|
||||
|
||||
@@ -95,6 +95,49 @@ def test_messages_to_openai_tool_roundtrip():
|
||||
assert out[2]["role"] == "user"
|
||||
|
||||
|
||||
def test_forced_tool_choice_disables_thinking():
|
||||
captured: dict = {}
|
||||
|
||||
class FakeCompletions:
|
||||
async def create(self, **kwargs):
|
||||
captured.update(kwargs)
|
||||
fn = SimpleNamespace(name="save_resolved_specs", arguments="{}")
|
||||
tc = SimpleNamespace(id="c1", function=fn)
|
||||
msg = SimpleNamespace(content=None, reasoning_content=None, tool_calls=[tc])
|
||||
usage = SimpleNamespace(
|
||||
prompt_tokens=10, completion_tokens=5,
|
||||
prompt_cache_hit_tokens=0, prompt_tokens_details=None,
|
||||
)
|
||||
return SimpleNamespace(
|
||||
choices=[SimpleNamespace(message=msg, finish_reason="tool_calls")],
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
class FakeClient:
|
||||
def __init__(self):
|
||||
self.chat = SimpleNamespace(completions=FakeCompletions())
|
||||
|
||||
from backend.services.llm.deepseek_provider import DeepSeekSession
|
||||
session = DeepSeekSession(
|
||||
client=FakeClient(),
|
||||
model="deepseek-v4-flash",
|
||||
system="sys",
|
||||
max_tokens=256,
|
||||
thinking=True,
|
||||
reasoning_effort="medium",
|
||||
)
|
||||
import asyncio
|
||||
from backend.services.llm.types import Message, TextBlock, ToolSchema
|
||||
asyncio.run(session.complete(
|
||||
messages=[Message("user", [TextBlock("resolve")])],
|
||||
tools=[ToolSchema(name="save_resolved_specs", description="x", input_schema={"type": "object"})],
|
||||
tool_choice={"name": "save_resolved_specs"},
|
||||
))
|
||||
assert captured["extra_body"]["thinking"]["type"] == "disabled"
|
||||
assert "reasoning_effort" not in captured["extra_body"]
|
||||
assert captured["tool_choice"]["function"]["name"] == "save_resolved_specs"
|
||||
|
||||
|
||||
def test_tool_schema_and_choice():
|
||||
schema = ToolSchema(
|
||||
name="save_pintable",
|
||||
|
||||
Reference in New Issue
Block a user