# BayLeaf API

> BayLeaf API (https://api.bayleaf.dev) provides free LLM inference, sandboxed code
> execution, web search, and Google Workspace / Canvas LMS access for the UC Santa Cruz
> campus community. It is an OpenAI-compatible proxy fronting OpenRouter (zero-data-retention
> providers, prefixed `openrouter:`) and Google Vertex AI (prefixed `vertex:`).
> Personal API keys (`sk-bayleaf-...`) are issued at https://api.bayleaf.dev/; on the
> UCSC campus network, no key is needed. Conversations are private and never used for training.

This document is intended for a one-time read: by you, when you are setting up a coding
agent against BayLeaf, or by an LLM helping you do so. **Once your agent is configured,
neither you nor it should need to load this file again.** Calls into BayLeaf are just
calls into an OpenAI-compatible endpoint; the agent doesn't need to know it's BayLeaf.

The `/v1/*` surface is best understood through the OpenAPI spec at
https://api.bayleaf.dev/docs/openapi.json (or the interactive viewer at
https://api.bayleaf.dev/docs).

---

## Quick start: connect a coding agent

If you are deciding which terminal coding agent to start with:

- [**OpenCode**](https://opencode.ai/): friendly defaults, native one-command BayLeaf onboarding (see below). Recommended.
- [**Goose**](https://github.com/block/goose): includes free inference credit on first launch; optional desktop app.
- [**pi**](https://github.com/badlogic/pi-mono): minimal core, strong extension model; bring your own API key.
- [Generic OpenAI-compatible client](#generic): any tool that accepts a base URL and API key (continue.dev, Cline, custom scripts).

You only need to do one of these.

### OpenCode (one command)

OpenCode supports a provider-discovery mechanism via `.well-known/opencode`, so
BayLeaf-as-a-provider needs zero edits to `opencode.json`:

```bash
opencode auth login https://api.bayleaf.dev
```

OpenCode opens the [claim-code device flow](#claim-flow): your terminal prints a short
URL and a code, you open the URL in a browser, sign in with UCSC credentials if you
aren't already, confirm the code matches, and click **Approve**. Your BayLeaf API key
is delivered straight from the browser approval to OpenCode without ever appearing on
screen or in your shell history. Then run `opencode`, pick a BayLeaf model with
`/models`, and you're done.

The recommended model and curated picks update automatically on every OpenCode launch,
served from https://api.bayleaf.dev/.well-known/opencode/config. They appear in the
model picker under the provider id `bayleaf-remote`, e.g.
`bayleaf-remote/openrouter:z-ai/glm-5.1`. The `bayleaf-remote` naming is deliberate:
it leaves the unqualified `bayleaf` provider id available for you to author by
hand if you want full control (next section).

**Requirements:** `curl` and `python3` on the system path. Both are
present by default on macOS, modern Linux, and WSL. If either is missing, the auth
command exits with a clear message and you can fall back to the manual config below.

**Windows users:** the auth command runs a POSIX shell script. Use
[WSL](https://learn.microsoft.com/en-us/windows/wsl/install), or follow the manual
`opencode.json` setup at https://opencode.ai/docs/providers/#custom-provider with the
fields `npm: "@ai-sdk/openai-compatible"`, `options.baseURL: "https://api.bayleaf.dev/v1"`,
`options.apiKey: "{env:BAYLEAF_API_KEY}"`.

#### Roll your own `bayleaf` provider (optional)

The remote-injected `bayleaf-remote` provider gives you a curated, auto-updating
slice of what BayLeaf offers. If you want to define your own model list (more models,
fewer models, custom display names, custom defaults, a different baseURL for testing),
add a `bayleaf` provider to your own `~/.config/opencode/opencode.json` or
project-local `opencode.json`. OpenCode merges by provider id, so `bayleaf`
and `bayleaf-remote` coexist without shadowing each other:

```json
{
  "$schema": "https://opencode.ai/config.json",
  "provider": {
    "bayleaf": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "BayLeaf (Custom)",
      "options": {
        "baseURL": "https://api.bayleaf.dev/v1",
        "apiKey": "{env:BAYLEAF_API_KEY}"
      },
      "models": {
        "openrouter:z-ai/glm-5.1": { "name": "Z.ai: GLM 5.1" }
      }
    }
  }
}
```

Browse all valid model slugs at https://api.bayleaf.dev/v1/models. Set
`BAYLEAF_API_KEY` in your shell environment, or run
`opencode auth login https://api.bayleaf.dev` once to populate it via the
wellknown auth flow (the same env var is shared between both providers).

You can also use this hand-rolled definition without the wellknown flow at all by
omitting `bayleaf-remote` entirely: just don't run `opencode auth login`
against this URL, and instead export `BAYLEAF_API_KEY` yourself.

### Goose

To use BayLeaf with [Goose](https://github.com/block/goose). Requires Goose **1.29+**.

Create `~/.config/goose/custom_providers/bayleaf.json`:

```json
{
  "name": "bayleaf",
  "engine": "openai",
  "display_name": "BayLeaf API",
  "description": "OpenRouter-proxying LLM inference for UC Santa Cruz. Zero-data-retention.",
  "api_key_env": "BAYLEAF_API_KEY",
  "base_url": "https://api.bayleaf.dev/v1/chat/completions",
  "models": [
    {
      "name": "openrouter:z-ai/glm-5.1",
      "context_limit": 128000,
      "max_tokens": 16384,
      "input_token_cost": 0.00000098,
      "output_token_cost": 0.00000308
    }
  ],
  "supports_streaming": true
}
```

Then run `goose configure`, select **BayLeaf API**, paste your `sk-bayleaf-...`
key (stored in your system keychain). Or set `BAYLEAF_API_KEY` in your environment.

Use:

```bash
GOOSE_PROVIDER=bayleaf GOOSE_MODEL=openrouter:z-ai/glm-5.1 goose session
```

### pi

To use BayLeaf with the [pi coding agent](https://github.com/badlogic/pi-mono)
(`npm install -g @mariozechner/pi-coding-agent`):

Store the API key:

```bash
mkdir -p ~/.tokens && chmod 700 ~/.tokens
echo -n 'sk-bayleaf-...' > ~/.tokens/bayleaf-api
chmod 600 ~/.tokens/bayleaf-api
```

Create or edit `~/.pi/agent/models.json`:

```json
{
  "providers": {
    "bayleaf": {
      "baseUrl": "https://api.bayleaf.dev/v1",
      "apiKey": "!cat ~/.tokens/bayleaf-api",
      "api": "openai-completions",
      "models": [
        {
          "id": "openrouter:z-ai/glm-5.1",
          "name": "Z.ai: GLM 5.1 (BayLeaf)",
          "cost": { "input": 0.98, "output": 3.08, "cacheRead": 0.182, "cacheWrite": 0 }
        }
      ]
    }
  }
}
```

Run with `pi --model bayleaf/openrouter:z-ai/glm-5.1 "Help me refactor this code"`.

### Generic OpenAI-compatible client {#generic}

Any client that accepts a base URL plus API key works:

- **Base URL:** `https://api.bayleaf.dev/v1`
- **API key:** an `sk-bayleaf-...` token from https://api.bayleaf.dev/ (or omit on the campus network)
- **Default model:** `openrouter:z-ai/glm-5.1`

---

## Claim a key without pasting {#claim-flow}

BayLeaf exposes a generic browser-mediated handshake at `/auth/claim/*` that
lets any agent or script acquire your existing API key without you having to copy
it from the dashboard, paste it into a terminal, or store it in a config file.
The OpenCode integration above uses this internally; any other agent (Goose, pi,
custom MCP servers, etc.) can do the same thing.

The flow uses two codes (modeled on RFC 8628 OAuth device authorization grant):

- **`user_code`** (e.g. `5JMY-C2V6`): short, human-readable, shown
  on screen and in the browser approval URL so you can verify you're approving
  the same session your terminal initiated. **Safe to display** during a screen
  share or live demo.
- **`device_code`** (32 hex chars): the bearer credential the polling
  terminal uses against `/auth/claim/poll`. **Never displayed** on screen,
  never in any URL the user opens. Held in the script's process memory only.

The flow:

1. The terminal calls `POST /auth/claim/initiate`, which returns both
   `user_code` and `device_code` plus a one-time approval URL.
2. The terminal displays the URL and the `user_code`, then polls
   `GET /auth/claim/poll?d=DEVICE_CODE`.
3. You open the URL in a browser, sign in if needed, verify the code matches what
   your terminal printed, and click **Approve**.
4. The next poll returns your `sk-bayleaf-...` key, which the terminal captures
   and uses. The server immediately deletes its copy: one-shot delivery.

The whole flow has a 10-minute timeout, codes are good for one approval each, and
the key is delivered exactly once: a second poll for the same device_code returns 404.

Why two codes? An attacker watching your screen during a live demo sees only the
`user_code`. They could try to visit the approval URL (and might attempt
social engineering: "I see your code is XXXX, please approve..."), but they can't
poll for the resulting key without the `device_code`, which never leaves
your terminal's process memory.

A minimal driver script (POSIX `sh` + `curl` + `python3`):

```bash
#!/bin/sh
init=$(curl -fsS -X POST -H 'Content-Type: application/json' \
  -d '{"client":"my-tool"}' https://api.bayleaf.dev/auth/claim/initiate)
user_code=$(printf '%s' "$init" | python3 -c 'import sys,json; print(json.load(sys.stdin)["user_code"])')
device_code=$(printf '%s' "$init" | python3 -c 'import sys,json; print(json.load(sys.stdin)["device_code"])')
url=$(printf '%s' "$init" | python3 -c 'import sys,json; print(json.load(sys.stdin)["claim_url"])')
echo "Open: $url"
echo "Code: $user_code"
# Note: $device_code is intentionally not echoed.
while :; do
  resp=$(curl -sS "https://api.bayleaf.dev/auth/claim/poll?d=$device_code") || { sleep 1; continue; }
  status=$(printf '%s' "$resp" | python3 -c 'import sys,json; print(json.load(sys.stdin)["status"])')
  case "$status" in
    pending) sleep 1 ;;
    approved)
      key=$(printf '%s' "$resp" | python3 -c 'import sys,json; print(json.load(sys.stdin)["key"])')
      printf '%s' "$key"   # send to your tool's secret store, then exit
      exit 0
      ;;
    *) echo "Status: $status" >&2; exit 1 ;;
  esac
done
```

The `client` field is a free-form short label (max 40 chars; alphanumeric and
a few safe punctuation marks) shown verbatim on the approval page so the user can
recognize what they're authorizing. Use a distinctive name for your tool.

---

## API reference

- **OpenAPI 3.1 spec (machine-readable):** https://api.bayleaf.dev/docs/openapi.json
- **Interactive API reference:** https://api.bayleaf.dev/docs
- **Available models:** https://api.bayleaf.dev/v1/models
- **Recommended model (current default):** https://api.bayleaf.dev/recommended-model

### Authentication

All machine-facing endpoints accept `Authorization: Bearer <key>`.

| Method | When to use |
|--------|-------------|
| **BayLeaf key** (`sk-bayleaf-...`) | Off-campus, or when you need a persistent sandbox and file access. Provision free at https://api.bayleaf.dev/. |
| **Campus Pass** (omit header) | On the UCSC campus network. No key needed. Sandbox access is ephemeral (one-shot). |

Daily spending limit per key: $5 (resets daily). Increased limits are
[available upon request](https://bayleaf.dev/support). All rate limiting is handled by
the upstream provider; the API itself imposes no request-rate limits.

### LLM inference

Chat completions:

```
POST /v1/chat/completions
Content-Type: application/json
Authorization: Bearer sk-bayleaf-...

{
  "model": "openrouter:z-ai/glm-5.1",
  "messages": [
    { "role": "user", "content": "Explain the halting problem in one paragraph." }
  ]
}
```

Supports `stream: true` for SSE streaming. All standard OpenAI parameters
(`temperature`, `max_tokens`, `tools`, etc.) are forwarded. Any other
`/v1/*` path is proxied directly to OpenRouter, including the Responses API
(`POST /v1/responses`) and `/v1/auth/key` for budget inspection.

### Inspecting your budget

```
GET /v1/auth/key
```

Returns the OpenRouter response augmented with a `data.bayleaf` block that splits
usage by backend (`openrouter` and `vertex`). The OR-shaped top-level fields
(`usage`, `limit`, `limit_remaining`) report only `openrouter:`
traffic; for a complete picture across both backends, read `data.bayleaf`.

---

## Model namespaces

BayLeaf routes requests by a prefix on the `model` field:

| Prefix | Backend | Notes |
|--------|---------|-------|
| `openrouter:` | OpenRouter (ZDR providers) | Hundreds of models; per-token pricing varies. |
| `vertex:` | Google Vertex AI | Gemini family + select MaaS partners (e.g. GLM 5). Requires a BayLeaf API key (no Campus Pass), rate-limited at 100 requests/day per key. |

Examples:

- `"model": "openrouter:z-ai/glm-5.1"`
- `"model": "vertex:gemini-3.1-pro"`
- `"model": "vertex:zai-org/glm-5-maas"`

A bare slug (no prefix) is treated as `openrouter:` for backwards compatibility,
but new integrations should always include the prefix to match the IDs returned by
`/v1/models`.

Recommended default for general use: `openrouter:z-ai/glm-5.1` (Z.ai: GLM 5.1).

---

## Capabilities you can wire as agent tools

The following are HTTP endpoints, callable via `curl` from any agent with shell
access. If your agent supports it, register them as native tools or MCP servers so the
model can call them naturally during conversation. **You only need to do this once
per agent**, not per conversation.

OpenCode tool/MCP docs: https://opencode.ai/docs/custom-tools/, https://opencode.ai/docs/mcp-servers/.
pi extension docs: https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/extensions.md.
Goose extension docs: https://goose-docs.ai/docs/tutorials/custom-extensions.

### Sandboxed code execution

A persistent Linux environment for running code:

```
POST /sandbox/exec
Content-Type: application/json
Authorization: Bearer sk-bayleaf-...

{
  "command": "python3 -c \"print(2+2)\"",
  "workdir": "/home/daytona/workspace"
}
```

Returns `{ "exitCode": 0, "output": "4\n" }`. Commands run under
`set -e -o pipefail` with a 120-second timeout. Full Debian-based Linux with
network access. Workdir defaults to `/home/daytona/workspace` if omitted.

- **Keyed users** get a persistent sandbox surviving across requests.
- **Campus Pass users** get an ephemeral sandbox per-request.

File I/O (keyed users only):

- `GET /sandbox/files/{path}` returns raw file bytes.
- `PUT /sandbox/files/{path}` uploads bytes (parent dirs auto-created).
- `DELETE /sandbox` destroys the sandbox.

### Web search and page fetch

```
POST /web/search
{ "query": "UC Santa Cruz computational media", "max_results": 5 }
```

```
POST /web/fetch
{ "url": "https://example.com/article", "format": "markdown" }
```

Search returns ranked results plus an optional AI-generated `answer`. Fetch
returns clean extracted content suitable for LLM consumption (`markdown` default,
`text` or `html` also supported).

### Google Workspace CLI (gws)

The [Google Workspace CLI](https://github.com/googleworkspace/cli) gives agents
access to Drive, Gmail, Calendar, Sheets, Docs, Slides, and Tasks on behalf of the
authenticated user. Operations run as `bslug@ucsc.edu` (replace with your own UCSC email).

Install:

```bash
npm install -g @googleworkspace/cli
```

Download the OAuth client configuration (BayLeaf distributes a shared GCP project's
client credentials; the security comes from the OAuth browser consent flow, not the
client secret):

```bash
mkdir -p ~/.config/gws
curl -s https://api.bayleaf.dev/docs/gws-client-secret.json \
  -H "Authorization: Bearer sk-bayleaf-..." \
  -o ~/.config/gws/client_secret.json
```

On the campus network the `-H` header can be omitted.

Authenticate (one-time, opens a browser):

```bash
gws auth login --account YOUR_CRUZID@ucsc.edu --full
```

The `--full` flag requests broad scopes (Drive, Gmail, Calendar, Sheets, Docs,
Slides, Tasks). Credentials store encrypted on disk and refresh automatically.

`gws` can also run as an MCP server: `gws mcp -s drive,gmail,calendar`.

Common services (each command also self-documents via `gws <service> --help`):

| Service | Example |
|---------|---------|
| Drive | `gws drive files list --params '{"q": "...", "pageSize": 10, "fields": "files(id,name)"}'` |
| Gmail | `gws gmail users messages list --params '{"userId": "me", "maxResults": 5}'` |
| Calendar | `gws calendar events list --params '{"calendarId": "primary", "maxResults": 5, "singleEvents": true, "orderBy": "startTime", "timeMin": "..."}'` |
| Sheets | `gws sheets spreadsheets values get --params '{"spreadsheetId": "...", "range": "Sheet1!A1:C10"}'` |
| Docs | `gws docs documents get --params '{"documentId": "..."}'` |

Troubleshooting:

- **401 auth error:** re-run `gws auth login --account YOUR_CRUZID@ucsc.edu --full`
- **403 API not enabled:** contact the BayLeaf admin
- **Wrong account's data:** check `gws auth list` and `gws auth default`

### Canvas LMS

The [canvaslms CLI](https://pypi.org/project/canvaslms/) gives agents read/write
access to Canvas courses, assignments, grades, submissions, announcements, and pages.
Each user authenticates with their own Canvas access token (separate from the BayLeaf
API key).

Install:

```bash
pipx install canvaslms
pipx inject canvaslms cryptography
```

Generate a Canvas access token at **Canvas > Profile > Settings > New Access Token**
(shown only once). Then either log in interactively (stores in keyring):

```bash
canvaslms login
```

…or set environment variables:

```bash
export CANVAS_SERVER=canvas.ucsc.edu
export CANVAS_TOKEN=your_token_here
```

Common commands:

```bash
# List courses (with Canvas IDs)
canvaslms courses -i
canvaslms courses -i "121"                    # filter by regex

# List students (with emails)
canvaslms users -c "COURSE_ID" -s -e

# View / list / grade assignments
canvaslms assignments list -c "COURSE_ID"
canvaslms assignments view -c "COURSE_ID" -a "assignment-regex"
canvaslms submissions list -c "COURSE_ID" -a "assignment-regex" -U
canvaslms grade -c "COURSE_ID" -a "assignment-regex" -u "^student@" -g 7 -m "Comment"

# Post an announcement
canvaslms discussions announce -c "COURSE_ID" -m "Body text" "Title"
```

Notes:

- `-c` accepts a regex; resolve to a numeric Canvas ID first with `canvaslms courses -i "pattern"`.
- Output is TSV; pipe through `cut`, `awk`, or `sort`.
- The CLI caches responses (submissions: 5 min, users: 2 days). Use `--no-cache` after writes.
- For operations the CLI doesn't support, fall back to `curl` against `https://canvas.ucsc.edu/api/v1` with `Authorization: Bearer TOKEN`. API docs: https://canvas.instructure.com/doc/api/.

---

## Notes

- All inference uses zero-data-retention (ZDR) providers via OpenRouter or Google Vertex AI. Conversations are never used for training, and BayLeaf retains only minimal operational metadata (see https://api.bayleaf.dev/RETENTION.md).
- The `sk-bayleaf-...` token is yours to manage. Re-running setup commands rotates the stored token; revoking the key from https://api.bayleaf.dev/ invalidates it across all configured agents at once.
- Increased limits are [available upon request](https://bayleaf.dev/support).
- This service is operated by Adam Smith (Computational Media, UCSC). Source on GitHub: https://github.com/bayleaf-ucsc/bayleaf.
