From 528a3d233746ec4a80dfe88795a61a5791d5cce9 Mon Sep 17 00:00:00 2001 From: Jason Matthew Date: Thu, 30 Apr 2026 20:54:09 +1000 Subject: [PATCH] fix: disable thinking on Moonshot calls so kimi-k2.6 populates content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kimi-k2.6 is a reasoning model that writes its chain-of-thought to `reasoning_content` and the answer to `content`. With graphify's default `max_completion_tokens=8192`, large chunks (the default 20-file batches frequently produce 50k+ token user messages) cause reasoning to consume the entire output budget — `content` comes back empty, graphify parses zero nodes, and the user pays for input + reasoning tokens without getting a graph. Reproduction on a 162-file repo (~125k words): - Before: 9 chunks × 8192 tokens = ~74k completion tokens → 2 nodes total - After: same chunks → 138 nodes (chunk 1 alone: 42 nodes / 75 edges) Moonshot's API exposes `extra_body={"thinking": {"type": "disabled"}}`. For structured graph extraction we don't need chain-of-thought, so disable it for Moonshot endpoints only — other OpenAI-compatible providers (real OpenAI, Together, Groq) would 400 on the flag. --- graphify/llm.py | 8 +++++++ tests/test_llm.py | 57 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 tests/test_llm.py diff --git a/graphify/llm.py b/graphify/llm.py index f07f8ec06..c21fb22a6 100644 --- a/graphify/llm.py +++ b/graphify/llm.py @@ -102,6 +102,14 @@ def _call_openai_compat( } if temperature is not None: kwargs["temperature"] = temperature + # kimi-k2.6 is a reasoning model: it writes its chain-of-thought to + # `reasoning_content` and the answer to `content`. When reasoning consumes + # the entire max_completion_tokens budget on large chunks, `content` stays + # empty and we parse zero nodes while still paying for the input + reasoning + # tokens. For structured extraction we don't need chain-of-thought, so + # disable thinking on Moonshot-hosted models. + if "moonshot" in base_url: + kwargs["extra_body"] = {"thinking": {"type": "disabled"}} resp = client.chat.completions.create(**kwargs) result = _parse_llm_json(resp.choices[0].message.content or "{}") result["input_tokens"] = resp.usage.prompt_tokens if resp.usage else 0 diff --git a/tests/test_llm.py b/tests/test_llm.py new file mode 100644 index 000000000..71c300637 --- /dev/null +++ b/tests/test_llm.py @@ -0,0 +1,57 @@ +"""Tests for the direct LLM backend (Claude / Kimi).""" +from unittest.mock import MagicMock, patch + + +def _mock_response(content: str = '{"nodes":[],"edges":[]}', completion_tokens: int = 10): + """Build a fake OpenAI ChatCompletion response.""" + resp = MagicMock() + resp.choices = [MagicMock(message=MagicMock(content=content))] + resp.usage = MagicMock(prompt_tokens=100, completion_tokens=completion_tokens) + return resp + + +def test_kimi_call_disables_thinking(): + """kimi-k2.6 is a reasoning model: when thinking is enabled and the chunk + is large, all of max_completion_tokens is consumed by reasoning_content, + leaving content empty and graphify parsing zero nodes. Disable thinking on + Moonshot endpoints so content is always populated for structured extraction. + """ + from graphify.llm import _call_openai_compat + + fake_client = MagicMock() + fake_client.chat.completions.create.return_value = _mock_response() + + with patch("openai.OpenAI", return_value=fake_client): + _call_openai_compat( + base_url="https://api.moonshot.ai/v1", + api_key="sk-test", + model="kimi-k2.6", + user_message="=== example.py ===\ndef hello(): pass\n", + temperature=None, + ) + + kwargs = fake_client.chat.completions.create.call_args.kwargs + assert "extra_body" in kwargs, "moonshot calls must pass extra_body to disable thinking" + assert kwargs["extra_body"] == {"thinking": {"type": "disabled"}} + + +def test_non_moonshot_call_does_not_disable_thinking(): + """The thinking-disabled flag is Moonshot-specific extra_body. Other + OpenAI-compatible providers (real OpenAI, Together, Groq, etc.) don't + accept it and would 400. Only set it for Moonshot URLs.""" + from graphify.llm import _call_openai_compat + + fake_client = MagicMock() + fake_client.chat.completions.create.return_value = _mock_response() + + with patch("openai.OpenAI", return_value=fake_client): + _call_openai_compat( + base_url="https://api.openai.com/v1", + api_key="sk-test", + model="gpt-4o", + user_message="hi", + temperature=0, + ) + + kwargs = fake_client.chat.completions.create.call_args.kwargs + assert "extra_body" not in kwargs, "non-Moonshot calls must not pass thinking flag"