Skip to main content

Route LLM calls through one send_message() across OpenAI-compatible providers and Anthropic's native Messages API; keep a JSONL ledger of every request and response for offline cost reconciliation.

Project description

llm-router-ledger

Route any LLM call through one send_message() and keep a JSONL ledger of every request and response for offline cost reconciliation.

Provider support

Status Adapter Providers
Supported direct Anthropic
Supported OpenAI-compat Azure OpenAI, DeepSeek, Local Ollama, MiniMax, OpenAI, OpenRouter, Qwen, Zhipu / GLM
Supported via OpenRouter ByteDance Seed, Xiaomi MiMo
Planned direct Gemini
  • All "Supported" rows in 0.1.2 are live-smoke-verified end-to-end.
  • Anthropic requires the optional [anthropic] extra: uv pip install llm-router-ledger[anthropic].
  • For ByteDance Seed and Xiaomi MiMo, use provider: openrouter with the appropriate model id.

Install

uv pip install llm-router-ledger

Quickstart

Set OPENROUTER_API_KEY in .env and create llm_endpoints.yaml in the working directory. The fastest path is to copy examples/llm_endpoints.example.yaml to llm_endpoints.yaml in your working directory and edit it.

from llm_router_ledger import UsageTracker, send_message

tracker = UsageTracker(
    log_path="logs/usage.jsonl",
    project_id="my-blog",
)
text, usage, gen_id = send_message(
    endpoint_name="openrouter-mimo-v2.5",
    system="You are concise.",
    user="Explain prompt caching in two sentences.",
    tracker=tracker,
)

Or against a local Ollama server, with no API costs:

text, usage, gen_id = send_message(
    endpoint_name="local-llama",
    system="You are concise.",
    user="Explain prompt caching in two sentences.",
    tracker=tracker,
)
  • send_message() returns (response_text, usage_dict, generation_id).
  • UsageTracker appends paired llm_request / llm_response events to the JSONL log, stamped with project_id, run_tag, run_label, and purpose for later grouping.

Per-endpoint request params

Model-specific knobs belong in config, not in every caller. Give an endpoint an extra_body and it is sent on every call to that endpoint:

endpoints:
  openrouter-deepseek:
    provider: openrouter
    model: deepseek/deepseek-chat
    api_key_env: OPENROUTER_API_KEY
    base_url: https://openrouter.ai/api/v1
    extra_body:
      reasoning:
        enabled: false
  • An extra_body passed to send_message() replaces the endpoint's value outright. The two layers are not merged, so a caller that wants both must combine them itself. An opaque vendor passthrough carries no merge rules to memorise as a result.
  • Known limitation: provider: anthropic ignores extra_body, so the field has no effect there. provider: openrouter reaches Claude with extra_body intact.

Mirroring usage elsewhere

UsageTracker.subscribe() registers a callback that receives every ledger entry, so usage can be mirrored to another store without this library depending on it:

tracker.subscribe(lambda entry: my_container.upsert_item(entry))
  • Each entry is written to the JSONL ledger before any subscriber runs.
  • A callback that raises is logged and skipped. The entry is already in the ledger, the call that produced it is unaffected, and the remaining subscribers still run.
  • Each subscriber receives its own copy of the entry.
  • Callbacks are synchronous and run on the calling thread, so a slow one delays every call. Queue the work inside the callback if the destination is remote.

JSONL ledger schema

  • UsageTracker writes two events per send_message() call: an llm_request before the call, and an llm_response after.
  • Both share a request_id so they can be paired. Top-level fields on each event include project_id, provider, model, purpose, run_tag, run_label, and timestamp.
  • The llm_response event additionally carries usage (with prompt_tokens, completion_tokens, total_tokens) and a response preview.

Identifying a response for billing reconciliation: the response id is routed to one of two fields based on prefix:

  • generation_id: set when the id starts with "gen-" (OpenRouter convention). Use this when joining against OpenRouter's CSV export, which calls the column generation_id.
  • provider_response_id: set for everything else. OpenAI, Azure OpenAI, Ollama, and most direct-provider endpoints return ids like "chatcmpl-..." that land here. Use this when joining against OpenAI-family billing exports or any provider-native log that exposes a chat completion id.

Exactly one of the two fields is populated per llm_response event; queries that join the ledger to billing data should COALESCE over both or branch on provider.

CLI

llm-router-ledger list                          # show configured endpoints
llm-router-ledger validate llm_endpoints.yaml   # validate the YAML
llm-router-ledger stale --days 30               # endpoints with stale pricing
llm-router-ledger chat --endpoint openrouter-mimo-v2.5 --system "You are concise." --user "Hello." --log-path logs/usage.jsonl --project-id my-project

Env vars

Variable Purpose
LRL_RUN_TAG Stamped on every JSONL event.
LRL_RUN_LABEL Stamped on every JSONL event.
LRL_CONFIG_PATH Default YAML path when load_config() is called with no argument.

Development

git clone https://github.com/nirmalyaghosh/llm-router-ledger
cd llm-router-ledger
uv sync --extra dev
pytest tests/unit

Verify a local Ollama setup end-to-end with python examples/smoke_test_ollama.py (see prerequisites at the top of the script).

License

MIT. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

llm_router_ledger-0.1.3.tar.gz (34.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

llm_router_ledger-0.1.3-py3-none-any.whl (24.3 kB view details)

Uploaded Python 3

File details

Details for the file llm_router_ledger-0.1.3.tar.gz.

File metadata

  • Download URL: llm_router_ledger-0.1.3.tar.gz
  • Upload date:
  • Size: 34.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for llm_router_ledger-0.1.3.tar.gz
Algorithm Hash digest
SHA256 ffe4b65deb3ad4f3b7d331fbd024818a319c7a992e561eef4a2d620bd80101fe
MD5 3ec818cfa7f5ee22839e7800b5ab4806
BLAKE2b-256 005f6e80d8bbad79ee9f611e9fdfb73003ae5eb829c65872546c065a7bc1335b

See more details on using hashes here.

File details

Details for the file llm_router_ledger-0.1.3-py3-none-any.whl.

File metadata

File hashes

Hashes for llm_router_ledger-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 133ddad0fe31452b9a3c19cfd28ada58260730b116b1c026bf419c9474800bc9
MD5 85ba91b575c89b26f2018bdb77ab73e2
BLAKE2b-256 004803d0e196534b2f734f093727c0c516b782292a955be64da2b25bef5044fc

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page