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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ Create a `.env` file in the project root (or export these in your shell):
```bash
HF_TOKEN=<your-hugging-face-token> # HF Router inference + Hub actions
GITHUB_TOKEN=<github-personal-access-token>
HF_BILL_TO=<org-name> # optional: bill Inference Providers usage to an org
```

All API-based model calls go through Hugging Face [Inference Providers](https://huggingface.co/docs/inference-providers/en/index), so your `HF_TOKEN` must be allowed to make Inference Provider calls. If no `HF_TOKEN` is set, the CLI will prompt you to paste one on first launch unless you start on a local model. To get a `GITHUB_TOKEN` follow the tutorial [here](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token). See the [local models section below](#local-models) for instructions on using agents that run on your hardware.
All API-based model calls go through Hugging Face [Inference Providers](https://huggingface.co/docs/inference-providers/en/index), so your `HF_TOKEN` must be allowed to make Inference Provider calls. Set `HF_BILL_TO` to an org you belong to to charge inference to that org's credits instead of your personal monthly allowance — this sends an `X-HF-Bill-To` header on every router call (main agent, research sub-agent, and compaction alike). If no `HF_TOKEN` is set, the CLI will prompt you to paste one on first launch unless you start on a local model. To get a `GITHUB_TOKEN` follow the tutorial [here](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token). See the [local models section below](#local-models) for instructions on using agents that run on your hardware.

### Usage

Expand Down
10 changes: 6 additions & 4 deletions agent/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from pathlib import Path
from typing import Any, Literal, Union

from dotenv import load_dotenv
from dotenv import find_dotenv, load_dotenv
from fastmcp.mcp_config import (
RemoteMCPServer,
StdioMCPServer,
Expand Down Expand Up @@ -207,10 +207,12 @@ def load_config(
Use ${VAR_NAME} in your JSON for any secret.
Automatically loads from .env file.
"""
# Load .env from project root first (so it works from any directory),
# then CWD .env can override if present
# Repo .env first, then let the launch directory's .env fill in any gaps.
# find_dotenv needs usecwd=True to look at the launch CWD, not this file.
load_dotenv(_PROJECT_ROOT / ".env")
load_dotenv(override=False)
cwd_dotenv = find_dotenv(usecwd=True)
if cwd_dotenv:
load_dotenv(cwd_dotenv, override=False)

raw_config = _load_json_config(Path(config_path))
if include_user_defaults:
Expand Down
19 changes: 19 additions & 0 deletions agent/core/llm_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ def _resolve_hf_router_token(session_hf_token: str | None = None) -> str | None:
# an accepted-looking value, so this stays intentionally small and generic.
_HF_EFFORTS = {"low", "medium", "high"}

# When ``HF_BILL_TO`` names an org you belong to, the router charges that org's
# credits instead of your personal allowance — same header huggingface_hub's
# ``bill_to=`` sets, applied here because we go through LiteLLM, not InferenceClient.
HF_BILL_TO_ENV = "HF_BILL_TO"
HF_BILL_TO_HEADER = "X-HF-Bill-To"


def _resolve_hf_bill_to() -> str | None:
"""Org to bill HF Inference Providers usage to, or None if unset."""
value = os.environ.get(HF_BILL_TO_ENV)
return value.strip() or None if value else None


def _hf_router_effort_level(reasoning_effort: str) -> str:
level = "low" if reasoning_effort == "minimal" else reasoning_effort
Expand Down Expand Up @@ -120,6 +132,10 @@ def _resolve_llm_params(
1. session.hf_token — the user's own token (CLI / OAuth / cache file).
2. huggingface_hub cache — ``HF_TOKEN`` / ``HUGGING_FACE_HUB_TOKEN`` /
local ``hf auth login`` cache.

When ``HF_BILL_TO`` is set, an ``X-HF-Bill-To`` header is attached to
HF-router calls so Inference Providers usage is charged to that org's
credits instead of the token owner's personal monthly allowance.
"""
normalized_model = strip_huggingface_model_prefix(model_name) or model_name

Expand All @@ -136,6 +152,9 @@ def _resolve_llm_params(
"api_base": HF_ROUTER_BASE_URL,
"api_key": api_key,
}
bill_to = _resolve_hf_bill_to()
if bill_to:
params["extra_headers"] = {HF_BILL_TO_HEADER: bill_to}
if reasoning_effort:
hf_level = _hf_router_effort_level(reasoning_effort)
if hf_level not in _HF_EFFORTS:
Expand Down
36 changes: 35 additions & 1 deletion tests/unit/test_llm_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@

from agent.core.hf_tokens import resolve_hf_request_token
from agent.core.llm_params import (
HF_BILL_TO_ENV,
HF_BILL_TO_HEADER,
UnsupportedEffortError,
_resolve_hf_router_token,
_resolve_llm_params,
)
from agent.core.model_ids import HF_ROUTER_BASE_URL


@pytest.fixture(autouse=True)
def _clear_bill_to(monkeypatch):
"""Keep the developer's own HF_BILL_TO out of the default-behavior tests."""
monkeypatch.delenv(HF_BILL_TO_ENV, raising=False)


def test_hf_router_params_for_default_model_uses_session_token():
params = _resolve_llm_params(
"anthropic/claude-opus-4.8:fal-ai",
Expand Down Expand Up @@ -63,13 +71,39 @@ def test_router_params_fall_back_to_hf_cache_when_session_token_missing(monkeypa
assert "extra_headers" not in params


def test_router_params_never_set_bill_to_headers():
def test_router_params_omit_bill_to_header_when_env_unset():
params = _resolve_llm_params("moonshotai/Kimi-K2.7-Code", "session-token")

assert params["api_key"] == "session-token"
assert "extra_headers" not in params


def test_router_params_add_bill_to_header_when_env_set(monkeypatch):
monkeypatch.setenv(HF_BILL_TO_ENV, "my-org")

params = _resolve_llm_params("moonshotai/Kimi-K2.7-Code", "session-token")

assert params["extra_headers"] == {HF_BILL_TO_HEADER: "my-org"}


def test_router_params_strip_whitespace_and_ignore_blank_bill_to(monkeypatch):
monkeypatch.setenv(HF_BILL_TO_ENV, " my-org ")
assert _resolve_llm_params("moonshotai/Kimi-K2.7-Code")["extra_headers"] == {
HF_BILL_TO_HEADER: "my-org"
}

monkeypatch.setenv(HF_BILL_TO_ENV, " ")
assert "extra_headers" not in _resolve_llm_params("moonshotai/Kimi-K2.7-Code")


def test_local_model_params_never_get_bill_to_header(monkeypatch):
monkeypatch.setenv(HF_BILL_TO_ENV, "my-org")

params = _resolve_llm_params("ollama/llama3.1:8b")

assert "extra_headers" not in params


def test_huggingface_prefix_is_stripped_for_router_calls():
params = _resolve_llm_params("huggingface/openai/gpt-5.5:fal-ai")

Expand Down
Loading