guide

The AI route

Your app talks to OpenAI, Anthropic or Google through Gemmein, on your own provider key, which never reaches the browser, through a named AI tool whose definition lives on the server: the instructions, the prompt template, the inputs it takes, the model, the caps, beside the price in credits and the access gate. A signed-in person's app sends a name and inputs; Gemmein composes the request, spends the tool's credits, adds your key, and streams the answer back.

What it is. A named AI operation, defined on the server, that you price in credits and gate by access.

Does. Composes the provider request from the tool's own instructions and template with the inputs the app sent; runs it on your key with the pinned model and the output cap; spends the tool's credits before the call and refunds them if the provider fails before answering; records every call on ai_calls, with the prompt and answer only when you switch that on for the tool.

Does not. Does not let the browser compose the request or see the prompt (raw calls are off unless the owner switches them on for a provider key); does not price by token; does not let the browser set a price, a model or a gate; does not run from a server key; does not rename a tool after creation.

Needs something else when. You want a plan-dependent price for the same operation → make two tools and gate each; you meter something that is not an AI call → spendCredits from your server; you need images, audio or embeddings → your server calls the provider directly (chat is the first kind).

Example. "Deep Research", openai, gpt-4o, 20 credits, requires access:pro-max, with instructions and a question input.

Also true of the route itself. Your server makes the model call: call the provider directly with your key, and ask Gemmein's server gate who the person is and what they hold. The call is not a chat call, such as embeddings, images or audio: the provider directly, from your server. The provider is not on the list: write to hello@gemmein.com.

Tools

You create a tool on the dashboard's AI tools page, or your AI writes a file at gemmein/ai/tools/<name>.json and npx gemmein sync carries it to development; npx gemmein sync --live carries it into production with a sync key (Secret keys → production → Sync key: your sign-in code to mint, one hour to use, shown once, never saved). The file owns the implementation on every sync; the dashboard owns the commerce (label, credits, gate, on/off, record calls) after creation. A tool can be defined before its provider key exists: runs are refused ai_not_configured until the key is added on the AI tools page, and the console shows the tool as waiting on a key until then. A tool's name is fixed once created: it is what the app calls.

FieldRule
name1–40 lowercase letters, numbers or hyphens, starting with a letter or number. Fixed once created
label1–60 characters, shown to the person and on the ledger
provider / modelopenai, anthropic or google; pin the model a composed call runs on
creditsa whole number from 1 to 10,000
requiresan entitlement key like access:pro-max; a person without it is refused
instructionsthe system prompt, up to 20,000 characters. Never leaves the server
promptTemplatethe user turn, with {{input}} placeholders naming declared inputs; absent, the inputs are rendered one per line
inputsup to 20 of { name, type: text | number | boolean, required?, maxLength? }; a text input holds 4,000 characters unless it says (up to 20,000)
recordCallskeep the prompt and answer on ai_calls. Off by default
boundsmaxOutputTokens (a composed call's ceiling; 4,096 when unset), stream, maxBodyBytes (raw calls only, up to 262,144)

A tool file for the example above, at gemmein/ai/tools/deep-research.json:

{
  "label": "Deep Research",
  "provider": "openai",
  "model": "gpt-4o",
  "credits": 20,
  "requires": "access:pro-max",
  "instructions": "You are a careful research assistant. Answer with sources.",
  "promptTemplate": "Research this for a {{audience}} reader:\n\n{{question}}",
  "inputs": [
    { "name": "question", "type": "text", "required": true, "maxLength": 2000 },
    { "name": "audience", "type": "text" }
  ],
  "bounds": { "maxOutputTokens": 4000 }
}

A person without the entitlement sees "Deep Research requires Pro Max."; one short of the price sees "Deep Research costs 20 credits. You have 7."; the ledger line reads "20 credits spent · Deep Research · 87 remaining." One tool holds one credit price: a plan-dependent price for the same operation is two tools, each gated.

The worked case: a name and inputs; the server composes the rest

The app never sends a prompt or a provider request. It names the tool and passes the inputs the tool declared; Gemmein composes the request in the provider's own grammar, from the instructions and the template on the server. The answer is the provider's own shape, so a streaming run streams:

const res = await g.ai.run("deep-research", { question: text, audience: "beginner" }, { stream: true })

for await (const chunk of res.body) render(chunk)   // the provider's SSE, byte for byte
res.headers.get("x-gemmein-credits-remaining")     // the balance after this call

For a non-stream answer as one string, whichever provider answered, and for the person's own history:

const answer = await g.ai.runText("deep-research", { question: text })
const { calls } = await g.ai.calls()   // what they ran, when, what it cost, how it ended

g.ai.runText reads choices[0].message.content from OpenAI, joins content[].text from Anthropic, and joins candidates[0].content.parts[].text from Google. A provider's non-2xx answer is returned as it came by g.ai.run, not thrown; g.ai.runText, like g.ai.text, throws it as provider_error with the provider's status and message; a refusal by Gemmein throws GemmeinError with one of the codes below. An unknown input, a missing required one, a wrong type or a value over its cap is refused by name before anything is spent (invalid_inputs).

Raw calls: off by default

g.ai.chat(body), where the browser sends the provider's own request, is off until you switch raw calls on for that key on the AI tools page. Behind the switch it is the route it always was: the body forwarded as sent, the answer passed back byte for byte, a call that names no tool running as the default tool for one credit. Without the switch it answers raw_calls_off. The local rail's fake provider, with no key set, keeps raw calls open; there is no switch locally.

The record: ai_calls

Every call lands as one row in the ai_calls collection, per person: the tool, the kind, the provider and model, tokens in and out when the provider said, the credits it cost, how it ended, the refusal code, the latency, the time. The prompt and the answer ride the row only for a tool whose record calls switch is on. A person reads their own with g.ai.calls(); you read them by app in the Records room and on the person's record; erasure removes a person's rows.

The key, the providers, the models

The AI tools page holds one key per provider, per environment. Paste it, make a test call, and it is set; the room shows the last four characters of a key of 16 or more characters (a shorter key shows dots) and the last call. A key is 8 to 512 printable characters. Replace it by pasting again. Removing it is a step-up action, and your app's AI features stop the moment it is removed. Optionally, list up to 20 models the app may call (the test call uses the first); any other model answers model_not_allowed. The model name is the provider's, 1 to 80 characters of letters, digits, dots, colons, underscores and hyphens; Google needs one, OpenAI and Anthropic refuse a missing one themselves. ?provider= and ?stream=1 on the URL do what the body fields do.

ProviderThe body you sendWhere it goes
openaiChat completions: model, messages, stream?OpenAI's chat completions endpoint
anthropicMessages: model, max_tokens, messages, stream?Anthropic's messages endpoint
googlegenerateContent: model, contents; stream: true selects the streaming formGoogle's generateContent endpoint for that model

Those three endpoints are the whole list. There is no field for a URL, and no other host is reachable through the route.

Credits and the refund rule

The tool's credits are spent before the request is forwarded; the default tool spends one. A person short of the price is answered 402 credits_exhausted before anything is sent, with the balance in the message. Those credits are refunded only when the provider fails before its first byte, a non-2xx answer or no answer at all; the response then carries x-gemmein-credit: refunded. A stream that dies after the first byte is not refunded, and hanging up early does not refund. Every answer that passed the spend carries x-gemmein-credits-remaining. x-gemmein-tool names the tool; absent on the implicit default. A provider that echoes the key in a refusal reaches the app as ***<hint>. How credits are bought, granted and spent is on Credits.

Numbers

  • The credits the owner set for that tool; one by default.
  • Up to 50 tools per environment; a name is at most 40 characters, a label at most 60.
  • 20 calls per person per minute.
  • 64 KB of inputs on a g.ai.run call.
  • 256 KB raw request body on a g.ai.chat call, unless a tool sets a smaller cap (up to 262,144 bytes); nested at most 32 levels.
  • 170 seconds in all; on a stream, 10 seconds to the first response headers.
  • Every call counts toward the app's request band like any other request.
  • One test call per minute from the AI tools page; a test call spends no credits.

Codes

CodeStatusMeaning · what to do
raw_calls_off403The browser may not compose provider requests for this provider. Call a named tool with g.ai.run, or switch raw calls on for the key on the AI tools page
invalid_inputs400An input is unknown, missing, the wrong type or over its cap; the message names it
tool_incomplete409The tool composes nothing: no template and no inputs
unknown_tool404No AI tool by that name in this environment
tool_disabled403The owner switched this tool off
entitlement_required403The tool requires a plan or product this person lacks; the message names it
model_pinned403This tool's model is fixed; leave model out of the body
too_many_tools409An environment holds at most 50 AI tools
invalid_tool400Creating or updating a tool with a bad field; the message names it
credits_exhausted402The person's balance is below the tool's price; the message carries it. Show the pack
ai_not_configured409No provider key on this app and environment. The owner pastes one on the AI tools page, and the tool then runs unchanged
provider_required400More than one provider key is set; pass provider
model_not_allowed403The owner's allowlist names the models this app may call; the message lists them
ai_capped42920 calls per person per minute; wait for resetAt
payload_too_large413On g.ai.run: the inputs are over 64 KB; send less. On g.ai.chat: the raw body is over 256 KB; shorten the conversation you send
invalid_body400The body is not the provider's JSON request object, or is nested deeper than 32 levels
session_required401No signed-in person; sign in first
scope_denied403A secret key called the route. The route is for the browser; a server calls the provider directly
provider_unreachable502The provider did not answer before the first byte. Nothing was charged; the credit is refunded. Retry
provider_errorthe provider'sThrown by g.ai.text and g.ai.runText: the provider's own non-2xx, its message in err.message. g.ai.chat and g.ai.run return it as it came
ai_test_capped429The AI tools page's test call; one a minute per app

Both rails

gemmein dev answers the route without a key: a fake provider echoes a stream, marked x-gemmein-ai: fake, and the tool's credits are spent from the local ledger. A fresh person's balance is zero, and the route refuses at zero on both rails, so the loop starts with a credit arriving. Locally that is the pay simulator: declare a product with “Grants credits” in gemmein/payments.json (npx gemmein payments setup asks for it), call g.payments.buy("credits pack"), and open the URL it returns — the simulated payment travels the real signed-webhook path and the credits land on that person's local ledger. A relay's grant_credits is the other local road, for a receiver you have wired. In the cloud the two roads are the same purchase, and a comp the owner adds by hand on that person's page under Customers; there is no comp on the local rail, because the console is not part of it. Set GEMMEIN_AI_KEY_OPENAI, GEMMEIN_AI_KEY_ANTHROPIC or GEMMEIN_AI_KEY_GOOGLE in the local rail's environment for a real call. Keys never sync; the cloud app holds its own, pasted on the AI tools page. Tools do sync: a file at gemmein/ai/tools/<name>.json hot-reloads like a relay, and the boot card prints a line per tool — AI tools: deep-research (20 credits · requires access:pro-max · openai).

Two facts for the reference

  • The key is write-only. No response, no audit row and no page shows more than its last four characters.
  • The Usage & billing page counts AI calls for the last 30 days: spent by your customers' credits, priced by your provider. Gemmein meters the calls; your provider bills the tokens.