# proxy The `proxy` extension serves an OpenAI-compatible Chat Completions endpoint for every saved provider profile, so other tools can use albedo's providers (Codex, Antigravity, OpenAI-compatible endpoints) through one base url. It is installed and disabled; enable it globally in `/extensions` or with `{"enabled": {"proxy": true}}` in `$ALBEDO_HOME/extensions.json`. The change applies to the next request. ## albedo proxy `albedo proxy` finds or starts the daemon, then starts a detached forwarder on `http://127.0.0.1:4318/v1` (`--port`, 0 for a free port). It prints the base URLs, its API key, and the providers grouped by extension, with model counts and up to three example `/` IDs per group. The proxy extension must be on globally; otherwise the command prints how to turn it on and exits without creating a key or listener. It never enables the extension itself. The forwarder keeps running after the command returns, until `albedo proxy --stop`. Running `albedo proxy` again reuses it and prints its start time and the full summary. Different listen options require `--stop` first. `--foreground` serves in the calling terminal until Ctrl-C instead. Without `--save`, the detached process is not supervised and does not restart after a crash or reboot. A saved proxy runs through the user login service instead. Clients use the printed proxy key, not the daemon token. It lives at `$ALBEDO_HOME/proxy-key`, mode `0600`, and survives stops and restarts. `albedo proxy --new-key` replaces it atomically and revokes the previous key, including on an already-running forwarder. Every forwarded request requires `Authorization: Bearer `, even when the daemon allows anonymous loopback proxy access. The forwarder accepts only `GET /v1/models` and `POST /v1/chat/completions`; it adds the daemon token server-side. Other paths return 404 and other methods return 405. Streaming replies pass through without buffering. Chat request bodies are limited to 32,000,000 bytes. For example, configure an OpenAI-compatible client with: ```sh export OPENAI_BASE_URL=http://127.0.0.1:4318/v1 export OPENAI_API_KEY='the printed proxy key' ``` It listens on loopback only by default, unlike `albedo web`'s automatic tailnet sharing. To share explicitly, use the web command's `--allow ` syntax (repeatable; CIDR or an IPv4 address with trailing zero octets, such as `10.0.0.0` for `/8`). `--allow 100.64.0.0/10` shares the tailnet; the summary includes this machine's matching addresses and detected MagicDNS names. Only loopback clients and clients in an allowed network may connect; the proxy key is still required. Other hostnames and foreign browser origins are refused. `--save` stores the given networks in the daemon's `ui.proxy_allow` setting and installs a user login service. The service runs `albedo proxy --foreground` with those saved networks and retains the port and quiet flag. `--save` alone stops the proxy, removes its login service, and clears the saved networks. These settings are separate from `ui.web_allow` and never change web sharing. `--save` rejects `--public-url` and `--tailnet-https`; the login service does not retain reverse-proxy or Tailscale HTTPS claims. `--public-url https://proxy.example` prints and accepts the origin of an existing reverse proxy. `--tailnet-https` claims the machine's Tailscale HTTPS root through the same guarded `tailscale serve` machinery as `albedo web`, and releases its own handler when stopped. It refuses a root already serving something else (including albedo web) or one opened through Funnel. The two flags are exclusive. Neither flag silently opens a LAN listener. On macOS, the service is a LaunchAgent in `~/Library/LaunchAgents` with `RunAtLoad` and `KeepAlive`, bootstrapped into `gui/`. It starts at login and restarts after a crash. On Linux, it is a systemd user unit installed under `$XDG_CONFIG_HOME/systemd/user` (or `~/.config/systemd/user`), enabled and started immediately. Linux users without lingering receive a `loginctl enable-linger` hint; the command never enables lingering itself. Lingering permits startup at boot and operation after logout. Each role (`web` or `proxy`) and `ALBEDO_HOME` has its own service identity. The service preserves the invoked binary path, including moving package symlinks, and passes `ALBEDO_HOME`, `PATH`, and any explicit `ALBEDO_ROOT` or `ALBEDO_DAEMON`. `ALBEDO_DAEMON` and `ALBEDO_WEB_DIST` values under `/nix/store` are omitted so the current package wrapper supplies fresh defaults at each start; non-store overrides are retained. With a login service installed, a bare `albedo proxy` starts or kickstarts that service and prints its summary. `--stop` stops the service through its manager, so supervision does not restart it immediately. It returns at the next login; `albedo proxy --save` forgets it permanently. Other listen options require an explicit `--save` update or removal of the service. A per-home process lock prevents foreground, detached, and supervised forwarders from running together. Each request logs its model, HTTP status and duration to `$ALBEDO_HOME/proxy.log` in background mode, or stderr with `--foreground`. `--quiet` disables request logs. No prompt, response, or key is logged. The log is replaced on each detached or supervised start and discards older lines when it reaches 8 MiB. `$ALBEDO_HOME/proxy.json` records the listener PID, start time, options and summary; it contains no keys and is removed on a clean stop. If the daemon stops, the forwarder stays up and answers 503 with a message explaining how to start it again. A failed connection or daemon 401 refreshes the saved daemon endpoint, so a subsequent request reaches the replacement. A chat POST is never replayed automatically: retry it explicitly. Stopping the forwarder does not stop the daemon, and stopping it does not require a running daemon. Ownership: `cli/internal/app/proxy.go` reads the extension choice and model catalog; `cli/internal/proxyserver` owns keys, authentication and forwarding; `cli/internal/cli/proxy*.go` owns command rendering and lifecycle. `cli/internal/cli/login_service.go` owns the shared login-service definitions and manager calls; `proxy_service.go` owns the proxy workflow. `ALBEDO_LAUNCHCTL` and `ALBEDO_SYSTEMCTL` can override the manager executables for OS-boundary tests. The listener reuses `webui.Access`, network parsing, tailnet discovery and HTTPS claims, and the CLI's shared detached-process helpers. ## endpoint The proxy is a [service](extensions.md) mounted at `http://127.0.0.1:/extensions/proxy/v1`. Set `ALBEDO_PORT` in the daemon's environment to keep the port stable; otherwise it changes each time the daemon starts (see `$ALBEDO_HOME/daemon.json`). - `GET /extensions/proxy/v1/models` lists `/` for every usable profile in `config.json`. A profile with its own `baseUrl` (an API-key profile) lists only its saved model, because models.dev can only guess what an arbitrary endpoint serves. Other profiles, such as `codex` and `antigravity`, list their provider's catalog, or their saved model while no catalog is cached. Profiles that cannot be used, such as a malformed entry, are named under a nonstandard `errors` field (`[{"profile", "message"}]`) instead of failing the list. - The list only helps clients choose. Any `/` is requested as given, listed or not, and a broken or signed-out profile fails only its own requests. - `POST /extensions/proxy/v1/chat/completions` accepts `model` as `/`, or a bare `` for its saved model. Streaming (`stream`, `stream_options.include_usage`) and non-streaming replies are supported. Use the daemon bearer token as the client's API key. The operator can set `ALBEDO_PROXY_ALLOW_ANONYMOUS=1` when starting the daemon to allow anonymous loopback requests to these two proxy routes. Core and other extension routes still require their own authentication. Browser requests follow the daemon's configured origin policy. The request body limit is 32,000,000 bytes. Fixed-length and chunked uploads are supported; ambiguous framing returns `400`, and oversized bodies return `413`. A request without body framing has an empty body. ## translation Each request is translated into albedo's request types and projected onto the profile's upstream, as a session's history is: - Leading `system` and `developer` messages become the instructions. Later ones become user notes in place. - `user` text and base64 `data:` images become user input. Image urls are not fetched. - `assistant` messages with `tool_calls` become replayable tool calls. `tool` messages become their results. Assistant content may be null; nullable `refusal` is accepted and dropped. Nullable `reasoning_content` (or `reasoning`) is replayed as portable reasoning. - `tools`, `max_tokens`/`max_completion_tokens`, `temperature`, `top_p`, `stop`, `tool_choice`, `parallel_tool_calls`, `reasoning_effort` (or `reasoning.effort`), and `response_format` (`json_object` or `json_schema`) become albedo's generation options. Each provider applies what its wire can express: - OpenAI Chat Completions and Responses take all of them, except that Responses has no stop sequences. - Codex keeps its fixed sampling. Effort, tool choice, parallel calls, and format override its defaults (medium effort, automatic parallel tools). - Antigravity maps effort onto each model's thinking budget or level, and sampling, stop sequences, and format into `generationConfig`. Claude routes always run tools `VALIDATED`. On Gemini routes a forced tool choice is also restated as a final instruction, because Cloud Code Assist drops the tool mode there. Unknown top-level request fields are ignored; their names, never their values, are logged once per request at debug level. This includes provider routing hints and `prompt_cache_key`, which are not forwarded. `reasoning` accepts an object with optional `effort`; other keys are ignored. Fields used by the proxy still require valid types, and malformed JSON, duplicate keys and oversized bodies remain errors. Type errors name the affected field path. Text and reasoning stream as `delta.content` and `delta.reasoning_content`. Each tool call is sent whole, with its id, name, and arguments, when the turn ends. Errors from the provider arrive as an `error` chunk while streaming, or as a JSON error otherwise. ## state The proxy stores nothing. Each request carries its conversation, and provider identity that should be stable across it, such as a Codex account or an Antigravity trajectory, is derived from the profile and the first user message. Providers also return opaque state that they require back within a tool-calling turn: Codex's encrypted reasoning items, Gemini's thought signatures on function calls, and Claude's signed thinking. Clients echo only plain Chat Completions, but they always echo a tool call's id verbatim. So when a turn ends in tool calls, the proxy compresses that turn's native provider output, tags it with the profile that produced it, and appends it to the first call's id as `__albedo__`, using only `[A-Za-z0-9_-]`. When the history comes back, those turns become albedo transcript entries again and pass through the same projection a session uses. A request to the same profile replays them verbatim; a request to another profile gets the portable text and calls. Tool results are matched to the provider's own call ids. State larger than 256 KiB is not carried, and damaged state falls back to the portable history. Turns without tool calls carry no state, which the providers do not need after the next user message.