Skip to content

th llm onboard: connect Big Smooth to a Smoo org in one step - #506

Open
brentrager wants to merge 6 commits into
mainfrom
th-llm-provision
Open

brentrager wants to merge 6 commits into
mainfrom
th-llm-provision

Conversation

@brentrager

@brentrager brentrager commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

What this is

Big Smooth is something a Smoo AI org adopts, and there was no adoption path. Three
pieces existed and nothing joined them: th auth login (the Smoo session),
th llm keys create <name> (mints a LiteLLM virtual key, prints the value exactly
once
), th model login smooai-gateway (writes credentials into
~/.smooth/providers.json). Joining them meant a human copying a live, billable
credential out of their terminal and pasting it into a second command.

th llm onboard (alias of th llm provision) is the whole path:

  1. Reuse the Smoo session — sign in if there isn't one.
  2. Choose the org. No silent default.
  3. Mint big-smooth-<hostname> on that org, through the existing org-admin-gated
    API as the logged-in user.
  4. Back up providers.jsonproviders.json.bak-<stamp> (the naming th reclaim
    already sweeps).
  5. Write the key into the smooai-gateway provider and point every routing slot at it.
  6. Make one real call through the gateway and report the result.

The key value never touches the terminal. The single exception is a failed write —
at that point the key is live, billable, and unrecoverable, so it is printed loudly with
the name to revoke it by, and the command exits non-zero.

Why the org matters

The key is minted on the onboarding org, so that org's LiteLLM team and budget are
what Big Smooth spends against (team_id == org_id). Per-org billing and isolation fall
out of the existing model — no platform special-case. That is exactly what is broken
today: Big Smooth runs on the master org's __backend__ key, sharing a team with
smoo.ai's public chat agent, and in 2026-08 it spent $377.92 of $396.44 (95.3%) of that
team's budget and took the public agent down with it (that agent's own key had spent
$11.08).

So org selection is a first-class step, not a flag with a default:

  • --org-id takes a UUID or a name/slug substring; omitted, you get the same interactive
    picker th org switch uses; with no TTY it is a hard error.
  • It reuses resolve_switch_org rather than inventing selection UI.
  • Silently taking the active org would bill Big Smooth to whichever customer you last
    looked at — and the first user of this administers ~20 orgs.

--org-id <other-org> is the only thing that changes the blast radius: a second key on
the same org gives attribution (per-key spend in LiteLLM_SpendLogs), not isolation.
That sentence is in --help, the output footer, the changeset, and the CLI guide.

Auth: the existing route, no side path

Every llm-gateway route is requireSupabaseUser + requireOrgAdmin server-side. This
goes through it as the th auth login user — no LiteLLM admin key, no master key, nothing
CLI-specific in what onboarding means, so the product UI can call the same thing. Failures
are reported in product terms: a 403 becomes "connecting Big Smooth to <org> needs ADMIN
on that org — ask an admin to run this, or pick an org you administer"; a missing session
signs in rather than erroring.

Named key, not the org's default key

Big Smooth mints its own named key via the named-keys path. An org can already have a
gateway key for its own reasons, so "the org has a key" is not "Big Smooth is onboarded" —
and the create-key 409 the default path throws is a normal state for such an org, not an
error to route around. A named key also keeps Big Smooth's spend separable in
LiteLLM_SpendLogs. Same team, so same budget: attribution, not isolation.

Idempotency follows from that: re-running recognises already connected to <org> and
offers --rotate (also how a second machine gets a value), never stacking keys. The
pre-check is a GET /keys, so the 409 is never surfaced raw.

Minting is not read-only

The route runs syncOrgLlmLimits before minting ("no cap, no key"), which re-stamps the
org's tier budget onto its team — the exact mechanism behind the outage above (a mint put a
monthly-sized cap on a team carrying months of lifetime spend). The output says this
happened. It does not report the resulting number: no API surface returns the tier,
maxBudget, or budgetDurationoverview returns { key, spendMtd } and the key routes
return neither. Filed rather than guessed.

Deliberately NOT shipped

--max-budget. POST /organizations/{org}/llm-gateway/keys accepts { name } and nothing
else — the route never passes maxBudget/budgetDuration to createVirtualKey, so the flag
would be a switch wired to nothing. Pearl th-a23962 (smooai monorepo) covers both this and
exposing the cap for read. When the cap ships, the window must ship with it: max_budget
with no budget_duration is a LIFETIME cap that never resets.

Tests — 15 assertions, each negative-controlled

Every one was broken, the run watched, and restored:

mutation test that failed
drop the leading-char strip in sanitize_key_name sanitized_names_always_satisfy_the_api_rule
key_exists matches the mask instead of the name key_exists_matches_by_name_only
skip the backup write_stores_the_key_wires_routing_and_keeps_other_providers
skip the retired-alias migration
start from a fresh registry (evict other providers)
wire routing even under --credential-only credential_only_leaves_routing_alone
key_lost_message omits the key value a_failed_write_errors_and_the_message_carries_the_key
write_gateway_key swallows the save_to_file error a_failed_save_errors_too
verify_gateway reports success regardless verification_fails_when_the_gateway_rejects_the_key
verify against a retired smooth-* alias verify_model_is_concrete
org_label falls back to empty instead of the id org_label_never_comes_out_empty
admin_hint drops the org name from the 403 a_403_explains_the_admin_requirement_and_names_the_org
admin_hint rewrites every failure as a permissions problem other_failures_are_left_alone

The swallow-the-save mutation is why there are two write-failure tests: the first forces
failure in back_up and never reaches the save, so it still passed under that mutation.
The read-only-directory case is the one that actually covers it.

Verification is tested against a tiny_http stub returning 401 — a rejected key must not
read as verified.

Gates

cargo fmt --all --check, cargo clippy --workspace --all-targets (exit 0, zero errors;
the repo deliberately does not pass -D warnings — pedantic/nursery are advisory by
design), cargo test -p smooai-smooth-cli → 801 + 3 passed. Changeset included (minor).
CI was green on the first push.

Nothing was minted against prod — the network paths are unexercised by design, for Brent to
dogfood.

🤖 Generated with Claude Code

`th auth login`, `th llm keys create`, and `th model login` all existed and
nothing joined them, so provisioning Big Smooth meant a human copying a
once-shown credential out of the terminal and pasting it into a second command.

`th llm provision` does the whole path — reuse the session (or sign in), mint
`big-smooth-<hostname>`, back up providers.json, write the key into the
smooai-gateway provider with concrete (not retired `smooth-*`) routing, then make
one real gateway call to prove it works. The value only ever reaches the terminal
when the write failed, because at that point it is live, billable, and gone.

A new key gives ATTRIBUTION, not isolation: LiteLLM budgets per team and the team
is the org, so `--org-id` is the only real boundary. The help text says so.
The negative control for "a failed write surfaces the key" still passed when
save_to_file's error was swallowed — the existing test forces failure in back_up
and never reaches the save. Add a read-only-directory case that does.
@changeset-bot

changeset-bot Bot commented Aug 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4296010

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@smooai/smooth Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Big Smooth is something a Smoo org adopts, and the key is minted ON that org — so
its LiteLLM team and budget are what Big Smooth spends against (team_id == org_id)
and per-org billing falls out of the existing model. That reframes three things:

- The org is CHOSEN, never defaulted. Reuses the `th org switch` resolver (UUID /
  name-slug substring / interactive picker, hard error with no TTY). Silently
  taking the active org would bill Big Smooth to whichever customer you last
  looked at — and the picker's user is an admin of ~20 orgs.
- Idempotency is "already connected to <org>", not "a key of that name exists",
  and `--rotate` is framed as how a second machine gets a value.
- Output is onboarding: which org Big Smooth is connected to, that spend bills
  there, that minting re-applied the org's tier budget to its team (the route
  calls syncOrgLlmLimits — "no cap, no key" — which is the mechanism behind the
  outage this exists to fix), and how to disconnect.

Still the existing org-admin-gated API route as the logged-in user — no LiteLLM
admin key, no side path. A 403 now reads "connecting Big Smooth to <org> needs
ADMIN on that org" instead of a raw status line.

Big Smooth mints its own NAMED key rather than the org's single `default` key: an
org can already have a gateway key for its own reasons, so "the org has a key" is
not "Big Smooth is onboarded", and a named key keeps its spend separable.
@brentrager brentrager changed the title th llm provision: mint a gateway key straight into providers.json th llm onboard: connect Big Smooth to a Smoo org in one step Aug 25, 2026
…applied

Converging with the Big Smooth dashboard (smooai, SMOODEV-bigsmooth-ui), which
represents connection state as an active org_llm_keys row — the key IS the
connection, so nothing can disagree with reality. Adopting that, with one change:
it has to be a PREFIX (`big-smooth` or `big-smooth-<x>`), not one exact name.

Big Smooth runs on more than one machine. A single shared `big-smooth` key means
the second machine to onboard must rotate — which invalidates the first machine's
key — and every machine's spend lands in one bucket, losing the attribution that
is the only thing a per-key mint buys. So `onboard` keeps minting per machine and
reports the org's other Big Smooth keys as context; a second machine on an
already-connected org is normal and needs no --rotate.

Also consumes `overview`'s new `limits` payload to print the cap the mint just
applied (syncOrgLlmLimits, "no cap, no key"). Best-effort — an older deployment
without the field just says less. The window is printed WITH the cap, and a
budget carrying no budgetDuration is labelled "LIFETIME — never resets": reading
one of those as monthly is what turned a routine mint into an outage.
overview's limits is a pure computation from the org's tier — no LiteLLM
round-trip — so the number can disagree with the live team if someone edited it
directly. Printing it unqualified invites reading it as the real ceiling during
an incident.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant