th llm onboard: connect Big Smooth to a Smoo org in one step - #506
Open
brentrager wants to merge 6 commits into
Open
brentrager wants to merge 6 commits into
brentrager wants to merge 6 commits into
Conversation
`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 detectedLatest commit: 4296010 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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.
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 exactlyonce),
th model login smooai-gateway(writes credentials into~/.smooth/providers.json). Joining them meant a human copying a live, billablecredential out of their terminal and pasting it into a second command.
th llm onboard(alias ofth llm provision) is the whole path:big-smooth-<hostname>on that org, through the existing org-admin-gatedAPI as the logged-in user.
providers.json→providers.json.bak-<stamp>(the namingth reclaimalready sweeps).
smooai-gatewayprovider and point every routing slot at it.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 fallout 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 withsmoo.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-idtakes a UUID or a name/slug substring; omitted, you get the same interactivepicker
th org switchuses; with no TTY it is a hard error.resolve_switch_orgrather than inventing selection UI.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 onthe 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-gatewayroute isrequireSupabaseUser+requireOrgAdminserver-side. Thisgoes through it as the
th auth loginuser — no LiteLLM admin key, no master key, nothingCLI-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 ADMINon 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
defaultkeyBig 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-key409 the default path throws is a normal state for such an org, not anerror 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>andoffers
--rotate(also how a second machine gets a value), never stacking keys. Thepre-check is a
GET /keys, so the 409 is never surfaced raw.Minting is not read-only
The route runs
syncOrgLlmLimitsbefore minting ("no cap, no key"), which re-stamps theorg'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, orbudgetDuration—overviewreturns{ key, spendMtd }and the key routesreturn neither. Filed rather than guessed.
Deliberately NOT shipped
--max-budget.POST /organizations/{org}/llm-gateway/keysaccepts{ name }and nothingelse — the route never passes
maxBudget/budgetDurationtocreateVirtualKey, so the flagwould be a switch wired to nothing. Pearl
th-a23962(smooai monorepo) covers both this andexposing the cap for read. When the cap ships, the window must ship with it:
max_budgetwith no
budget_durationis a LIFETIME cap that never resets.Tests — 15 assertions, each negative-controlled
Every one was broken, the run watched, and restored:
sanitize_key_namesanitized_names_always_satisfy_the_api_rulekey_existsmatches the mask instead of the namekey_exists_matches_by_name_onlywrite_stores_the_key_wires_routing_and_keeps_other_providers--credential-onlycredential_only_leaves_routing_alonekey_lost_messageomits the key valuea_failed_write_errors_and_the_message_carries_the_keywrite_gateway_keyswallows thesave_to_fileerrora_failed_save_errors_tooverify_gatewayreports success regardlessverification_fails_when_the_gateway_rejects_the_keysmooth-*aliasverify_model_is_concreteorg_labelfalls back to empty instead of the idorg_label_never_comes_out_emptyadmin_hintdrops the org name from the 403a_403_explains_the_admin_requirement_and_names_the_orgadmin_hintrewrites every failure as a permissions problemother_failures_are_left_aloneThe swallow-the-save mutation is why there are two write-failure tests: the first forces
failure in
back_upand 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_httpstub returning 401 — a rejected key must notread 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 bydesign),
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