small gleam coding and (not yet) persistent agent daemon with a detachable cli forked from okami.mom/albedo
albedo robot-docs proxy.md
12 kB · 72 lines
Markdown
at main

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 <profile>/<model> 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 <proxy-key>, 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:

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 <network> 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/<uid>. 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/loginservice 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 mounted at http://127.0.0.1:<port>/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 <profile>/<model> 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 <profile>/<model> 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 <profile>/<model>, or a bare <profile> 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 <id>__albedo__<state>, 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.