[Bug]: response_format_json_object=True is silently dropped, so JSON mode never reaches the model
Maintainers usually reply within 5 days
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 72/100
Research direction
Start in packages/graphrag-llm/graphrag_llm/completion/lite_llm_completion.py, where _base_completion and _base_completion_async check "response_format" not in new_args before enabling JSON mode; the key is always present with a None value, so the check should test for None instead. Then look at QueryState.add_all_follow_ups and find_incomplete_actions in graphrag/query/structured_search/drift_search/state.py, which should skip non-string follow-ups. Done means the reproduction script shows {'type': 'json_object'} passed to LiteLLM, reproduction 2 no longer raises AttributeError, and unit tests cover both.
Written by the indexing model from the issue text.
Description
Do you need to file an issue?
- I have searched the existing issues and this bug is not already filed.
- My model is hosted on OpenAI or Azure. If not, please look at the "model providers" issue and don't file a new one here.
- I believe this is a legitimate bug, not just a question. If this is a question, please use the Discussions area.
Describe the bug
Note on the provider checkbox: we run open-weight models via vLLM, but this bug does not depend on the provider. The reproduction below uses model_provider="openai" and never contacts a model.
Several query paths request JSON mode with response_format_json_object=True: the DRIFT local search actions (drift_search/search.py#L97), the global search map step (global_search/search.py#L97, #L240) and rate_relevancy (#L62). The flag never takes effect: the request sent to the model contains no response_format.
Root cause (graphrag_llm/completion/lite_llm_completion.py):
completionandcompletion_asyncpopresponse_format(defaultNone) and always pass it on explicitly (#L158, #L197)._base_completionand_base_completion_asynconly enable JSON mode if the key is absent (#L262, #L288):if json_object and "response_format" not in new_args:
Since the key is always present with the value None, this branch is never taken. LiteLLM then receives response_format=None and drops it.
Consequences in DRIFT (silent, only warnings are logged):
- Without JSON mode, models often answer an action in plain prose.
try_parse_json_objectthen returns{}, and the action yields no answer and no follow-ups ("No answer found for query"). - Models also return non-strict JSON with single quotes. The repair in
try_parse_json_objectsplits follow-up queries at escaped apostrophes and at the comma before a year, and the year becomes an int. Example:'...SBF\'s political dealings were mentioned in the October 7, 2023 TechCrunch digest?'becomes["What specific details about SBF ", "s political dealings were mentioned in the October 7", 2023, "TechCrunch digest?'"]. QueryState.add_all_follow_upslogs a warning for non-string items but still adds them to the graph (state.py#L45-L53).find_incomplete_actionsthen raisesAttributeError: 'int' object has no attribute 'is_complete'(#L57), and the whole DRIFT query fails.
Observed impact (vLLM 0.30.0, default DRIFT settings, MultiHop-RAG corpus): 5 to 25 % of DRIFT actions were answered in plain prose depending on the model (Qwen3.8-27B 5 to 14 %, Mistral-Small-3.2-24B 25 %). With Qwen3.8-27B, 3 to 5 % of DRIFT queries failed with the AttributeError even after two retries.
Steps to reproduce
- JSON mode is dropped (no network access, no model needed):
import asyncio
import logging
import graphrag_llm.completion.lite_llm_completion as llc
from graphrag_llm.completion import create_completion
from graphrag_llm.config import ModelConfig
logging.getLogger("graphrag_llm").setLevel(logging.CRITICAL) # hide the logged RuntimeError below
captured = {}
async def fake_acompletion(**kwargs):
captured.update(kwargs)
raise RuntimeError("stop before any network call")
llc.litellm.acompletion = fake_acompletion
model = create_completion(
ModelConfig(model_provider="openai", model="gpt-4o-mini", api_key="not-used")
)
async def main():
try:
await model.completion_async(
messages=[{"role": "user", "content": "Answer in JSON."}],
response_format_json_object=True,
)
except RuntimeError:
pass
asyncio.run(main())
print("response_format passed to litellm:", captured.get("response_format"))
# Actual: None
# Expected: {'type': 'json_object'}
- A non-string follow-up crashes DRIFT:
from graphrag.query.structured_search.drift_search.action import DriftAction
from graphrag.query.structured_search.drift_search.state import QueryState
state = QueryState()
parent = DriftAction(query="parent question")
state.add_action(parent)
state.add_all_follow_ups(parent, ["a valid follow-up question", 2023])
state.find_incomplete_actions()
# AttributeError: 'int' object has no attribute 'is_complete'
Expected Behavior
When response_format_json_object=True is passed and no explicit response_format is given, LiteLLM receives response_format={"type": "json_object"}. A follow-up query that is not a string should be skipped instead of crashing the DRIFT search.
GraphRAG Config Used
# Excerpt of settings.yaml. All other settings are GraphRAG 3.1.0 defaults.
# The JSON-mode bug does not depend on this config: reproduction 1 runs without any settings file.
completion_models:
default_completion_model:
model_provider: hosted_vllm # bug also reproduced with model_provider: openai (reproduction 1)
model: Qwen/Qwen3.8-27B # also observed with mistralai/Mistral-Small-3.2-24B-Instruct-2506
api_base: ${VLLM_API_BASE}
api_key: ${GRAPHRAG_API_KEY}
call_args: # query time
temperature: 0
top_p: 1
max_tokens: 4000
retry:
type: exponential_backoff
max_retries: 2
embedding_models:
default_embedding_model:
model_provider: hosted_vllm
model: BAAI/bge-m3
api_base: ${VLLM_EMBED_API_BASE}
api_key: ${GRAPHRAG_API_KEY}
drift_search: # GraphRAG 3.1.0 defaults
completion_model_id: default_completion_model
embedding_model_id: default_embedding_model
n_depth: 3
drift_k_followups: 20
primer_folds: 5
concurrency: 32
Logs and screenshots
Reproduction 1 prints: response_format passed to litellm: None
Reproduction 2 raises: AttributeError: 'int' object has no attribute 'is_complete' (state.py, line 57)
Additional Information
- Versions: graphrag 3.1.0, graphrag-llm 3.1.0, litellm 1.86.2, Python 3.13. The same code is in v3.1.2, v3.2.0, v3.3.0 and on main (34b2b67).
- Suggested fix 1, in both
_base_completionand_base_completion_async:
if json_object and new_args.get("response_format") is None:
With this change, reproduction 1 prints{'type': 'json_object'}. - Suggested fix 2 (defensive): in
QueryState.add_all_follow_ups,continueafter the warning for non-string items. With fix 1 the trigger mostly disappears, butjson_objectmode does not constrain types, so a model can still return non-string items. - I am happy to open a PR with both changes and unit tests.
- Dominant language
- Python
- Stars
- 36.6k
- Forks
- 3.9k
- Avg merge
- 4d 11h
- Merged PRs (30d)
- 1
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from microsoft/graphrag
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
microsoft/graphrag#2626 · 1 comment ·
Maintainers usually reply within 5 days
-
Difficulty 1/5 Under an hour Newbie friendliness 95/100
Maintainers usually reply within 5 days
-
[Bug]: JsonCache.get() crashes with unhandled AttributeError on non-dict or invalid envelope JSON payloadsMay be free again A pull request for this issue was closed without being merged. Open
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
microsoft/graphrag#2614 · 2 comments ·
Maintainers usually reply within 5 days
-
[Bug]: JSONLinesFileReader drops rows containing U+2028, U+2029 or U+0085 (uses str.splitlines)Open
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
microsoft/graphrag#2594 · 1 comment ·
Maintainers usually reply within 5 days
-
[Bug]: TokenChunker never terminates when chunking overlap >= size (no validation of size/overlap)Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
microsoft/graphrag#2593 · 2 comments ·
Maintainers usually reply within 5 days
All issues in microsoft/graphrag
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
FinanceFlash/unvibecode#819 ·
Maintainers usually reply within 1 day
-
documentation type/bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
add entry to blockOpen
Difficulty 1/5 Under an hour Newbie friendliness 65/100
StevenBlack/hosts#3299 ·
-
`garak --report` overwrites the input report when its name has no ".report"Possibly taken @0z5a claimed this today. Openneeds-triage
Difficulty 2/5 Half a day Newbie friendliness 72/100
Maintainers usually reply within 2 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Maintainers usually reply within 2 days