Skip to content

Latest commit

 

History

73 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

prism

Route local AI tools (Claude Code, Codex, OpenCode, anything that talks to the Anthropic or OpenAI API) through your Teleport cluster's LLM gateways, powered by Teleport Beams.

(The name? A prism redirects beams.)

prism claude        # Claude Code, routed via Teleport
prism codex         # Codex CLI, routed via Teleport
prism opencode      # OpenCode, routed via Teleport
prism pi            # Pi, routed via Teleport
prism exec <cmd>    # any other tool, with prism env vars set

No API keys to manage, no shared secrets, no .env files. Auth is your Teleport identity.


Prerequisites

  • A Teleport cluster with Beams enabled. Beams provisions cluster-wide apps (anthropic and openai) that proxy traffic to the upstream LLM APIs. Prism tunnels to these apps — it doesn't talk to Anthropic or OpenAI directly.
  • tsh on PATH and a valid tsh login to the Beams cluster. Prism uses tsh proxy app (or tbot for unattended use) to establish authenticated tunnels to the Beams apps.
  • Go ≥ 1.26 (only if building from source).

Install

Homebrew (macOS / Linux)

brew install webvictim/tap/prism

From source

git clone https://github.com/webvictim/prism.git
cd prism
make
sudo make install              # or: make install PREFIX=$HOME/.local

Release notes live in CHANGELOG.md and on the releases page. Upgrading is usually just prism down && prism up afterwards.


Quick start (tsh, the "just try it" path)

If you already have an interactive tsh login to a Beams-enabled cluster, you can be running in 30 seconds:

tsh login --proxy=<your-cluster>.beams.run:443
prism config set proxy <your-cluster>.beams.run:443
prism claude

Prism logs into both cluster apps, starts a local daemon, and execs claude with the right env vars. Your tsh session expires after 12-24h; when it does, run tsh login again and prism's daemon auto-recovers.

For unattended use (overnight agents, long jobs, CI), use tbot instead — it never expires. See the next section.


Recommended setup: tbot (no token expiry, no re-login)

tbot is Teleport's Machine ID daemon. Prism runs it as a sidecar so your AI tools have a self-refreshing identity that doesn't expire. This is the right setup if you ever:

  • leave Claude Code or Codex running overnight,
  • use prism from CI / scripts / cron,
  • don't want to think about tsh login ever again.

One-time setup (assumes you have tctl admin perms on the cluster):

# 1. Generate Machine ID resource templates and a join token.
prism tbot bootstrap

# 2. Log in with admin perms so tctl can apply them.
tsh login --proxy=<your-cluster>.beams.run:443

# 3. Apply the three generated YAMLs (paths printed by bootstrap).
tctl create -f ~/.config/prism/tbot/role.yaml
tctl create -f ~/.config/prism/tbot/bot.yaml
tctl create -f ~/.config/prism/tbot/token.yaml

# 4. Fetch the registration secret and persist tbot config.
prism tbot configure

# 5. Switch prism over to the tbot backend.
prism config set identity tbot
prism config set tbot.dir ~/.config/prism/tbot
prism up

# 6. (Linux) Optionally install as a systemd user service for persistence.
prism install

prism tbot bootstrap prints this exact sequence with your hostname filled in (resources are named prism-bot-<hostname> so multiple machines can share a cluster).

On Linux, prism install sets up a systemd user service so the daemon starts on boot and survives logout — ideal for unattended use. Requires lingering enabled for your user (sudo loginctl enable-linger $USER).

Verify it's working:

prism tbot status      # validates tbot dir, prints proxy/role/bot/secret state
prism status           # shows daemon liveness + listener ports
prism test             # smoke-tests all three wire formats

After that, every invocation of prism claude / prism codex / prism opencode / prism pi / prism exec uses the tbot identity — no re-login, ever.

Note: Teleport caps bot certificates at 12 hours (DefaultBotMaxSessionTTL). Prism configures tbot to renew every 8 hours, well within this limit. If you see warnings about expired bot identities, run prism down && prism up to regenerate tbot.yaml with the correct renewal interval.


Daily use

prism claude [args...]        # run Claude Code through prism
prism codex [args...]         # run Codex through prism
prism opencode [args...]      # run OpenCode through prism
prism pi [args...]            # run Pi through prism
prism exec <cmd> [args...]    # run any command with prism env vars set

All of them auto-start the daemon if it isn't already running. Claude Code, Codex, OpenCode and prism exec get endpoint environment variables pointing at the local router. Pi ignores those variables, so prism pi also updates Pi's model configuration before it starts. Any flags pass through:

prism claude --print "what's 2+2?"
prism codex --model openai.gpt-5.6-sol
prism opencode run "explain this repo"
prism pi -c
prism exec python my_script.py

For tools you launch outside prism (an IDE plugin, a shell script), export the env vars yourself:

prism up
eval "$(prism env)"

Two caveats specific to OpenCode, which is why prism opencode exists rather than just prism exec opencode:

  • It only offers a provider's models when one of the environment variables named in its catalog is set — ANTHROPIC_API_KEY for Anthropic — so prism opencode sets it to a dummy value that the router strips before forwarding. eval "$(prism env)" does not set it, so OpenCode launched that way shows no Anthropic models unless you have already run /connect.
  • A provider.anthropic.options.baseURL in your own OpenCode config takes precedence over the environment, and would silently send traffic straight to the vendor instead of through prism. If a session doesn't show up in prism logs, check for that first.

Claude Code Remote Control

Claude Code disables Remote Control when ANTHROPIC_BASE_URL is set (which prism does by default). To keep Remote Control while still routing inference through Teleport, enable forward-proxy mode:

prism config set claude_forward_proxy_mode true
prism down && prism up

In this mode, prism claude sets HTTPS_PROXY and NODE_EXTRA_CA_CERTS instead of ANTHROPIC_BASE_URL. The daemon acts as an HTTPS forward proxy: model API calls (/v1/messages) are intercepted and routed through the Teleport tunnel, while everything else (Remote Control, feature flags, telemetry, MCP) passes through to the real api.anthropic.com with your credentials intact. A locally-generated CA certificate (~/.config/prism/ca.pem) makes the interception transparent to Claude Code.

Both interactive sessions and headless Remote Control work:

prism claude       # interactive session, controllable from claude.ai/code
prism claude rc    # headless Remote Control worker

To go back to plain base-URL mode: prism config set claude_forward_proxy_mode false, then prism down && prism up.


Token usage tracking

Prism tracks token consumption from every API request that flows through the router. Usage is recorded per-request in monthly JSONL files under ~/.config/prism/usage/ and can be queried with prism usage:

prism usage              # today's totals by model and proxy
prism usage --week       # last 7 days
prism usage --all        # all time
prism usage --json       # raw JSONL records (pipe to jq)

Example output:

prism usage (today, 42 requests)
────────────────────────────────────────────────────────────

Model                                       Input   Output
────────────────────────────────────────────────────────────
claude-opus-5                              150.2k    28.4k  cache: r=120.0k w=15.0k
claude-sonnet-5                             45.0k    12.3k  cache: r=30.0k w=5.0k
openai.gpt-5.6-sol                           8.5k     3.2k

Proxy                                       Input   Output
────────────────────────────────────────────────────────────
prod.teleport.example.com:443              180.2k    35.6k
staging.teleport.example.com:443            23.5k     8.3k
────────────────────────────────────────────────────────────
TOTAL                                      203.7k    43.9k

The per-proxy breakdown is useful for tracking consumption against cluster-level token caps.


Pi integration

Pi ignores ANTHROPIC_BASE_URL and OPENAI_BASE_URL; it reads base URLs from its model registry instead. Use the first-class launcher and prism handles both pieces:

prism pi [args...]

It starts the daemon, gives Pi dummy Anthropic and OpenAI keys, mirrors Pi's catalog into ~/.pi/agent/models.json with each baseUrl pointed at the local router, then starts Pi. On a fresh Pi install it first runs pi update --models to populate the catalog. If that refresh cannot reach Pi's catalog service, the launcher stops rather than starting Pi with direct vendor routes. PI_CODING_AGENT_DIR is respected when Pi's configuration lives somewhere else.

Pi's overrides match by model id, so prism copies the ids and each model's cost, context and compatibility metadata from models-store.json; no model name is compiled into prism. Existing custom providers in models.json, such as llama-swap, are left alone. Each prism pi launch refreshes the Anthropic and OpenAI entries from the current catalog and router port.

The older setup-only command remains available when you want to route only one model from either provider:

prism pi config --openai-model gpt-5.6-sol
prism exec pi

Use prism exec pi after a narrowed setup. A normal prism pi restores every catalog model before launching. Pi also has its own unrelated config command; run that as prism exec pi config because prism pi config belongs to prism.


chat/completions shim

Newer gateways serve OpenAI models only on the Responses API. Ask for any model on /v1/chat/completions and you get:

model `openai.gpt-5.6-sol` isn't supported on this route

Clients that only speak chat/completions — MacWhisper, Teleport session summaries, most "OpenAI-compatible endpoint" boxes — would be dead in the water. So prism translates: /v1/chat/completions in, /v1/responses upstream, chat-shaped reply back out, streaming included. Requests to /v1/responses are untouched, so Codex and anything else already speaking Responses is unaffected.

It's on by default. Turn it off to talk to an older gateway that still serves chat/completions natively:

prism config set openai_chat_completions_shim false
prism down && prism up

With it off, /v1/chat/completions is relayed byte-for-byte, which is what makes a current prism usable against a legacy Beam.

Translated requests are visible in the log:

POST /v1/chat/completions [-> /v1/responses] 200 req=141B resp=373B model=openai.gpt-5.6-luna in=11 out=9 cache_read=0 cache_write=0 2.95s

No model names are compiled into prism. Reasoning models reject parameters like temperature and top_p, but which ones depends on the model, so prism doesn't guess: it forwards what the client sent, reads the parameter name out of the gateway's rejection, drops it, and retries — remembering the rejection per model for the life of the daemon.

chatcompat: openai.gpt-5.6-sol rejected parameter "temperature" — dropped it,
retrying, and remembering for the rest of this daemon's life

The memory is deliberately in-process, not on disk: a persisted cache would keep stripping a parameter forever after the gateway started accepting it. Relearning costs one extra round trip per model per restart.

Two limitations worth knowing:

  • Tool calling isn't translated. A request carrying tools gets a 400 pointing at /v1/responses.
  • Reasoning tokens count against max_tokens. A client that asks for max_tokens: 40 may spend all of it on reasoning and get finish_reason: "length" with little or no text. That's the gateway's accounting, not prism's — raise the client's limit if replies come back truncated.

Commands

Command What it does
prism claude [args...] Ensures the daemon is up; execs claude with prism env.
prism codex [args...] Same, for codex.
prism opencode [args...] Same, for opencode, plus a dummy ANTHROPIC_API_KEY so it surfaces the Anthropic models.
prism pi [args...] Same, for Pi, plus automatic model-catalog configuration.
prism exec <cmd> [args...] Same, for any command.
prism up Starts the local daemon (tunnels + router). Idempotent.
prism down Stops the daemon and logs out of the apps.
prism status Port assignments, identity state, daemon liveness.
prism env Prints export statements for your shell to eval.
prism logs Tails the local daemon log (request-level logging).
prism test [anthropic|openai|all] Smoke test. --format anthropic|openai-responses|openai-completions picks a wire format, --model a model (default: let the gateway choose), --stream exercises SSE.
prism usage [--week|--all|--json] Show token usage by model and proxy.
prism pi config Configure Pi without launching it. --anthropic-model / --openai-model narrow either provider to one id.
prism config [show|set|unset|clear] View/edit persistent config (proxy, identity, tbot.dir, claude_forward_proxy_mode, openai_chat_completions_shim).
prism tbot bootstrap Generate Machine ID resources for tbot identity.
prism tbot configure Persist the bound-keypair registration secret.
prism tbot status Validate the tbot working directory.
prism install Install as a system service (Linux systemd / macOS LaunchAgent).
prism uninstall Remove the system service.
prism version Print build version.

Troubleshooting

prism status shows [DEAD]

The daemon was killed. prism down && prism up.

A tunnel port is busy after prism down

If the daemon was SIGKILL'd on macOS/Linux, the tbot subprocess gets reparented to PID 1 and keeps holding its port. pkill -x tbot and retry. (Windows uses Job Objects, so this can't happen there.)

tbot: app "anthropic"/"openai" not found

The bot's role doesn't grant access to the cluster's LLM apps. Re-run prism tbot bootstrap and re-apply the role:

tctl create -f ~/.config/prism/tbot/role.yaml

tbot: too many levels of symbolic links

tbot refuses to write to its storage directory if the path contains symlinks (common in Lima VMs where ~/.config is symlinked). Move the tbot directory to a real path:

prism tbot bootstrap --dir ~/prism-tbot
prism config set tbot.dir ~/prism-tbot
# then re-run tctl create and prism tbot configure as normal

API Error: 400 The inference provider rejected the request…

The cluster's Anthropic gateway is Bedrock-backed and rejects some request fields the first-party API accepts (top-level fields such as thinking and Pi's fallbacks, plus unknown keys inside cache_control). Prism strips the known ones; if a new one shows up, turn on debug logging and check the daemon log:

prism down
PRISM_DEBUG=1 prism up
prism logs    # in another terminal, reproduce the error

Then add the offending field to anthropicStripFields (or cacheControlAllowedKeys) in internal/scrub/anthropic.go. To find the culprit, replay the failing request body against http://127.0.0.1:7331/v1/messages with fields removed until it succeeds.

tsh session expired

Run tsh login — the daemon detects the refreshed identity and restarts its subprocesses automatically. If you're tired of this, switch to tbot (see above).

Remote Control disabled / Remote Control environments are not available

Enable forward-proxy mode — see Claude Code Remote Control above. Both interactive sessions and headless prism claude rc work in that mode.

Debugging the system service (prism install)

If prism is installed as a service and misbehaving, these commands help:

macOS (LaunchAgent):

# Check if the job is loaded (modern launchctl)
launchctl print gui/$(id -u)/com.prism.daemon

# Stop and restart
launchctl bootout gui/$(id -u)/com.prism.daemon
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.prism.daemon.plist

# View the plist
cat ~/Library/LaunchAgents/com.prism.daemon.plist

# Tail daemon logs
prism logs

Linux (systemd):

# Service status + recent logs
systemctl --user status prism

# Full logs (follow mode)
journalctl --user -u prism -f

# Restart
systemctl --user restart prism

# Check if lingering is enabled (survives logout)
ls /var/lib/systemd/linger/$USER

How it works

┌────────────────────────┐
│ Claude Code / Codex /  │  ANTHROPIC_BASE_URL=http://127.0.0.1:7331
│ any AI tool            │  OPENAI_BASE_URL=http://127.0.0.1:7331/v1
└──────────┬─────────────┘
           │ plain HTTP
           ▼
┌────────────────────────┐
│ prism router (:7331)   │  path-based dispatch + Bedrock scrubbing
│  /v1/messages → :7333  │
│  /v1/chat/*   → :7334  │
└────┬──────────────┬────┘
     │              │
     ▼              ▼
┌─────────┐   ┌─────────┐
│ tsh/tbot│   │ tsh/tbot│  tunnel subprocesses
│ proxy   │   │ proxy   │
│anthropic│   │ openai  │
└────┬────┘   └────┬────┘
     │              │
     ▼              ▼
  Teleport      Teleport     Beams-managed LLM gateway apps
  anthropic     openai
  app           app
     │              │
     ▼              ▼
  Anthropic     OpenAI
  (Bedrock)     API

The router (internal/router/) dispatches by path; the shared scrub package (internal/scrub/) makes requests Bedrock-compatible (strips fields like thinking and metadata, sanitizes cache_control, caps non-streaming max_tokens at 8192 — Bedrock requires streaming above that). For OpenAI requests, it renames max_tokens to max_completion_tokens, which newer models require. /v1/chat/completions is handled by internal/chatcompat/ (see chat/completions shim). The router also captures token usage from responses into ~/.config/prism/usage/. The tunnels are tsh proxy app subprocesses, or — in tbot mode — a single tbot start with two application-tunnel services.

In forward-proxy mode (see Claude Code Remote Control), the same router also accepts HTTPS CONNECT and plain forward-proxy requests: api.anthropic.com model calls are intercepted and sent through the anthropic tunnel with the same scrubbing, and everything else passes through untouched.


Repo layout

cmd/prism/             local CLI + daemon
internal/router/       HTTP router with path dispatch + usage capture
internal/scrub/        Bedrock/OpenAI request scrubbing (shared)
internal/mitm/         forward proxy for Remote Control compatibility
internal/tunnel/       subprocess supervisor
internal/tbot/         tbot config rendering and bootstrap
internal/identity/     tsh session expiry watcher
internal/state/        runtime state persistence
internal/config/       persistent machine config
internal/usage/        token usage tracking (JSONL writer + reader)
internal/logfile/      rotating daemon log writer
internal/tshwrap/      tsh CLI wrappers
Makefile               build targets for all platforms

License

Apache 2.0 — see LICENSE.

About

A prism redirects beams.

Resources

Stars

9 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages