Skip to content

[Bug] extract_edges hard-codes max_tokens=16384, overriding the model's real budget; truncated JSON then surfaces as a messageless Exception #1869

Description

@drmm

Summary

extract_edges hard-codes a 16,384 output-token cap and passes it as an explicit max_tokens=, which overrides the model's real output budget. On a model graphiti itself records as supporting 65,536 output tokens, edge extraction is therefore capped at a quarter of capacity. When a document produces more edge JSON than that, the reply is truncated mid-string, json.loads fails, and — on the Gemini path — the error surfaces as a bare Exception with no message.

Related but not the same: #811 asks for the constant to be hoisted. This issue is about the cap causing silent, hard-to-diagnose ingest failure, and about the messageless exception that hides it. #763 is the mirror-image problem (config max_tokens ignored) on the OpenAI clients.

Version: graphiti-core 0.30.2 (eaa4128), Python 3.12, Windows 10, Kuzu store, GeminiClient(model='gemini-2.5-flash', thinking_config=ThinkingConfig(thinking_budget=0)).

1. The cap overrides the model

graphiti_core/utils/maintenance/edge_operations.py:141

extract_edges_max_tokens = 16384

passed at edge_operations.py:205:

llm_response = await llm_client.generate_response(
    prompt_library.extract_edges.edge(context),
    response_model=ExtractedEdges,
    max_tokens=extract_edges_max_tokens,   # <-- explicit, so it wins
    ...
)

GeminiClient._resolve_max_tokens (graphiti_core/llm_client/gemini_client.py:175-201) documents its own precedence, and an explicit parameter is rule 1:

1. Explicit max_tokens parameter passed to generate_response()
2. Instance max_tokens set during client initialization
3. ...

So the per-model table three dozen lines above it is never consulted for this call — even though it says (gemini_client.py:51-57):

GEMINI_MODEL_MAX_TOKENS = {
    'gemini-2.5-pro': 65536,
    'gemini-2.5-flash': 65536,
    'gemini-2.5-flash-lite': 64000,
    ...

There is no configuration knob: setting LLMConfig(max_tokens=...) does not help, because the call site passes its own value.

2. What that looks like at runtime

Ingesting a corpus of 81 markdown documents (~1 MB total), a single larger document blows the cap:

ERROR 🦀 LLM generation failed parsing as JSON, will try to salvage.
ERROR Raw output (truncated): {"edges": [{"source_entity_name": "ORCHESTRATOR", "target_entity_name": ...
ERROR Error in generating LLM response: Failed to parse structured response:
      Unterminated string starting at: line 1 column 43665 (char 43664)
ERROR 🦀 LLM generation failed and retries are exhausted.
ERROR Max retries (2) exceeded. Last error:

43,665 characters of dense JSON is almost exactly 16,384 tokens — the reply is being cut at the cap, not by the model. salvage_json is attempted and returns None for these. Retrying twice cannot help: the same prompt regenerates the same over-long answer.

Note thinking_budget=0 is already set, so this is not #1868 (thinking tokens consuming the output budget) — this is the raw cap.

3. The error arrives with no message

graphiti_core/llm_client/gemini_client.py:361:

        except Exception as e:
            ...
            logger.error(f'Error in generating LLM response: {e}')
            raise Exception from e

raise Exception from e constructs an argument-less Exception. Anything that catches and formats it — a bulk runner, an MCP server, a user's except Exception as exc: log(exc) — gets an empty string:

[13-24/81] FAIL  209.9s  Exception:

The cause survives only because logger.error on the line above happens to print it. raise (bare, preserving the original) or raise Exception(str(e)) from e would cost nothing. The same pattern appears at :344 where the message is preserved (raise Exception(f'Failed to parse structured response: {e}') from e), so this looks like an oversight rather than intent.

4. Blast radius in add_episode_bulk

One oversized document fails the entire batch. In the run above, chunk [13-24/81] — twelve documents — was lost to a single file that overflowed, because add_episode_bulk (graphiti.py:1290) has no per-episode error isolation. With the recommended chunking from its own docstring (graphiti.py:1345-1348), larger chunks mean a bigger blast radius from one bad document.

Suggested fixes

  1. Make the edge-extraction cap respect the model. Either drop the explicit max_tokens= at edge_operations.py:205 and let _resolve_max_tokens consult GEMINI_MODEL_MAX_TOKENS, or make the constant a floor rather than a ceiling: max_tokens=max(extract_edges_max_tokens, model_max). Hoisting it (refactor: hoist EXTRACT_EDGES_MAX_TOKENS constant #811) would also make it overridable.
  2. gemini_client.py:361 — preserve the message: bare raise, or raise Exception(str(e)) from e.
  3. Optional, higher value: isolate per-episode failures in add_episode_bulk so one unparseable document does not discard its whole chunk.

Happy to open a PR for (1) and (2) if that's useful.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions