Sign in Book a demo 134

The local proxy

An optional listener inside tj serve that records what a cost policy would do. It ships in suggest mode, forwards every request unmodified, and never evaluates a policy against traffic it resolves as subscription.

tj proxy is an optional listener that runs inside tj serve and sits between your agent and its provider. It speaks the Anthropic /v1/messages and OpenAI /v1/chat/completions shapes, forwards each request to the real provider, and streams the response back.

It ships in suggest mode only. It records what a policy would have done. It blocks nothing, rewrites nothing, and changes no request. Off by default; you turn it on.

tj proxy enable

What suggest mode means

Every request that reaches the listener is forwarded unmodified. For traffic that is eligible for a policy, the proxy also evaluates the policies you defined and records the outcome: would_block, a soft near_ceiling flag, or nothing. That record is what you read later. No request is delayed, altered or refused on the basis of it.

Enforcement is a separate piece and is not in the open tree. The evaluators never say “blocked”; they say “would block”. Policies defined in OSS run unvalidated, since there is no certification engine here, so a suggestion is never implied to have been checked as safe.

The pricing-mode gate

This is the first thing the proxy does with a request. It is an invariant, not a setting you can flip.

The proxy resolves the pricing mode for the targeted provider, reusing the same declared-plan logic the rest of tj uses. Then:

Pricing modePathWhat happens
api (usage-billed)policythe only traffic the policy engine ever evaluates
subscriptionobserve-onlyforwarded unmodified, recorded, never evaluated
localobserve-onlynot usage-billed, so there is no spend to evaluate
unknownobserve-onlythe deliberate fail-safe

Be precise about what “observe-only” means, because the wiring tj proxy enable writes is not plan-selective. It sets ANTHROPIC_BASE_URL and OPENAI_BASE_URL in ~/.claude/settings.json, which routes Claude Code’s traffic through the listener whatever plan you are on. Subscription traffic therefore does arrive at the listener. What the gate decides is what happens next, and for subscription traffic the answer is: relay it and write down that you saw it.

Concretely, a subscription request is forwarded to the real provider with its own headers, including whatever credential it carried, minus the hop-by-hop headers any relay has to drop. The proxy records an observation of the gate decision: the provider, the plan tier, the pricing mode, the path chosen and the reason. It never builds a policy envelope for that request, so no policy evaluates it, no decision is recorded against it, and nothing about it is changed or delayed. Intercepting subscription-plan traffic to act on it is outside provider terms, and the gate is what keeps the policy path away from it.

One thing to know about that guarantee: the gate resolves the pricing mode from the plan tier you declared under [budget.<provider>] plan, not from anything it reads off the request. tj init prompts for it once, and you can edit it by hand. Declare api for a provider you actually use on a subscription and the gate resolves api, which is the policy row. If you hold both an API key and a subscription with one provider, check what you declared before enabling the proxy, and use tj init --reconfigure to change it.

What the proxy does not do with a credential: capture it, store it, log it, substitute one of its own, or reuse it for anything of its own. It relays and forgets.

The gate decision is a plain value object and is unit-tested on its own.

Safety doctrine

Pass-through is sacred. An error anywhere in classification, policy evaluation or recording is logged and the request is forwarded anyway. Nothing the proxy does for its own purposes can break your traffic.

The proxy holds no keys. Your client’s credentials are relayed to the provider on the request that carried them. The proxy injects none of its own, stores none, and logs none.

disable only removes its own wiring. tj proxy enable sets ANTHROPIC_BASE_URL and OPENAI_BASE_URL in the env block of ~/.claude/settings.json, the same file tj init --claude-code already manages, and disable removes them. It only ever removes a key whose value points at the tj proxy, so a base URL you set yourself is left alone.

That is the only base-URL wiring written. Claude Code reads that env block; nothing else does. Any other client, a Python or TypeScript agent on the Anthropic or OpenAI SDK for instance, keeps talking to the provider directly until you point its own base URL at http://127.0.0.1:7392 yourself, through its client options or its own environment. tj proxy status prints the URL to use.

The listener cannot outlive the server. It starts and stops with tj serve’s own lifespan, in the same event loop. There is no separate daemon to orphan.

tj doctor flags the footgun. If the base-URL wiring still points at the proxy while the proxy is off, your traffic would hit a dead port. Doctor reports that orphaned wiring, and so does tj proxy status.

Commands

tj proxy enable       # turn it on and wire the provider base URLs at it
tj proxy status       # config, killswitch state, and detected wiring
tj proxy disable      # turn it off and remove the wiring
tj proxy killswitch   # forward everything, classify nothing
tj proxy killswitch --off

For the machine-readable form of status, the --json flag is on the top-level command:

tj --json proxy status

The killswitch flips the proxy to pass-through-everything while the listener stays alive. Every request is then forwarded with no classification and no policy evaluation. --off releases it and normal classification resumes.

Restart tj serve after enable, disable or killswitch for the running listener to pick the change up.

Turning it on during setup

tj init --enforce

Runs the same tj proxy enable wiring at the end of setup, then prints what it does and does not touch, including the pricing-mode gate. It composes with --hooks. If no tj config exists yet, it says so and asks you to run tj init first.

Policies

A policy is data rather than code. It binds a registered evaluator to an optional provider or agent target:

[[policies]]
name = "anthropic-monthly-ceiling"
kind = "budget_cap"
enabled = true              # default
mode = "suggest"            # suggest only; enforce is scaffolded and gated off
target_provider = "anthropic"   # omit for any provider
target_agent = "my-agent"       # omit for any agent
params = { warn_at = 0.8 }      # budget_cap: the soft near_ceiling fraction

budget_cap is the concrete one. It reads the [budget.<provider>] usd ceiling you already configured and compares it against that provider’s current-cycle spend. Over the ceiling it records would_block. At or above 80% of it, overridable with a warn_at param, it records a soft near_ceiling flag. Under, nothing. With no ceiling configured, or with current-cycle spend unavailable, it records that there was nothing to evaluate. It never guesses at a ceiling.

Decisions are persisted, and tj policy decisions shows recent ones. tj policy list shows the configuration.

The savings meter that rides alongside records estimated recoverable amounts: what a policy would have recovered had it been enforced. Nothing there is a realized saving, realized is always false, and dollar figures are api-only.

Configuration

[proxy]
enabled = false
host = "127.0.0.1"
port = 7392
mode = "suggest"
killswitch = false
anthropic_base_url = "https://api.anthropic.com"
openai_base_url = "https://api.openai.com"

mode accepts suggest. The enforce path is scaffolded and gated off.

A side benefit: stream usage

When a request asks to stream, the proxy arms a read-only tap on the response and records whether the trailing usage payload ever arrived. The bytes relayed to your client are unchanged. Those observations feed the stream-usage analyzer, which flags streamed calls whose token counts were never reported and are therefore missing from your spend total.

Whether you can read that finding depends on the window, not on the tap. tj proxy enable routes Claude Code, and stream-usage is one of the analyzers skipped for a window an interactive coding agent dominates, so the observations get recorded and the analyzer never runs. Wiring an SDK client in does not by itself move that: what counts is the share of the window’s traffic each side carries. The analyzer runs once the SDK side is the larger one, and it reports something only if a stream there actually closed without its usage payload.

See also

  • CLI reference — tj serve, tj init, tj doctor
  • Configuration — the rest of the TOML surface. [proxy] and [[policies]] are documented on this page, not there
  • Troubleshooting — when traffic stops flowing