Skip to content

fix(label): escalate the token budget before splitting on a blank completion (#3747) - #3755

Open
abhay-codes07 wants to merge 1 commit into
Graphify-Labs:v8from
abhay-codes07:fix/label-budget-escalation
Open

abhay-codes07 wants to merge 1 commit into
Graphify-Labs:v8from
abhay-codes07:fix/label-budget-escalation

Conversation

@abhay-codes07

@abhay-codes07 abhay-codes07 commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #3747.

What

Community labeling batches communities into one prompt with a completion budget of min(256 + 48*n, 8192). When a reply won't parse, _label_batch_with_retry splits the batch and retries each half — which, for a smaller batch, computes a smaller budget.

For a reasoning model that is a dead end. The model spends its whole completion allowance on the chain-of-thought (returned separately from content) and comes back with empty content and finish_reason=length. The empty string is unparseable, so the batch splits, the budget shrinks, and every retry returns empty again — the reporter traced 1216 → 736 → 496 → 352 tokens, all empty. Labeling degrades to Community N placeholders for the entire run, and the split-retry can never recover because it moves the budget in the wrong direction.

Fix

Escalate the budget before splitting. A blank completion with room left under the 8192 cap is the budget-exhaustion signature, so double the budget and retry the same whole batch; only split once a larger budget stops helping. A non-empty but malformed reply is a parse problem a bigger budget won't fix, so that still splits immediately — no wasted full-budget calls. The escalation is bounded (stops at 8192), so a persistently-blank batch still raises and the caller falls back to placeholders rather than looping.

Verification

Three tests, driven by a mock _call_llm (no real backend needed):

  • A reasoning model that returns empty until the budget doubles (352 → 704) now gets labeled with the batch kept whole — fails without the fix (clean v8 splits to 1+1, both blank, and drops the batch).
  • A non-empty malformed reply still splits and is not escalated (one call at the base budget).
  • A batch blank even at the 8192 cap raises rather than looping (guards loop termination).

The existing labeling / retry / backend suites stay green (154 tests).

Scope

This is the minimal, robust fix (budget escalation). The issue's other suggestions — disabling thinking via extra_body ({"thinking": {"type": "disabled"}} / reasoning_effort: "low") and a CLI/env budget override — are complementary and left out here.

…pletion (Graphify-Labs#3747)

Community labeling batches a set of communities into one prompt with a
completion budget of `min(256 + 48*n, 8192)`. When the reply won't parse,
`_label_batch_with_retry` splits the batch in half and retries — which for a
smaller batch computes a SMALLER budget.

For a reasoning model that is a dead end. Such a model spends its whole
completion allowance on the chain-of-thought (returned separately from
`content`) and comes back with empty content and finish_reason=length. The
empty string is unparseable, so the batch splits, the budget shrinks, and every
retry — 1216 → 736 → 496 → 352 tokens in the reporter's trace — returns empty
again. Labeling degrades to `Community N` placeholders for the whole run.

Escalate the budget before splitting: a blank completion with room left under
the 8192 cap is the budget-exhaustion signature, so double the budget and retry
the same (whole) batch, and only split once more room stops helping. A non-empty
but malformed reply is a parse problem a larger budget won't fix, so that still
splits immediately — no extra full-budget calls. The escalation is bounded (it
stops at 8192), so a persistently-blank batch still raises and the caller falls
back to placeholders rather than looping.

Tests: a reasoning model that returns empty until the budget doubles now gets
labeled with the batch kept whole (fails without the fix); a non-empty malformed
reply still splits without escalating; a batch blank even at 8192 raises rather
than looping. The existing labeling/retry/backend suites stay green (154 tests).

Disabling thinking via `extra_body` or a CLI budget override (also suggested on
the issue) are complementary and left out of this minimal fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
Copilot AI lite review requested due to automatic review settings September 22, 2026 17:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

Formal verification. No changes could be formally verified in this run.


Graphify review — findings

Escalates the community-labeling completion budget before splitting when a batch fails to parse: a blank completion with room left doubles max_tokens up to the 8192 cap and retries the whole batch, since that's the signature of a reasoning model burning its allowance on chain-of-thought (#3747). A non-empty but malformed reply still splits immediately (a bigger budget won't fix it), and a batch that stays blank at the cap raises so the caller falls back to placeholders rather than looping forever.

No blocking issues surfaced. 3 lower-confidence candidates did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 864 functions depend on the 191 functions this change touches.

Health — this change adds coupling hotspots:

  • new: deduplicate_entities() — 77 callers, 24 callees
  • new: build_merge() — 76 callers, 14 callees
  • new: extract_files_direct() — 17 callers, 20 callees
  • new: build() — 52 callers, 6 callees
  • new: _call_claude_cli() — 33 callers, 9 callees
  • new: extract_corpus_parallel() — 26 callers, 11 callees
  • new: dispatch_command() — 2 callers, 125 callees
  • new: _extract_with_adaptive_retry() — 22 callers, 10 callees
  • …and 17 more — each is listed as a finding

Verification — 864 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 529 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

41 of 290 test file(s) selected (14%) via static blast radius.

  • tests/test_backend_env_isolation.py — impact
  • tests/test_backend_extras.py — impact
  • tests/test_build.py — impact
  • tests/test_build_merge_dedup_scope.py — impact
  • tests/test_build_merge_hyperedges_and_prune.py — impact
  • tests/test_build_merge_shrink_guard.py — impact
  • tests/test_carried_hyperedge_remap.py — impact
  • tests/test_charmap_encoding.py — impact
  • tests/test_chunking.py — impact
  • tests/test_claude_cli_backend.py — impact
  • tests/test_corrupt_graph_json.py — impact
  • tests/test_cross_extension_reexport_self_cycle.py — impact
  • tests/test_dedup.py — impact
  • tests/test_dedup_remaps_hyperedges.py — impact
  • tests/test_dedup_survivor_richness.py — impact
  • tests/test_evidence_binding.py — impact
  • tests/test_file_slice.py — impact
  • tests/test_global_graph.py — impact
  • tests/test_go_qualified_resolution.py — impact
  • tests/test_hyperedge_member_shapes.py — impact
  • tests/test_image_vision.py — impact
  • tests/test_injection_sentinel_coverage.py — impact
  • tests/test_issue_3472_source_file_collision.py — impact
  • tests/test_label_retry.py — impact, changed-test
  • tests/test_labeling.py — impact
  • tests/test_llm_backends.py — impact
  • tests/test_llm_parser.py — impact
  • tests/test_llm_parser_reasoning.py — impact
  • tests/test_no_dedup_flag.py — impact
  • tests/test_non_string_node_ids.py — impact
  • tests/test_ollama.py — impact
  • tests/test_ollama_retry_cap.py — impact
  • tests/test_oversized_document_slicing.py — impact
  • tests/test_partial_cache.py — impact
  • tests/test_pdf_slicing.py — impact
  • tests/test_pdf_token_estimate.py — impact
  • tests/test_provider_registry.py — impact
  • tests/test_prs.py — impact
  • tests/test_prune_sweeps_orphans.py — impact
  • tests/test_semantic_fragment_sanitize.py — impact
  • tests/test_unverified_semantic_shrink.py — impact

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

Formal verification

Could not verify: Could not verify \_label\_batch\_with\_retry.

The verifier did not have enough to check \_label\_batch\_with\_retry, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: not verifiable: all 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)

· 1 grounded finding(s) anchored inline below; 24 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/llm.py
return left | right


def label_communities(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — label_communities()

20 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Community labeling returns empty content with reasoning models (reasoning consumes the completion budget; split-retry reduces the budget further)

2 participants