Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ skills/
!tools/skillgen/fragments/core/**
docs/superpowers/
.vscode/
.idea/
.kilo
openspec/
# Local benchmark scripts — never commit
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ Full release notes with details on each version: [GitHub Releases](https://githu
- Fix: Python extraction no longer crashes resolving an over-deep relative import (`from ....x import y` above the package root) (#2605, thanks @SinghAman21).
- Fix: a Go qualified type (`pkg.Type`) resolves by import path instead of losing the qualifier and binding by bare name to an unrelated same-named symbol (#2608, thanks @gnukeno).
- Fix: `graph.html`'s document title no longer embeds the generator's absolute host path (#2598, thanks @michaelxer); it keeps the path from the output-dir marker onward.
- Feat: verbose LLM tracing for community labeling and the dedup tiebreaker. `graphify cluster-only . --verbose` (or `graphify label . --verbose`, or `GRAPHIFY_LLM_VERBOSE=1` on any command that calls the LLM) prints each call's prompt, thinking, response, and per-call token counts to stderr, followed by a run-level token total with a cost estimate. For the `claude` backend, verbose mode enables extended thinking (1024-token budget, billed as output tokens) so there is reasoning to show; for `claude-cli` it switches to `--output-format stream-json --verbose` and reads thinking blocks from the streamed assistant events; OpenAI-compatible and Bedrock backends surface `reasoning_content`/`reasoningContent` when the deployment returns it. Non-verbose runs are byte-for-byte unchanged in behavior and cost.
- Feat: token-economics-only mode. `graphify cluster-only . --tokens` (or `graphify label . --tokens`, or `GRAPHIFY_LLM_TOKENS=1` on any command) prints one line of per-call token counts plus the run total with a cost estimate — without the prompt/thinking/response dumps, and without verbose's call-shaping side effects (no extended thinking on `claude`, no stream-json on `claude-cli`), so the accounting is billing-neutral. `--verbose` implies it: the full exchange already ends with the token line.

## 0.9.39 (2026-08-10)

Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,8 @@ These are only needed for **headless / CI extraction** (`graphify extract`). Whe
| `GRAPHIFY_MAX_GRAPH_BYTES` | Override the 512 MiB graph.json size cap — e.g. `700MB`, `2GB`, or plain bytes | optional — useful for very large corpora |
| `GRAPHIFY_MAX_CONTEXTS` | Maximum number of non-default project graphs retained by one multi-project MCP server | optional — default: `8`; invalid values use `8`, and values below `1` use `1` |
| `GRAPHIFY_LLM_TEMPERATURE` | Override LLM temperature for semantic extraction — e.g. `0.7`, or `none` to omit | optional — auto-omitted for o1/o3/o4/gpt-5 reasoning models |
| `GRAPHIFY_LLM_VERBOSE` | Print every labeling/dedup LLM call's prompt, thinking, response, and per-call token counts to stderr | optional; also `--verbose` on `cluster-only`/`label` |
| `GRAPHIFY_LLM_TOKENS` | Print only the token economics: one line of per-call token counts plus a run total — no prompt/thinking/response dumps | optional; also `--tokens` on `cluster-only`/`label` |

---

Expand Down Expand Up @@ -784,12 +786,18 @@ graphify cluster-only ./my-project --exclude-hubs 99 # exclude p99 deg
graphify cluster-only ./my-project --no-label # keep "Community N" placeholders
graphify cluster-only ./my-project --backend=gemini # backend for community naming
graphify cluster-only ./my-project --backend=gemini --model gemini-2.5-pro # specific model
graphify cluster-only ./my-project --verbose # trace labeling LLM calls: prompt, thinking, response, tokens
graphify cluster-only ./my-project --tokens # token economics only: per-call counts and a run total
graphify label ./my-project # (re)name communities with the configured backend
graphify label ./my-project --backend=openai --model gpt-4o # force a specific backend and model
```

> **Community names:** inside an agent (Claude Code, Gemini CLI) the agent names communities itself. When you run the bare CLI, `cluster-only` auto-names them with the configured backend (built-in or custom OpenAI-compatible provider) — pass `--no-label` to keep `Community N`, or run `graphify label` to (re)generate names on demand.

> **Verbose labeling:** `--verbose` on `cluster-only`/`label` (or `GRAPHIFY_LLM_VERBOSE=1` on any command) prints each labeling call's prompt, thinking, response, and per-call token counts to stderr, then a run total with a cost estimate. On the `claude` backend verbose mode enables extended thinking so the reasoning is visible (billed as output tokens); on `claude-cli` it reads thinking from the CLI's streamed assistant events. Non-verbose runs are unchanged.

> **Token economics only:** `--tokens` (or `GRAPHIFY_LLM_TOKENS=1` on any command) prints just one line of per-call token counts plus the run total — no prompt/thinking/response dumps. Unlike `--verbose` it changes nothing about the calls themselves (no extended thinking, no stream-json), so the accounting is side-effect free.

---

## Learn more
Expand Down
4 changes: 4 additions & 0 deletions graphify/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,12 +546,16 @@ def _run_cli() -> None:
print(" --model=<name> model to use for community naming")
print(" --max-concurrency=N parallel community-labeling LLM calls (default 4; forced to 1 for ollama/claude-cli)")
print(" --batch-size=N communities per labeling LLM call (default 100)")
print(" --verbose trace labeling LLM calls: prompt, thinking, response, tokens")
print(" --tokens token economics only: per-call token counts and a run total")
print(" label <path> (re)name communities with the configured LLM backend, regenerate report")
print(" --missing-only keep existing labels and only name missing/placeholder communities")
print(" --backend=<name> backend to use (default: auto-detect from API keys)")
print(" --model=<name> model to use for community naming")
print(" --max-concurrency=N parallel labeling LLM calls (default 4; forced to 1 for ollama/claude-cli)")
print(" --batch-size=N communities per labeling LLM call (default 100)")
print(" --verbose trace labeling LLM calls: prompt, thinking, response, tokens")
print(" --tokens token economics only: per-call token counts and a run total")
print(" query \"<question>\" BFS traversal of graph.json for a question")
print(" --dfs use depth-first instead of breadth-first")
print(" --context C explicit edge-context filter (repeatable)")
Expand Down
46 changes: 45 additions & 1 deletion graphify/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1724,6 +1724,8 @@ def dispatch_command(cmd: str) -> None:
no_label = "--no-label" in sys.argv
missing_only = "--missing-only" in sys.argv
co_timing = "--timing" in sys.argv
co_verbose = "--verbose" in sys.argv
co_tokens = "--tokens" in sys.argv
_backend_arg = next((a for a in sys.argv if a.startswith("--backend=")), None)
label_backend = _backend_arg.split("=", 1)[1] if _backend_arg else None
_model_arg = next((a for a in sys.argv if a.startswith("--model=")), None)
Expand Down Expand Up @@ -1772,7 +1774,7 @@ def dispatch_command(cmd: str) -> None:
label_batch_size = int(args[i_arg + 1]); label_batch_size_explicit = True; i_arg += 2
elif a.startswith("--batch-size="):
label_batch_size = int(a.split("=", 1)[1]); label_batch_size_explicit = True; i_arg += 1
elif a in ("--no-viz", "--missing-only") or a.startswith("--min-community-size="):
elif a in ("--no-viz", "--missing-only", "--verbose", "--tokens") or a.startswith("--min-community-size="):
i_arg += 1
elif a.startswith("--"):
i_arg += 1
Expand All @@ -1782,6 +1784,20 @@ def dispatch_command(cmd: str) -> None:
i_arg += 1
if watch_path is None:
watch_path = Path(".")
if co_verbose:
# Trace every labeling LLM exchange (prompt, thinking, reply,
# per-call tokens) to stderr. Env-var equivalent:
# GRAPHIFY_LLM_VERBOSE=1, which also covers paths with no flag
# parsing (watch, update, dedup tiebreaker).
from graphify.llm import set_llm_verbose
set_llm_verbose(True)
if co_tokens:
# Token-economics-only: per-call token lines plus the run total,
# without prompt/thinking/response dumps and without verbose's
# call-shaping side effects (extended thinking, stream-json).
# Env-var equivalent: GRAPHIFY_LLM_TOKENS=1.
from graphify.llm import set_llm_tokens
set_llm_tokens(True)
graph_json = graph_override if graph_override is not None else watch_path / _GRAPHIFY_OUT / "graph.json"
if not graph_json.exists():
print(
Expand Down Expand Up @@ -1887,6 +1903,8 @@ def dispatch_command(cmd: str) -> None:
("--model", label_model is not None),
("--batch-size", label_batch_size_explicit),
("--max-concurrency", label_max_concurrency_explicit),
("--verbose", co_verbose),
("--tokens", co_tokens),
) if given]
if _ignored_label_flags:
print(
Expand Down Expand Up @@ -1996,6 +2014,32 @@ def dispatch_command(cmd: str) -> None:
if v and v != f"Community {cid}" and v != str(cid)
})
stages.mark("label")
# Gate on the EFFECTIVE tracing state, not just the CLI flags: with
# GRAPHIFY_LLM_VERBOSE/GRAPHIFY_LLM_TOKENS set in the env but no flag,
# every per-call line prints yet this run total used to be skipped.
# The flags imply the effective state via set_llm_verbose/set_llm_tokens.
from graphify.llm import _llm_tokens as _tracing_on_tokens, _llm_verbose as _tracing_on_verbose
if (_tracing_on_verbose() or _tracing_on_tokens()) and (label_token_usage["input"] or label_token_usage["output"]):
# Run-level token accountability: per-call numbers were already
# traced by llm.py's verbose/tokens hook; this is the spendable
# total, with a cost estimate when the backend's pricing is known.
from graphify.llm import detect_backend as _detect_backend, estimate_cost as _est_cost
_cost_backend = label_backend
if _cost_backend is None:
try:
_cost_backend = _detect_backend()
except Exception:
_cost_backend = None
_cost = (
_est_cost(_cost_backend, label_token_usage["input"], label_token_usage["output"])
if _cost_backend else 0.0
)
print(
f"[graphify llm] label run total: {label_token_usage['input']:,} in · "
f"{label_token_usage['output']:,} out "
f"(~${_cost:.4f} @ {_cost_backend or 'unknown backend'})",
file=sys.stderr,
)
questions = suggest_questions(G, communities, labels)
# cluster-only re-clusters an EXISTING graph: the code content is exactly
# what extract saw, so keep the extract-time commit stamp instead of
Expand Down
Loading