Skip to content

feat(gemini): a second model, on a key Caprock never holds - #8

Merged
dspv merged 3 commits into
masterfrom
feat/gemini-byok
Sep 1, 2026
Merged

feat(gemini): a second model, on a key Caprock never holds#8
dspv merged 3 commits into
masterfrom
feat/gemini-byok

Conversation

@dspv

@dspv dspv commented Sep 1, 2026

Copy link
Copy Markdown
Owner

The second paid feature. A user points Caprock at Google's Gemini with their own AI Studio key; Caprock makes the call and counts what it cost, and Google bills them directly.

The key is never stored

Read from GEMINI_API_KEY in the daemon's environment at call time — not written to config.json, not accepted by PUT /v1/settings, not returned by GET /v1/settings, and sent in a header rather than a query string so nothing that logs a URL can capture it.

This answers an objection that was already written down, in 17-teams.md: "a bug in Caprock shows a wrong number, and with a vault a bug in Caprock leaks credentials." A key held in the environment cannot leak from a database Caprock does not write. The cost is an honest one and the panel says it out loud: you set a variable and restart, which is a worse first run than pasting a key into a field.

The licence is checked on the server

Which the spend cap deliberately does not do. ADR-022 made the key a convenience rather than a lock, and the cap follows it — a free user who sets the threshold by curl gets a working cap, because a cap spends nothing.

This spends the user's Gemini quota and opens an outbound connection, so the check runs in the handler before the request leaves. A test asserts an unlicensed ask never reaches the client. ADR-023 keeps the precedent narrow on purpose: server-side gates belong to features that spend money or reach the network, not to features that draw a panel.

What makes it worth paying for is the context

Asked "what did I spend yesterday" with no figures, a model can only explain what spending is. Every question carries today's and the week's totals, the top models and projects, and what is running — all already computed for the dashboard.

What it does not carry is prompts, replies, tool output and file paths, each pinned by its own test. The database holds the prose Claude wrote and every command it ran, and none of that belongs in a request the user did not specifically ask for.

The model is chosen, and priced before it is spent

A question costs 0.04 cents on Flash Lite and 1.00 cent on Pro — twenty-five times, on the user's own card — so being locked to whichever default we picked was our choice made with their money. The list is built from the pricing table, so one place adds a model and the price shown is the price charged.

Sub-cent figures render in cents: fmtUSD prints $0.002 as $0.00, and a reader who believes a question is free will believe it fifty times.

Pricing

Eight Gemini text models added with their source and the date read. Three limits are stated in the notes rather than hidden: the introductory Flash rates expire 2026-12-31, Gemini has no per-token cache-write price (Google bills storage per hour), and the Pro models are tiered by prompt size so the ≤200k rate under-reports a long prompt.

Verification

make check green. 20+ new tests, including that an unlicensed request never reaches Google, that the key appears in no response body, and that prompts and tool output never enter the outbound context. Checked against a live daemon, not only tests: 402 without a licence, 8 priced models, key absent from every endpoint.

Also mocks xterm in Session.test.tsx (mounting the terminal tab printed six unactionable jsdom canvas errors) and stops polling /v1/gemini (the timer outlived its test and fired into a torn-down jsdom).

https://claude.ai/code/session_01DR8fggA2LRHcjNWUsqtDcF

dspv added 3 commits September 1, 2026 19:02
The second paid feature. A user points Caprock at Google's Gemini with
their own AI Studio key; Caprock makes the call and counts what it cost,
and Google bills them directly.

**The key is never stored.** It is read from GEMINI_API_KEY in the
daemon's environment at call time — not written to config.json, not
accepted by PUT /v1/settings, not returned by GET /v1/settings, and sent
in a header rather than the query string so nothing that logs a URL can
capture it. This is the direct answer to the objection in 17-teams.md:
"a bug in Caprock shows a wrong number, and with a vault a bug in Caprock
leaks credentials." A key held in the environment cannot leak from a
database Caprock does not write. The cost is an honest one and the panel
says it out loud: you set a variable and restart, which is a worse first
run than pasting a key into a field.

**The licence is checked on the server**, which the spend cap deliberately
does not do. The cap spends nothing, so a React-only paywall costs a free
user nothing to walk past; this spends their Gemini quota and opens an
outbound connection, so the check runs in the handler before the request
leaves. A test asserts an unlicensed ask never reaches the client. ADR-023
records the reasoning and keeps the precedent narrow: server-side gates
belong to features that spend money or reach the network, not to features
that draw a panel.

Answers enter the same event stream as everything else — an ordinary
turn.user + turn.assistant pair with source=gemini — so they are priced by
the same table, searched by the same index, and filtered by the same agent
column. Thinking tokens count as output because that is how Google bills
them; leaving them out would under-report exactly the models that reason
most.

Eight Gemini text models added to pricing.json with their source and the
date read. Three limits are stated in the notes rather than hidden: the
introductory Flash rates expire 2026-12-31, Gemini has no per-token
cache-write price (Google bills storage per hour), and the Pro models are
tiered by prompt size so the =<200k rate under-reports a long prompt.

Also mocks xterm in Session.test.tsx: mounting the terminal tab made jsdom
print six unactionable canvas errors, and an error nobody can act on
trains people to ignore the ones they can.

Claude-Session: https://claude.ai/code/session_01DR8fggA2LRHcjNWUsqtDcF
Two changes that turn a chat box into the feature people would pay for.

**The question carries the user's data.** Asked "what did I spend
yesterday" with no figures, a model can only explain what spending is.
Every answer now travels with today's and the week's totals, the top
models and projects, and what is running right now — all of it already
computed for the dashboard, so it costs nothing to hand over.

What goes is deliberately narrow: totals, model names, project names,
session counts. No prompts, no replies, no tool output, no file paths.
The database holds the prose Claude wrote and every command it ran, and
none of that belongs in an outbound request the user did not specifically
ask for; a test asserts each of those never appears. Someone who wants to
ask about one session can paste it, which is a decision rather than a
default. The costing basis travels with the figures so the model cannot
present a list price as the user's bill.

**The model is chosen, and priced before it is spent.** A question costs
0.04 cents on Flash Lite and 1.00 cent on Pro — twenty-five times, on the
user's own card — so being locked to whichever default we picked was our
choice made with their money. The picker lists every Gemini row in the
pricing table, cheapest first, each with what a short question costs at
its rates. The list comes from the table rather than a hand-kept array, so
one place adds a model and the price shown is the price charged.

Sub-cent figures are rendered in cents, not dollars: fmtUSD prints $0.002
as "$0.00", and a reader who believes a question is free will believe it
fifty times.

Also stops polling /v1/gemini. The key comes from the daemon's
environment and cannot change while it runs, so the timer bought nothing —
and it outlived the test that mounted it, firing into a torn-down jsdom.

Claude-Session: https://claude.ai/code/session_01DR8fggA2LRHcjNWUsqtDcF
17-teams.md said holding keys would trade the product's foundation for a
feature. That paragraph is not overturned by the Gemini work — it is the
reason the key lives in the environment, and the doc now says so rather
than leaving a reader to wonder whether we changed our minds.

Claude-Session: https://claude.ai/code/session_01DR8fggA2LRHcjNWUsqtDcF
@dspv
dspv merged commit 84c8e85 into master Sep 1, 2026
9 checks passed
@dspv
dspv deleted the feat/gemini-byok branch September 1, 2026 17:07
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