From 27a3ce8bd0824e06489677b1d80df4ff8d3b7dcd Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Fri, 28 Aug 2026 01:58:22 +0200 Subject: [PATCH] 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 --- backend/services/datasheet_finder.py | 4 +++ backend/services/digikey.py | 21 +++++++++-- backend/services/llm/deepseek_provider.py | 10 ++++-- tests/test_datasheet_finder.py | 4 +-- tests/test_deepseek_provider.py | 43 +++++++++++++++++++++++ 5 files changed, 76 insertions(+), 6 deletions(-) diff --git a/backend/services/datasheet_finder.py b/backend/services/datasheet_finder.py index 3c353db..490c574 100644 --- a/backend/services/datasheet_finder.py +++ b/backend/services/datasheet_finder.py @@ -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(): diff --git a/backend/services/digikey.py b/backend/services/digikey.py index 3e67b7f..1fa8976 100644 --- a/backend/services/digikey.py +++ b/backend/services/digikey.py @@ -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})") diff --git a/backend/services/llm/deepseek_provider.py b/backend/services/llm/deepseek_provider.py index 3dd0fbc..aa3d32b 100644 --- a/backend/services/llm/deepseek_provider.py +++ b/backend/services/llm/deepseek_provider.py @@ -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, diff --git a/tests/test_datasheet_finder.py b/tests/test_datasheet_finder.py index 863a5f1..863f618 100644 --- a/tests/test_datasheet_finder.py +++ b/tests/test_datasheet_finder.py @@ -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(): diff --git a/tests/test_deepseek_provider.py b/tests/test_deepseek_provider.py index 4acab3d..026d8b7 100644 --- a/tests/test_deepseek_provider.py +++ b/tests/test_deepseek_provider.py @@ -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",