@gemmein/sdk 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/REFERENCE.md +90 -37
- package/dist/index.cjs +20 -10
- package/dist/index.d.cts +25 -11
- package/dist/index.d.ts +25 -11
- package/dist/index.js +20 -10
- package/llms.txt +173 -63
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,49 @@ the same release.
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.8.0] — 2026-09-05
|
|
16
|
+
|
|
17
|
+
### Products are provider-neutral (engine 0.9.0, sdk 0.8.0, mcp 0.5.0)
|
|
18
|
+
|
|
19
|
+
A product is sold one of three ways, set on the Payments page ("How it's
|
|
20
|
+
sold" in the console, `sold` in the CLI): via a Stripe Payment Link, via a
|
|
21
|
+
relay (any provider whose webhook the founder maps — GoCardless, Lemon
|
|
22
|
+
Squeezy, Paddle, bank transfer), or not yet (defined, its grants and
|
|
23
|
+
credits known, no road wired). Two new relay actions carry the relay
|
|
24
|
+
road: `fulfil_product { product, ref? }` grants the product's key and
|
|
25
|
+
credits to the event's person and writes the receipt, exactly as a
|
|
26
|
+
Stripe purchase does; `refund_product { product, ref? }` takes them
|
|
27
|
+
back. Both are idempotent on `ref`. `g.payments.buy` on a product sold
|
|
28
|
+
via a relay or not yet answers 409 `product_not_sellable`; saving a
|
|
29
|
+
relay action or a product's relay road against a relay that does not
|
|
30
|
+
exist in this environment answers 400 `relay_missing`.
|
|
31
|
+
|
|
32
|
+
### The AI tool primitive (engine 0.9.0, sdk 0.8.0, mcp 0.5.0)
|
|
33
|
+
|
|
34
|
+
The AI route now runs named, priced tools instead of one flat rate. An
|
|
35
|
+
owner creates an `ai_tools` row (console Tools table, or `PUT
|
|
36
|
+
/internal/ai/tools`; the CLI's `gemmein ai tools` lists, sets and removes
|
|
37
|
+
them, and the local rail reads `gemmein/ai/tools/<name>.json`) — a slug,
|
|
38
|
+
a label, a provider, an optional pinned model, a credit price (1 to
|
|
39
|
+
10,000), an optional `requires` entitlement key, and optional bounds
|
|
40
|
+
(`maxBodyBytes`, `maxOutputTokens`, `stream`). `g.ai.chat` and `g.ai.text`
|
|
41
|
+
take a new `tool` option naming one; a call with no `tool` runs as the
|
|
42
|
+
**default tool** — one credit, the app's configured provider, any allowed
|
|
43
|
+
model — so every existing integration keeps working unchanged. Up to 50
|
|
44
|
+
tools per environment.
|
|
45
|
+
|
|
46
|
+
New codes: `unknown_tool` (404 — no tool by that name), `tool_disabled`
|
|
47
|
+
(403 — the owner switched it off), `entitlement_required` (403 — the
|
|
48
|
+
message names the plan or product the tool requires), `model_pinned`
|
|
49
|
+
(403 — the tool's model is fixed; leave `model` out of the body),
|
|
50
|
+
`provider_not_configured` (409 — creating a tool on a provider with no
|
|
51
|
+
key set), `too_many_tools` (409 — 50 tools per environment) and
|
|
52
|
+
`invalid_tool` (400 — a bad field creating or updating a tool).
|
|
53
|
+
|
|
54
|
+
A response from a named tool now carries `x-gemmein-tool` — the tool's slug —
|
|
55
|
+
alongside `x-gemmein-credits-remaining`. `x-gemmein-tool` names the tool;
|
|
56
|
+
absent on the implicit default.
|
|
57
|
+
|
|
15
58
|
## [0.7.0] — 2026-09-04
|
|
16
59
|
|
|
17
60
|
### Credits and the AI route
|
package/REFERENCE.md
CHANGED
|
@@ -238,6 +238,16 @@ Plans are `g.subscriptions`; one-off things are `g.payments`. `checkout` and
|
|
|
238
238
|
don't also redirect to the returned `url`, and never build a Stripe URL
|
|
239
239
|
yourself. Gate features on `(await g.subscriptions.mine())?.plan === "pro"`;
|
|
240
240
|
gate one-off fulfilment on the receipt record, never the redirect.
|
|
241
|
+
|
|
242
|
+
A product is sold one of three ways, set on the Payments page: **via a
|
|
243
|
+
Stripe Payment Link** (`buy` navigates there); **via a relay** — any
|
|
244
|
+
provider whose webhook the founder maps (GoCardless, Lemon Squeezy,
|
|
245
|
+
Paddle, bank transfer) fulfils it with the `fulfil_product` relay
|
|
246
|
+
action, and refunds it with `refund_product`; or **not yet** — the
|
|
247
|
+
product is defined, its grants and credits are known, no road wired.
|
|
248
|
+
`buy` on a product sold via a relay or not yet answers 409
|
|
249
|
+
`product_not_sellable`. The price always comes from the provider — the
|
|
250
|
+
Payment Link or the relay — never the app.
|
|
241
251
|
By-hand grants (trial, promotion, a support comp) are not listed to the
|
|
242
252
|
app — a gated read simply succeeds — so never rebuild the paywall from
|
|
243
253
|
`mine()`; let the server refuse with `entitlement_required`. The two grant
|
|
@@ -274,7 +284,7 @@ honestly, not hidden.)
|
|
|
274
284
|
## Credits — `g.credits` / `gemmeinServer(sk).spendCredits`
|
|
275
285
|
|
|
276
286
|
A balance your customers hold and your product spends: a pack they buy, a
|
|
277
|
-
comp the owner gives,
|
|
287
|
+
comp the owner gives, an AI tool call priced in credits. Credits are a **quantity beside
|
|
278
288
|
access**, never access itself — a locked collection still asks for the
|
|
279
289
|
entitlement, whatever the balance. The ledger adds at every purchase, grant
|
|
280
290
|
and refund, subtracts at every spend, and never goes below zero: a spend
|
|
@@ -302,8 +312,9 @@ Where credits come from, and where they go:
|
|
|
302
312
|
ledger lines and the credits the person spent in the last 30 days.
|
|
303
313
|
- **A relay** adds with `grant_credits { amount (1..10,000), reason? }` — see
|
|
304
314
|
**Relays**.
|
|
305
|
-
- **Your server** spends with `spendCredits`; **the AI route** spends
|
|
306
|
-
|
|
315
|
+
- **Your server** spends with `spendCredits`; **the AI route** spends the
|
|
316
|
+
credits the owner set for the named tool, one by default (see **AI**).
|
|
317
|
+
Nothing spends from the browser.
|
|
307
318
|
- Spend lines are kept 90 days on the cloud rail; purchases, grants and
|
|
308
319
|
clawbacks are kept. A balance carries at most 1,000,000,000. Account
|
|
309
320
|
erasure removes the person's balance and ledger.
|
|
@@ -345,22 +356,43 @@ connected.
|
|
|
345
356
|
## AI — `g.ai.chat` / `g.ai.text`
|
|
346
357
|
|
|
347
358
|
Your app talks to **OpenAI, Anthropic or Google** through Gemmein, on the
|
|
348
|
-
**owner's own provider key**, which never reaches the browser
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
359
|
+
**owner's own provider key**, which never reaches the browser, through a
|
|
360
|
+
**named AI tool** — a slug the owner prices in credits, pins a model or
|
|
361
|
+
provider to, and gates behind an entitlement, from the dashboard's Tools
|
|
362
|
+
page or a local `gemmein/ai/tools/<name>.json` file. Removing a tool is a
|
|
363
|
+
step-up action, like removing a key. A call that names no
|
|
364
|
+
`tool` runs as the **default tool**: one credit, the app's configured
|
|
365
|
+
provider, any allowed model — every existing integration that never passed
|
|
366
|
+
`tool` keeps working unchanged. The owner pastes each provider key once in
|
|
367
|
+
the dashboard's Keys room; it is write-only from then on. The route
|
|
368
|
+
forwards the provider's own request body as sent — minus the `provider`
|
|
369
|
+
field, and for Google minus `model` and `stream`, which ride the URL — adds
|
|
370
|
+
the provider's auth headers, `content-type` and `accept`, and passes the
|
|
371
|
+
status and the bytes straight back — a stream stays a stream. It does not
|
|
372
|
+
choose models, cache, summarise, moderate or reshape anything, and it is for
|
|
373
|
+
the browser only: a server key is refused (`403 scope_denied`) — a server
|
|
374
|
+
calls the provider directly.
|
|
358
375
|
|
|
359
376
|
| Method | Signature | Returns |
|
|
360
377
|
|--------|-----------|---------|
|
|
361
|
-
| `ai.chat` | `(body: object, options?: { provider?: "openai" \| "anthropic" \| "google"; signal?: AbortSignal })` | `Promise<Response>` — the fetch `Response`, untouched: the provider's status, headers and body, streaming intact. A non-2xx from the provider is returned as-is (not thrown) — an answer carrying `x-gemmein-credits-remaining` passed the spend and is the provider's; a Gemmein refusal throws `GemmeinError` |
|
|
362
|
-
| `ai.text` | `(body: object, options
|
|
363
|
-
|
|
378
|
+
| `ai.chat` | `(body: object, options?: { tool?: string; provider?: "openai" \| "anthropic" \| "google"; signal?: AbortSignal })` | `Promise<Response>` — the fetch `Response`, untouched: the provider's status, headers and body, streaming intact. A non-2xx from the provider is returned as-is (not thrown) — an answer carrying `x-gemmein-credits-remaining` passed the spend and is the provider's; a Gemmein refusal throws `GemmeinError` |
|
|
379
|
+
| `ai.text` | `(body: object, options?: { tool?: string; provider?: …; signal?: AbortSignal })` | `Promise<string>` — a non-stream call collected to one string, whichever provider answered (openai `choices[0].message.content`; anthropic `content[].text` joined; google `candidates[0].content.parts[].text` joined); a provider's non-2xx throws `provider_error` with the provider's status and message |
|
|
380
|
+
|
|
381
|
+
- `tool` names an `ai_tools` row by its slug; omit it for the default tool.
|
|
382
|
+
An unknown name is `404 unknown_tool`; a tool the owner switched off is
|
|
383
|
+
`403 tool_disabled`; a tool gated with `requires` refuses a person who
|
|
384
|
+
lacks that entitlement with `403 entitlement_required` (the message names
|
|
385
|
+
the plan or product). A tool with a pinned `model` refuses a body that
|
|
386
|
+
sets one with `403 model_pinned`; a tool with no pinned model follows the
|
|
387
|
+
allowlist rule below. A tool's own `bounds.maxBodyBytes` (≤ 262,144)
|
|
388
|
+
replaces the default 256 KB cap. `bounds.maxOutputTokens` is a ceiling: a
|
|
389
|
+
larger or absent value in the body becomes the bound; a smaller one stays.
|
|
390
|
+
It reaches the provider as ONE field, never two: the ceiling is written
|
|
391
|
+
onto the field the body already used, and a second spelling of the same
|
|
392
|
+
ceiling is folded away, so a browser cannot buy more output by naming the
|
|
393
|
+
field the route was not watching. A body that names none is sent the
|
|
394
|
+
current field — `max_completion_tokens` on OpenAI, `max_tokens` on
|
|
395
|
+
Anthropic, `generationConfig.maxOutputTokens` on Google.
|
|
364
396
|
- `body` is exactly what the provider documents for its chat endpoint —
|
|
365
397
|
OpenAI chat completions, Anthropic messages, Google generateContent. Its
|
|
366
398
|
`model` field, when present, must match `^[A-Za-z0-9._:-]{1,80}$`; Google
|
|
@@ -368,19 +400,24 @@ refused (`403 scope_denied`) — a server calls the provider directly.
|
|
|
368
400
|
20 — the Keys room's test call uses the first), any other answers
|
|
369
401
|
`403 model_not_allowed`.
|
|
370
402
|
- `provider` is optional when one key is configured and required when more
|
|
371
|
-
than one is (`400 provider_required`)
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
403
|
+
than one is (`400 provider_required`); a named tool fixes its own
|
|
404
|
+
provider, and a `provider` in the body or `?provider=` that disagrees with
|
|
405
|
+
it is `400 invalid_body`. No key at all is `409 ai_not_configured`.
|
|
406
|
+
`?provider=` and `?stream=1` on the URL do what the body fields do.
|
|
407
|
+
- Response headers from Gemmein. `x-gemmein-credits-remaining` (the balance
|
|
408
|
+
after this call) rides every answer that passed the spend.
|
|
409
|
+
`x-gemmein-credit` reads `refunded`, and is there only when the credits
|
|
410
|
+
came back. `x-gemmein-ai` reads `fake`, and is there only on the local
|
|
411
|
+
engine's rehearsal answer. `x-gemmein-tool` names the tool; absent on the
|
|
412
|
+
implicit default.
|
|
413
|
+
- **The refund rule.** The tool's credits are refunded only when the
|
|
414
|
+
provider fails before its first byte (a non-2xx, or
|
|
415
|
+
`502 provider_unreachable`). A stream that dies after the first byte is
|
|
416
|
+
not refunded; hanging up early does not refund. A provider that echoes
|
|
417
|
+
the key in a refusal reaches the app as `***<hint>`.
|
|
382
418
|
- Limits: 20 calls per person per minute (`429 ai_capped`, `resetAt`), 256 KB
|
|
383
|
-
body (`413 payload_too_large
|
|
419
|
+
body by default (`413 payload_too_large`, unless a tool sets a smaller
|
|
420
|
+
`bounds.maxBodyBytes`) nested at most 32 levels (`400 invalid_body`),
|
|
384
421
|
170 s in all and, on a stream, 10 s to the first response headers. Every
|
|
385
422
|
`/ai/chat` call counts toward the app's `api_requests` band like any other
|
|
386
423
|
request.
|
|
@@ -389,24 +426,31 @@ refused (`403 scope_denied`) — a server calls the provider directly.
|
|
|
389
426
|
const res = await g.ai.chat({
|
|
390
427
|
model: "gpt-4o-mini", stream: true,
|
|
391
428
|
messages: [{ role: "user", content: text }]
|
|
392
|
-
}, {
|
|
429
|
+
}, { tool: "deep-research" })
|
|
393
430
|
for await (const chunk of res.body) render(chunk) // the provider's SSE, byte for byte
|
|
394
431
|
|
|
395
|
-
const answer = await g.ai.text({
|
|
432
|
+
const answer = await g.ai.text({ messages: [{ role: "user", content: text }] }, { tool: "deep-research" })
|
|
396
433
|
```
|
|
397
434
|
|
|
398
435
|
| code | status | meaning · do |
|
|
399
436
|
|------|--------|--------------|
|
|
400
|
-
| `
|
|
437
|
+
| `unknown_tool` | 404 | No AI tool by this name in this environment — check the slug or the environment |
|
|
438
|
+
| `tool_disabled` | 403 | The owner switched this tool off — turn it on, or use another |
|
|
439
|
+
| `entitlement_required` | 403 | The tool's `requires` names a plan or product this person lacks — the message names it |
|
|
440
|
+
| `model_pinned` | 403 | This tool's model is fixed — leave `model` out of the body |
|
|
441
|
+
| `provider_not_configured` | 409 | Creating or updating a tool: no key is set for that provider yet — add one on the Keys page first |
|
|
442
|
+
| `too_many_tools` | 409 | This environment already holds 50 AI tools — delete one before adding another |
|
|
443
|
+
| `invalid_tool` | 400 | Creating or updating a tool with a bad field — the message names which one and its rule |
|
|
444
|
+
| `credits_exhausted` | 402 | The person's balance is below the tool's price — the message names the tool, the price and the balance ("Deep Research costs 20 credits. You have 7.", singular for 1). Show the pack |
|
|
401
445
|
| `ai_not_configured` | 409 | No provider key on this app and environment — the owner pastes one in the Keys room |
|
|
402
446
|
| `provider_required` | 400 | More than one provider key is set — pass `provider` |
|
|
403
|
-
| `model_not_allowed` | 403 | The owner's allowlist names the models this app may call; the message lists them |
|
|
447
|
+
| `model_not_allowed` | 403 | The owner's allowlist names the models this app may call; the message lists them (only for a tool with no pinned model) |
|
|
404
448
|
| `ai_capped` | 429 | 20 calls per person per minute — wait for `resetAt` |
|
|
405
|
-
| `payload_too_large` | 413 | The body is over 256 KB — shorten the conversation you send |
|
|
406
|
-
| `invalid_body` | 400 | The body must be the provider's JSON request object, nested at most 32 levels |
|
|
449
|
+
| `payload_too_large` | 413 | The body is over the cap (256 KB, or the tool's own `bounds.maxBodyBytes`) — shorten the conversation you send |
|
|
450
|
+
| `invalid_body` | 400 | The body must be the provider's JSON request object, nested at most 32 levels; or a named tool's own provider was contradicted |
|
|
407
451
|
| `session_required` | 401 | No signed-in person — sign in first |
|
|
408
452
|
| `scope_denied` | 403 | A secret key called the route — the route is for the browser; a server calls the provider directly |
|
|
409
|
-
| `provider_unreachable` | 502 | The provider did not answer before the first byte — nothing was charged (the
|
|
453
|
+
| `provider_unreachable` | 502 | The provider did not answer before the first byte — nothing was charged (the tool's credits are refunded); retry |
|
|
410
454
|
| `provider_error` | the provider's | `g.ai.text` only (client-side): the provider's own non-2xx, its message in `err.message` |
|
|
411
455
|
| `ai_test_capped` | 429 | The Keys room's test call — one a minute per app |
|
|
412
456
|
|
|
@@ -627,6 +671,8 @@ authorise on fields they cannot set, or from a receiver.
|
|
|
627
671
|
| `grant_access` | `entitlement` (plan or product **name**, or `access:<slug>`), `expiresAt?` (`"30d"`, `"12h"`, `"2w"` or an ISO date), `reason?` (≤ 200, templated) | source `relay`, the seventh grant source; skipped when this event already granted |
|
|
628
672
|
| `grant_credits` | `amount` (1..10,000), `reason?` (≤ 200, templated) | adds to the person's credit balance, once per event (a replay finds its own row); the ledger line reads "relay: <reason>"; a relay adds and never spends |
|
|
629
673
|
| `revoke_access` | `entitlement` | ends every live grant of that entitlement the person holds; skipped when none |
|
|
674
|
+
| `fulfil_product` | `product` (name, 1-40 chars), `ref?` (≤ 200, templated) | grants the product's key and credits to the event's person and writes the receipt, exactly as a Stripe purchase does; idempotent on `ref` (an absent `ref` → `rly:<eventId>`; a `ref` you wrote that names nothing on the event is refused, never replaced); a ref must be the provider's unique payment identifier; a ref already used by another purchase is refused and audited; the product's road must be this relay. The relay road binds by name: renaming or deleting the relay stops fulfilment until a relay with that name exists again; the product card shows it. |
|
|
675
|
+
| `refund_product` | `product` (name, 1-40 chars), `ref?` (≤ 200, templated) | takes back what `fulfil_product` under the same `ref` granted; `refund_product` may run from any relay in the environment; it refunds only a purchase a relay fulfilled, for the event's person and the product it names; idempotent on `ref` |
|
|
630
676
|
| `email_person` | `subject` (≤ 300), `text` (≤ 10,000), `kind?: "event" \| "account"` | rides `notify()`'s caps (200 per app per hour, 5 event sends per person per day) and the owner's sends switch; deduped per event |
|
|
631
677
|
| `call_url` | `url` | https only, no template in the URL, no IP literal, never a gemmein.com host, no credentials in the URL; `gemmein dev` allows http to localhost |
|
|
632
678
|
|
|
@@ -665,11 +711,16 @@ Egress refusals arrive as the action's error, naming the rule: `https_only`,
|
|
|
665
711
|
### Templates
|
|
666
712
|
|
|
667
713
|
In string values only — `write_record.data`, `email_person.subject` / `text`,
|
|
668
|
-
`grant_access.reason
|
|
714
|
+
`grant_access.reason`, `fulfil_product.ref` / `refund_product.ref`; never in
|
|
715
|
+
`call_url.url` or an `entitlement`:
|
|
669
716
|
`{{event.a.b}}` (the raw body), `{{mapped.x}}`, `{{record.data.x}}`,
|
|
670
717
|
`{{record.id}}`, `{{person.email}}`, `{{person.id}}`, `{{tick.periodStart}}`.
|
|
718
|
+
Mapped fields ride into templates as `{{mapped.<name>}}`; `{{event.<path>}}`
|
|
719
|
+
reads the raw payload.
|
|
671
720
|
A path that names nothing renders empty and adds a warning to the event;
|
|
672
|
-
objects render as JSON. No expressions, no filters.
|
|
721
|
+
objects render as JSON. No expressions, no filters. The one place an empty
|
|
722
|
+
render is a refusal rather than a blank is a `ref` you wrote on
|
|
723
|
+
`fulfil_product` / `refund_product`.
|
|
673
724
|
|
|
674
725
|
### Lifecycle
|
|
675
726
|
|
|
@@ -823,6 +874,8 @@ Branch on `err.code`. The gate's own codes (`session_invalid`,
|
|
|
823
874
|
| `invalid_response` (client-side, status 0) | the server answered 200 to `verifyEmailCode` without a session token — a proxy or mock in the path, not Gemmein | check `apiUrl` and anything rewriting responses; the call is safe to retry |
|
|
824
875
|
| `authentication_required` (401) | checkout/subscription/pay without a signed-in user | sign the user in first |
|
|
825
876
|
| `plan_has_no_link` (409) | the paid plan has no Payment Link pasted yet | ask the owner to paste it in their dashboard |
|
|
877
|
+
| `product_not_sellable` (409) | `g.payments.buy` on a product sold via a relay or not yet — there is no Payment Link to open | tell the buyer how the product is sold, or ask the owner to wire a road |
|
|
878
|
+
| `relay_missing` (400) | saving a relay action (or a product's `sold.relay`) names a relay that does not exist in this environment | create that relay first, or name one that exists |
|
|
826
879
|
| `account_suspended` (403) | the app owner's account is suspended (billing) | the owner fixes payment at app.gemmein.com |
|
|
827
880
|
| `credits_exhausted` (402) | the person's balance is below the spend — the message carries the balance ("this person has {balance} credits — the spend needs {amount}") | show the pack; never retry the same spend |
|
|
828
881
|
| `ai_not_configured` (409) | no provider key on this app and environment | the owner pastes one in the Keys room |
|
package/dist/index.cjs
CHANGED
|
@@ -9,7 +9,7 @@ exports.gemmeinServer = gemmeinServer;
|
|
|
9
9
|
* second module). `scripts/sync-version.mjs` rewrites the literal from
|
|
10
10
|
* package.json before every build (`prebuild`), and a test pins the two
|
|
11
11
|
* equal, so a bump can never ship with a stale header. */
|
|
12
|
-
exports.SDK_VERSION = "0.
|
|
12
|
+
exports.SDK_VERSION = "0.8.0"; // synced from package.json — do not edit by hand
|
|
13
13
|
/** W9.1 / CLIENT-INFO-1: every request the SDK makes to Gemmein carries
|
|
14
14
|
* `x-client-info: gemmein-sdk/<version>`. The server records it on the
|
|
15
15
|
* secret-key usage ledger ("last seen from gemmein-sdk/0.5.0"), so a
|
|
@@ -394,8 +394,10 @@ exports.CreditsClient = CreditsClient;
|
|
|
394
394
|
* The AI route. `chat` takes the provider's own request body — exactly what
|
|
395
395
|
* you would POST to OpenAI's /v1/chat/completions, Anthropic's /v1/messages
|
|
396
396
|
* or Google's generateContent — and answers with the fetch `Response`
|
|
397
|
-
* untouched, streaming intact (SSE stays SSE). Gemmein spends
|
|
398
|
-
* adds the owner's key, forwards, and passes status and bytes back.
|
|
397
|
+
* untouched, streaming intact (SSE stays SSE). Gemmein spends a credit,
|
|
398
|
+
* adds the owner's key, forwards, and passes status and bytes back. Pass
|
|
399
|
+
* `tool` (W9.3b) to run a named, owner-priced-and-gated operation instead
|
|
400
|
+
* of the implicit default (one credit, any allowed model, no gate).
|
|
399
401
|
* Response headers: `x-gemmein-credits-remaining` on every answer that
|
|
400
402
|
* passed the spend; `x-gemmein-credit: refunded` when the provider failed
|
|
401
403
|
* before its first byte.
|
|
@@ -416,16 +418,24 @@ class AiClient {
|
|
|
416
418
|
* `model_not_allowed` (403 — the owner's models list) · `ai_capped`
|
|
417
419
|
* (429 — 20 calls a minute per person; `err.resetAt`) ·
|
|
418
420
|
* `payload_too_large` (413 — 256 KB) · `provider_unreachable` (502,
|
|
419
|
-
* before the first byte, refunded).
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
* is
|
|
421
|
+
* before the first byte, refunded). W9.3b, `tool` only: `unknown_tool`
|
|
422
|
+
* (404 — no tool by that name in this environment) · `tool_disabled`
|
|
423
|
+
* (403) · `entitlement_required` (403 — the message names the plan or
|
|
424
|
+
* product that unlocks it) · `model_pinned` (403 — the tool's model is
|
|
425
|
+
* fixed; leave `model` out of the body). Those are GEMMEIN's refusals.
|
|
426
|
+
* The PROVIDER's own answer — 2xx or not — is returned as it came: an
|
|
427
|
+
* answer that carries `x-gemmein-credits-remaining` passed the spend, so
|
|
428
|
+
* its status and body are the provider's; read `res.ok` / `res.status`
|
|
429
|
+
* yourself (a provider 4xx before the first byte is refunded, header
|
|
430
|
+
* `x-gemmein-credit: refunded`). `x-gemmein-tool` names the tool; absent
|
|
431
|
+
* on the implicit default.
|
|
425
432
|
*/
|
|
426
433
|
async chat(body, options = {}) {
|
|
427
434
|
const payload = options.provider ? { provider: options.provider, ...body } : body;
|
|
428
|
-
const
|
|
435
|
+
const url = new URL("/ai/chat", this.config.apiUrl);
|
|
436
|
+
if (options.tool)
|
|
437
|
+
url.searchParams.set("tool", options.tool);
|
|
438
|
+
const response = await fetch(url, {
|
|
429
439
|
method: "POST",
|
|
430
440
|
body: JSON.stringify(payload),
|
|
431
441
|
headers: await runtimeHeaders(this.config, { "content-type": "application/json" }),
|
package/dist/index.d.cts
CHANGED
|
@@ -120,13 +120,13 @@ export type AuthSession = {
|
|
|
120
120
|
* second module). `scripts/sync-version.mjs` rewrites the literal from
|
|
121
121
|
* package.json before every build (`prebuild`), and a test pins the two
|
|
122
122
|
* equal, so a bump can never ship with a stale header. */
|
|
123
|
-
export declare const SDK_VERSION = "0.
|
|
123
|
+
export declare const SDK_VERSION = "0.8.0";
|
|
124
124
|
/** W9.1 / CLIENT-INFO-1: every request the SDK makes to Gemmein carries
|
|
125
125
|
* `x-client-info: gemmein-sdk/<version>`. The server records it on the
|
|
126
126
|
* secret-key usage ledger ("last seen from gemmein-sdk/0.5.0"), so a
|
|
127
127
|
* misbehaving integration can be attributed to an SDK version from day
|
|
128
128
|
* one. It is a report, not a proof — any caller can set it. */
|
|
129
|
-
export declare const CLIENT_INFO = "gemmein-sdk/0.
|
|
129
|
+
export declare const CLIENT_INFO = "gemmein-sdk/0.8.0";
|
|
130
130
|
export declare class GemmeinError extends Error {
|
|
131
131
|
readonly status: number;
|
|
132
132
|
readonly code: string;
|
|
@@ -389,8 +389,15 @@ export declare class AccountClient {
|
|
|
389
389
|
export type AiProvider = "openai" | "anthropic" | "google";
|
|
390
390
|
export type AiChatOptions = {
|
|
391
391
|
/** Which configured provider answers. Optional when exactly one key is
|
|
392
|
-
* set; refused `provider_required` (400) when it is ambiguous.
|
|
392
|
+
* set; refused `provider_required` (400) when it is ambiguous. Refused
|
|
393
|
+
* `invalid_body` when `tool` is also set and disagrees with the named
|
|
394
|
+
* tool's own provider — leave `provider` out when you pass `tool`. */
|
|
393
395
|
provider?: AiProvider;
|
|
396
|
+
/** W9.3b: a named AI tool (owner-configured in the console — credits,
|
|
397
|
+
* gate and provider/model are the tool's, not this call's). Sent as
|
|
398
|
+
* `?tool=`, never in the body. Omitted → the implicit default tool: one
|
|
399
|
+
* credit, any allowed model, no gate. */
|
|
400
|
+
tool?: string;
|
|
394
401
|
/** Abort the call — the stream closes; a call that dies mid-stream is
|
|
395
402
|
* not refunded. */
|
|
396
403
|
signal?: AbortSignal;
|
|
@@ -429,8 +436,10 @@ export declare class CreditsClient {
|
|
|
429
436
|
* The AI route. `chat` takes the provider's own request body — exactly what
|
|
430
437
|
* you would POST to OpenAI's /v1/chat/completions, Anthropic's /v1/messages
|
|
431
438
|
* or Google's generateContent — and answers with the fetch `Response`
|
|
432
|
-
* untouched, streaming intact (SSE stays SSE). Gemmein spends
|
|
433
|
-
* adds the owner's key, forwards, and passes status and bytes back.
|
|
439
|
+
* untouched, streaming intact (SSE stays SSE). Gemmein spends a credit,
|
|
440
|
+
* adds the owner's key, forwards, and passes status and bytes back. Pass
|
|
441
|
+
* `tool` (W9.3b) to run a named, owner-priced-and-gated operation instead
|
|
442
|
+
* of the implicit default (one credit, any allowed model, no gate).
|
|
434
443
|
* Response headers: `x-gemmein-credits-remaining` on every answer that
|
|
435
444
|
* passed the spend; `x-gemmein-credit: refunded` when the provider failed
|
|
436
445
|
* before its first byte.
|
|
@@ -450,12 +459,17 @@ export declare class AiClient {
|
|
|
450
459
|
* `model_not_allowed` (403 — the owner's models list) · `ai_capped`
|
|
451
460
|
* (429 — 20 calls a minute per person; `err.resetAt`) ·
|
|
452
461
|
* `payload_too_large` (413 — 256 KB) · `provider_unreachable` (502,
|
|
453
|
-
* before the first byte, refunded).
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
* is
|
|
462
|
+
* before the first byte, refunded). W9.3b, `tool` only: `unknown_tool`
|
|
463
|
+
* (404 — no tool by that name in this environment) · `tool_disabled`
|
|
464
|
+
* (403) · `entitlement_required` (403 — the message names the plan or
|
|
465
|
+
* product that unlocks it) · `model_pinned` (403 — the tool's model is
|
|
466
|
+
* fixed; leave `model` out of the body). Those are GEMMEIN's refusals.
|
|
467
|
+
* The PROVIDER's own answer — 2xx or not — is returned as it came: an
|
|
468
|
+
* answer that carries `x-gemmein-credits-remaining` passed the spend, so
|
|
469
|
+
* its status and body are the provider's; read `res.ok` / `res.status`
|
|
470
|
+
* yourself (a provider 4xx before the first byte is refunded, header
|
|
471
|
+
* `x-gemmein-credit: refunded`). `x-gemmein-tool` names the tool; absent
|
|
472
|
+
* on the implicit default.
|
|
459
473
|
*/
|
|
460
474
|
chat(body: Record<string, unknown>, options?: AiChatOptions): Promise<Response>;
|
|
461
475
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -120,13 +120,13 @@ export type AuthSession = {
|
|
|
120
120
|
* second module). `scripts/sync-version.mjs` rewrites the literal from
|
|
121
121
|
* package.json before every build (`prebuild`), and a test pins the two
|
|
122
122
|
* equal, so a bump can never ship with a stale header. */
|
|
123
|
-
export declare const SDK_VERSION = "0.
|
|
123
|
+
export declare const SDK_VERSION = "0.8.0";
|
|
124
124
|
/** W9.1 / CLIENT-INFO-1: every request the SDK makes to Gemmein carries
|
|
125
125
|
* `x-client-info: gemmein-sdk/<version>`. The server records it on the
|
|
126
126
|
* secret-key usage ledger ("last seen from gemmein-sdk/0.5.0"), so a
|
|
127
127
|
* misbehaving integration can be attributed to an SDK version from day
|
|
128
128
|
* one. It is a report, not a proof — any caller can set it. */
|
|
129
|
-
export declare const CLIENT_INFO = "gemmein-sdk/0.
|
|
129
|
+
export declare const CLIENT_INFO = "gemmein-sdk/0.8.0";
|
|
130
130
|
export declare class GemmeinError extends Error {
|
|
131
131
|
readonly status: number;
|
|
132
132
|
readonly code: string;
|
|
@@ -389,8 +389,15 @@ export declare class AccountClient {
|
|
|
389
389
|
export type AiProvider = "openai" | "anthropic" | "google";
|
|
390
390
|
export type AiChatOptions = {
|
|
391
391
|
/** Which configured provider answers. Optional when exactly one key is
|
|
392
|
-
* set; refused `provider_required` (400) when it is ambiguous.
|
|
392
|
+
* set; refused `provider_required` (400) when it is ambiguous. Refused
|
|
393
|
+
* `invalid_body` when `tool` is also set and disagrees with the named
|
|
394
|
+
* tool's own provider — leave `provider` out when you pass `tool`. */
|
|
393
395
|
provider?: AiProvider;
|
|
396
|
+
/** W9.3b: a named AI tool (owner-configured in the console — credits,
|
|
397
|
+
* gate and provider/model are the tool's, not this call's). Sent as
|
|
398
|
+
* `?tool=`, never in the body. Omitted → the implicit default tool: one
|
|
399
|
+
* credit, any allowed model, no gate. */
|
|
400
|
+
tool?: string;
|
|
394
401
|
/** Abort the call — the stream closes; a call that dies mid-stream is
|
|
395
402
|
* not refunded. */
|
|
396
403
|
signal?: AbortSignal;
|
|
@@ -429,8 +436,10 @@ export declare class CreditsClient {
|
|
|
429
436
|
* The AI route. `chat` takes the provider's own request body — exactly what
|
|
430
437
|
* you would POST to OpenAI's /v1/chat/completions, Anthropic's /v1/messages
|
|
431
438
|
* or Google's generateContent — and answers with the fetch `Response`
|
|
432
|
-
* untouched, streaming intact (SSE stays SSE). Gemmein spends
|
|
433
|
-
* adds the owner's key, forwards, and passes status and bytes back.
|
|
439
|
+
* untouched, streaming intact (SSE stays SSE). Gemmein spends a credit,
|
|
440
|
+
* adds the owner's key, forwards, and passes status and bytes back. Pass
|
|
441
|
+
* `tool` (W9.3b) to run a named, owner-priced-and-gated operation instead
|
|
442
|
+
* of the implicit default (one credit, any allowed model, no gate).
|
|
434
443
|
* Response headers: `x-gemmein-credits-remaining` on every answer that
|
|
435
444
|
* passed the spend; `x-gemmein-credit: refunded` when the provider failed
|
|
436
445
|
* before its first byte.
|
|
@@ -450,12 +459,17 @@ export declare class AiClient {
|
|
|
450
459
|
* `model_not_allowed` (403 — the owner's models list) · `ai_capped`
|
|
451
460
|
* (429 — 20 calls a minute per person; `err.resetAt`) ·
|
|
452
461
|
* `payload_too_large` (413 — 256 KB) · `provider_unreachable` (502,
|
|
453
|
-
* before the first byte, refunded).
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
* is
|
|
462
|
+
* before the first byte, refunded). W9.3b, `tool` only: `unknown_tool`
|
|
463
|
+
* (404 — no tool by that name in this environment) · `tool_disabled`
|
|
464
|
+
* (403) · `entitlement_required` (403 — the message names the plan or
|
|
465
|
+
* product that unlocks it) · `model_pinned` (403 — the tool's model is
|
|
466
|
+
* fixed; leave `model` out of the body). Those are GEMMEIN's refusals.
|
|
467
|
+
* The PROVIDER's own answer — 2xx or not — is returned as it came: an
|
|
468
|
+
* answer that carries `x-gemmein-credits-remaining` passed the spend, so
|
|
469
|
+
* its status and body are the provider's; read `res.ok` / `res.status`
|
|
470
|
+
* yourself (a provider 4xx before the first byte is refunded, header
|
|
471
|
+
* `x-gemmein-credit: refunded`). `x-gemmein-tool` names the tool; absent
|
|
472
|
+
* on the implicit default.
|
|
459
473
|
*/
|
|
460
474
|
chat(body: Record<string, unknown>, options?: AiChatOptions): Promise<Response>;
|
|
461
475
|
/**
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* second module). `scripts/sync-version.mjs` rewrites the literal from
|
|
5
5
|
* package.json before every build (`prebuild`), and a test pins the two
|
|
6
6
|
* equal, so a bump can never ship with a stale header. */
|
|
7
|
-
export const SDK_VERSION = "0.
|
|
7
|
+
export const SDK_VERSION = "0.8.0"; // synced from package.json — do not edit by hand
|
|
8
8
|
/** W9.1 / CLIENT-INFO-1: every request the SDK makes to Gemmein carries
|
|
9
9
|
* `x-client-info: gemmein-sdk/<version>`. The server records it on the
|
|
10
10
|
* secret-key usage ledger ("last seen from gemmein-sdk/0.5.0"), so a
|
|
@@ -378,8 +378,10 @@ export class CreditsClient {
|
|
|
378
378
|
* The AI route. `chat` takes the provider's own request body — exactly what
|
|
379
379
|
* you would POST to OpenAI's /v1/chat/completions, Anthropic's /v1/messages
|
|
380
380
|
* or Google's generateContent — and answers with the fetch `Response`
|
|
381
|
-
* untouched, streaming intact (SSE stays SSE). Gemmein spends
|
|
382
|
-
* adds the owner's key, forwards, and passes status and bytes back.
|
|
381
|
+
* untouched, streaming intact (SSE stays SSE). Gemmein spends a credit,
|
|
382
|
+
* adds the owner's key, forwards, and passes status and bytes back. Pass
|
|
383
|
+
* `tool` (W9.3b) to run a named, owner-priced-and-gated operation instead
|
|
384
|
+
* of the implicit default (one credit, any allowed model, no gate).
|
|
383
385
|
* Response headers: `x-gemmein-credits-remaining` on every answer that
|
|
384
386
|
* passed the spend; `x-gemmein-credit: refunded` when the provider failed
|
|
385
387
|
* before its first byte.
|
|
@@ -400,16 +402,24 @@ export class AiClient {
|
|
|
400
402
|
* `model_not_allowed` (403 — the owner's models list) · `ai_capped`
|
|
401
403
|
* (429 — 20 calls a minute per person; `err.resetAt`) ·
|
|
402
404
|
* `payload_too_large` (413 — 256 KB) · `provider_unreachable` (502,
|
|
403
|
-
* before the first byte, refunded).
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
* is
|
|
405
|
+
* before the first byte, refunded). W9.3b, `tool` only: `unknown_tool`
|
|
406
|
+
* (404 — no tool by that name in this environment) · `tool_disabled`
|
|
407
|
+
* (403) · `entitlement_required` (403 — the message names the plan or
|
|
408
|
+
* product that unlocks it) · `model_pinned` (403 — the tool's model is
|
|
409
|
+
* fixed; leave `model` out of the body). Those are GEMMEIN's refusals.
|
|
410
|
+
* The PROVIDER's own answer — 2xx or not — is returned as it came: an
|
|
411
|
+
* answer that carries `x-gemmein-credits-remaining` passed the spend, so
|
|
412
|
+
* its status and body are the provider's; read `res.ok` / `res.status`
|
|
413
|
+
* yourself (a provider 4xx before the first byte is refunded, header
|
|
414
|
+
* `x-gemmein-credit: refunded`). `x-gemmein-tool` names the tool; absent
|
|
415
|
+
* on the implicit default.
|
|
409
416
|
*/
|
|
410
417
|
async chat(body, options = {}) {
|
|
411
418
|
const payload = options.provider ? { provider: options.provider, ...body } : body;
|
|
412
|
-
const
|
|
419
|
+
const url = new URL("/ai/chat", this.config.apiUrl);
|
|
420
|
+
if (options.tool)
|
|
421
|
+
url.searchParams.set("tool", options.tool);
|
|
422
|
+
const response = await fetch(url, {
|
|
413
423
|
method: "POST",
|
|
414
424
|
body: JSON.stringify(payload),
|
|
415
425
|
headers: await runtimeHeaders(this.config, { "content-type": "application/json" }),
|
package/llms.txt
CHANGED
|
@@ -41,7 +41,8 @@ creating no accounts is the correct behavior until the verdict is in.
|
|
|
41
41
|
built in; any provider that signs its webhooks drives access through a
|
|
42
42
|
relay), consumable credits (buy a pack, spend atomically, a zero
|
|
43
43
|
floor — see Credits), a chat call to OpenAI, Anthropic or Google on
|
|
44
|
-
the founder's own key at
|
|
44
|
+
the founder's own key, at the credits the owner set for that tool
|
|
45
|
+
(one by default — see The AI route),
|
|
45
46
|
image uploads and paid file delivery, entitlement-gated
|
|
46
47
|
access, race-safe bookings/stock/counters, account-deletion cascade.
|
|
47
48
|
Out of scope — each of these downgrades the verdict and must never be
|
|
@@ -56,7 +57,8 @@ creating no accounts is the correct behavior until the verdict is in.
|
|
|
56
57
|
- usage-based INVOICING (a bill after the fact, by token or by minute):
|
|
57
58
|
not shipped. Credits ARE — consumable credits: buy a pack, spend
|
|
58
59
|
atomically, a zero floor — see Credits; a "100 credit pack" is a
|
|
59
|
-
product with Grants credits = 100, and
|
|
60
|
+
product with Grants credits = 100, and an AI tool call costs what
|
|
61
|
+
the owner priced that tool at (one credit by default)
|
|
60
62
|
- seats (owner-run: not coming)
|
|
61
63
|
- hosted functions or cron: Gemmein runs no code of yours — but your
|
|
62
64
|
own function anywhere asks Gemmein who a person is and what they
|
|
@@ -448,8 +450,8 @@ contents.
|
|
|
448
450
|
Stripe stays built in; any provider that signs its webhooks drives
|
|
449
451
|
access the same way.
|
|
450
452
|
Does: write a record · grant access · revoke access · grant credits ·
|
|
451
|
-
email the person the event is about · call
|
|
452
|
-
a server YOU run).
|
|
453
|
+
fulfil or refund a product · email the person the event is about · call
|
|
454
|
+
your URL (a signed notice to a server YOU run).
|
|
453
455
|
Does not: run your code; call OpenAI or any third API for you (call_url
|
|
454
456
|
carries Gemmein's body and signature, never your key or a request you
|
|
455
457
|
shape); attach a person to a schedule (no "email everyone due tomorrow"
|
|
@@ -460,7 +462,8 @@ contents.
|
|
|
460
462
|
transform, a third API — put that in your own function behind call_url;
|
|
461
463
|
it receives a verified event with the person already resolved. The AI
|
|
462
464
|
route is where Gemmein holds your provider key and makes a chat call for
|
|
463
|
-
a signed-in person at
|
|
465
|
+
a signed-in person at the tool's credit price (one by default) — see
|
|
466
|
+
The AI route.
|
|
464
467
|
Use a relay when: a provider other than Stripe signs a webhook that
|
|
465
468
|
should change access or write a record (GoCardless, Paddle, Lemon
|
|
466
469
|
Squeezy, a form or signature tool); a record change should tell the
|
|
@@ -517,6 +520,54 @@ contents.
|
|
|
517
520
|
- email_person { subject, text, kind? } — rides notify's caps.
|
|
518
521
|
- call_url { url } — https, signed POST (`X-Gemmein-Signature`), 10 s; the
|
|
519
522
|
same `id` on every retry and replay — your URL deduplicates on it.
|
|
523
|
+
- fulfil_product { product, ref? } — product is a name on the Payments
|
|
524
|
+
page; ref is the provider's payment reference.
|
|
525
|
+
- refund_product { product, ref? } — the refund twin.
|
|
526
|
+
fulfil_product grants the product's key and credits to the event's
|
|
527
|
+
person and writes the receipt, exactly as a Stripe purchase does;
|
|
528
|
+
refund_product takes them back. Both are idempotent on ref: a ref must
|
|
529
|
+
be the provider's unique payment identifier; a ref already used by
|
|
530
|
+
another purchase is refused and audited. A ref you wrote that names
|
|
531
|
+
nothing on the event is refused, never replaced: only an absent ref
|
|
532
|
+
falls back to rly:<eventId>. The relay road binds by name for
|
|
533
|
+
FULFILMENT: renaming or deleting the relay stops fulfilment until a
|
|
534
|
+
relay with that name exists again; the product card shows it.
|
|
535
|
+
refund_product may run from any relay in the environment; it refunds
|
|
536
|
+
only a purchase a relay fulfilled, for the event's person and the
|
|
537
|
+
product it names. This is how
|
|
538
|
+
a product sold via a relay (How it's sold = a relay's name, not a
|
|
539
|
+
Stripe Payment Link) gets fulfilled — a provider without Payment Links
|
|
540
|
+
(GoCardless, Lemon Squeezy, Paddle, bank transfer) confirms a payment,
|
|
541
|
+
the relay fulfils the product:
|
|
542
|
+
|
|
543
|
+
{
|
|
544
|
+
"name": "gocardless-paid",
|
|
545
|
+
"trigger": {
|
|
546
|
+
"kind": "receiver",
|
|
547
|
+
"verify": { "scheme": "shared_token" },
|
|
548
|
+
"map": {
|
|
549
|
+
"event_id": "events.0.id",
|
|
550
|
+
"event_type": "events.0.action",
|
|
551
|
+
"person_email": "events.0.details.customer_email",
|
|
552
|
+
"payment_id": "events.0.links.payment"
|
|
553
|
+
},
|
|
554
|
+
"when": { "event_type": "confirmed" }
|
|
555
|
+
},
|
|
556
|
+
"actions": [
|
|
557
|
+
{ "type": "fulfil_product", "product": "Starter pack", "ref": "{{mapped.payment_id}}" }
|
|
558
|
+
]
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
Mapped fields ride into templates as {{mapped.<name>}}; {{event.<path>}}
|
|
562
|
+
reads the raw payload.
|
|
563
|
+
A template that names nothing is recorded as a warning on the relay
|
|
564
|
+
event — read it on the Relays page before trusting a ref.
|
|
565
|
+
|
|
566
|
+
The refund twin swaps the trigger's `when` to
|
|
567
|
+
`{ "event_type": "refunded" }` and the action to `refund_product`.
|
|
568
|
+
`g.payments.buy` on a product sold this way answers 409
|
|
569
|
+
`product_not_sellable`; saving a relay action against a relay name that
|
|
570
|
+
does not exist in this environment answers 400 `relay_missing`.
|
|
520
571
|
Full depth — every verify scheme, the map and when grammar, the call_url
|
|
521
572
|
contract and signature, every refusal code, the limits and both rails:
|
|
522
573
|
https://docs.gemmein.com/relays (the same chapter is REFERENCE.md
|
|
@@ -645,6 +696,17 @@ contents.
|
|
|
645
696
|
returns. Elevation is READ-only: build admin views that see everything,
|
|
646
697
|
but route status changes on other users' records (fulfilment, moderation)
|
|
647
698
|
to your human's Gemmein dashboard — they click the record there.
|
|
699
|
+
- Products — a capability card.
|
|
700
|
+
What it is: A named thing you sell once: a download, a licence, a
|
|
701
|
+
credit pack.
|
|
702
|
+
Does: Grants its key and its credits on purchase; writes the buyer's
|
|
703
|
+
receipt; a full refund takes both back.
|
|
704
|
+
Does not: Does not set a price — the provider does. Does not sell
|
|
705
|
+
subscriptions (those are plans).
|
|
706
|
+
Needs something else when: You sell through a provider without a
|
|
707
|
+
Payment Link → a relay with fulfil_product; you meter by usage →
|
|
708
|
+
credits spent per AI tool.
|
|
709
|
+
Example: "Starter pack", 100 credits, sold via a GoCardless relay.
|
|
648
710
|
- Payments: the builder names plans in the dashboard, pastes one Stripe
|
|
649
711
|
signing secret, and pastes each paid plan's Stripe Payment Link there too.
|
|
650
712
|
The app's ONLY checkout job is `await g.subscriptions.checkout("pro")` on the upgrade
|
|
@@ -723,13 +785,19 @@ contents.
|
|
|
723
785
|
scope, said out loud): plans are for subscriptions; products are for
|
|
724
786
|
things. Selling a SERVICE session this way (tutoring, coaching, a
|
|
725
787
|
consultation) is fine — nothing ships; the recorded purchase is the
|
|
726
|
-
proof the session was paid for. The builder adds products
|
|
727
|
-
|
|
788
|
+
proof the session was paid for. The builder adds products on the same
|
|
789
|
+
Payments page and sets how each is sold: a Stripe Payment Link, a relay
|
|
790
|
+
(any provider whose webhook the founder maps — GoCardless, Lemon
|
|
791
|
+
Squeezy, Paddle, bank transfer — fulfils it with `fulfil_product`), or
|
|
792
|
+
not yet (the product is defined, its grants and credits are known, no
|
|
793
|
+
road wired). The app calls
|
|
728
794
|
`await g.payments.buy("beat")` — or, when one product covers many items (license
|
|
729
795
|
tiers over a catalog), names the item:
|
|
730
796
|
`await g.payments.buy("premium license", { item: "beat_37" })` (display text
|
|
731
|
-
only; the PRICE always comes from the
|
|
732
|
-
item note can never change what's paid).
|
|
797
|
+
only; the PRICE always comes from the provider — the Payment Link or
|
|
798
|
+
the relay — so the item note can never change what's paid). A product
|
|
799
|
+
sold via a relay or not yet has no Payment Link to open:
|
|
800
|
+
`g.payments.buy` answers 409 `product_not_sellable`. Gemmein records every completed
|
|
733
801
|
payment itself — `await g.purchases.mine()` is the buyer's proof:
|
|
734
802
|
{ item, kind, status: "paid"|"part_refunded"|"refunded", amountMinor,
|
|
735
803
|
currency, refundedMinor, grants, paidAt, delivery? }. Selling a FILE (a
|
|
@@ -751,9 +819,10 @@ contents.
|
|
|
751
819
|
what the app actually sells — use those names.
|
|
752
820
|
- Credits — a quantity beside access. See https://docs.gemmein.com/credits
|
|
753
821
|
What it is: a balance your customers hold and your product spends — a
|
|
754
|
-
pack they buy, a comp you give,
|
|
822
|
+
pack they buy, a comp you give, an AI tool call priced in credits.
|
|
755
823
|
Does: a product grants credits at purchase (its Grants credits field);
|
|
756
|
-
your server spends them with a reason; the AI route spends
|
|
824
|
+
your server spends them with a reason; the AI route spends the credits
|
|
825
|
+
the owner set for the named tool, one by default;
|
|
757
826
|
a relay grants them; the owner's dashboard shows the balance and the
|
|
758
827
|
ledger and comps by hand. Every movement is one ledger line.
|
|
759
828
|
Does not: expire; go negative (floor 0 — a spend past the balance is
|
|
@@ -801,70 +870,111 @@ contents.
|
|
|
801
870
|
key already names a different movement) · `credits_ceiling` (409 — the
|
|
802
871
|
balance would pass 1,000,000,000; nothing added).
|
|
803
872
|
- The AI route — your app talks to OpenAI, Anthropic or Google through
|
|
804
|
-
Gemmein, on YOUR provider key, which never reaches the browser
|
|
873
|
+
Gemmein, on YOUR provider key, which never reaches the browser, through
|
|
874
|
+
a named AI tool your human prices and gates in the dashboard (or the
|
|
875
|
+
local `gemmein/ai/tools/<name>.json` file). Removing a tool is a
|
|
876
|
+
step-up action, like removing a key.
|
|
805
877
|
See https://docs.gemmein.com/ai
|
|
806
|
-
What it is:
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
key
|
|
828
|
-
|
|
878
|
+
What it is: A named AI operation you price in credits and gate by
|
|
879
|
+
access.
|
|
880
|
+
Does: Runs on your provider key with the model and limits you set;
|
|
881
|
+
spends the tool's credits before the call and refunds them if the
|
|
882
|
+
provider fails before answering.
|
|
883
|
+
Does not: Does not price by token, does not let the browser set a
|
|
884
|
+
price or a model the tool pins, does not run from a server key.
|
|
885
|
+
Needs something else when: You want a plan-dependent price for the
|
|
886
|
+
same operation → make two tools and gate each; you meter something
|
|
887
|
+
that is not an AI call → spendCredits from your server.
|
|
888
|
+
Example: "Deep Research", openai, 20 credits, requires
|
|
889
|
+
access:pro-max — a tool file at `gemmein/ai/tools/deep-research.json`:
|
|
890
|
+
|
|
891
|
+
{
|
|
892
|
+
"label": "Deep Research",
|
|
893
|
+
"provider": "openai",
|
|
894
|
+
"credits": 20,
|
|
895
|
+
"requires": "access:pro-max"
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
On the local rail a tool saves without a Keys-page allowlist or a
|
|
899
|
+
configured key; the cloud refuses both at save.
|
|
900
|
+
Also true of the route itself: `sk_` is refused; it does not choose
|
|
901
|
+
models, cache, summarise, moderate, or reshape the request or the
|
|
902
|
+
answer; it does not refund a call that dies mid-stream, or one the
|
|
903
|
+
caller abandons before the headers (hanging up early does not
|
|
904
|
+
refund). Your SERVER makes the model call → call the provider
|
|
905
|
+
directly with your key (the gate still answers who they are and what
|
|
906
|
+
they hold). The call is not a chat call → embeddings, images, audio
|
|
907
|
+
go to the provider directly, from your server. The provider is not on
|
|
908
|
+
the list → write to hello@gemmein.com.
|
|
909
|
+
A call that names no tool runs as the default tool: one credit, your
|
|
910
|
+
configured provider, any allowed model.
|
|
911
|
+
A person who lacks the entitlement sees "Deep Research requires Pro
|
|
912
|
+
Max."; one short of the price sees "Deep Research costs 20 credits.
|
|
913
|
+
You have 7."; the ledger line reads "20 credits spent · Deep Research
|
|
914
|
+
· 87 remaining."
|
|
915
|
+
The case: "a chat app where each message costs the credits the owner
|
|
916
|
+
set for that tool — one by default — and my OpenAI key stays
|
|
917
|
+
private". The body is exactly what the provider documents for its
|
|
918
|
+
chat endpoint; `g.ai.chat` returns the fetch Response untouched:
|
|
829
919
|
|
|
830
920
|
const res = await g.ai.chat({
|
|
831
921
|
model: "gpt-4o-mini", stream: true,
|
|
832
922
|
messages: [{ role: "user", content: text }]
|
|
833
|
-
}, {
|
|
923
|
+
}, { tool: "deep-research" }) // tool is optional — leaving it
|
|
924
|
+
// out runs the default tool
|
|
834
925
|
// res.status and res.body are the provider's own (SSE stays SSE) —
|
|
835
926
|
// a provider 4xx/5xx comes back the same way: read res.ok; only
|
|
836
927
|
// Gemmein's own refusals throw
|
|
837
928
|
// res.headers: x-gemmein-credits-remaining: 41
|
|
838
|
-
// x-gemmein-
|
|
929
|
+
// x-gemmein-tool: deep-research (names the tool;
|
|
930
|
+
// absent on the implicit default)
|
|
931
|
+
// x-gemmein-credit: refunded (only when the credits
|
|
932
|
+
// came back — a failure before the first byte)
|
|
839
933
|
|
|
840
934
|
For a non-stream answer as one string, whichever provider answered (a
|
|
841
935
|
provider non-2xx throws `provider_error` with its status and message):
|
|
842
936
|
|
|
843
|
-
const answer = await g.ai.text({
|
|
844
|
-
|
|
937
|
+
const answer = await g.ai.text({ messages: [{ role: "user",
|
|
938
|
+
content: text }] }, { tool: "deep-research" })
|
|
845
939
|
|
|
846
|
-
Branch on `err.code`:
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
940
|
+
Branch on `err.code`: unknown_tool (404 — no tool by that name in
|
|
941
|
+
this environment) · tool_disabled (403 — the owner switched it off)
|
|
942
|
+
· entitlement_required (403 — the message names the plan or product
|
|
943
|
+
it needs) · model_pinned (403 — the tool's model is fixed; leave
|
|
944
|
+
`model` out of the body) · provider_not_configured (409 — creating a
|
|
945
|
+
tool: add that provider's key first) · too_many_tools (409 — 50
|
|
946
|
+
tools per environment) · invalid_tool (400 — creating or updating a
|
|
947
|
+
tool with a bad field) · credits_exhausted (402 — the message names
|
|
948
|
+
the tool, its price and the balance, singular for 1; show the pack)
|
|
949
|
+
· ai_not_configured (409 — no
|
|
950
|
+
key set; the owner pastes one) · provider_required (400 — more than
|
|
951
|
+
one key set; name `provider`) · model_not_allowed (403 — the owner's
|
|
952
|
+
allowlist names what is allowed, for a tool with no pinned model) ·
|
|
953
|
+
ai_capped (429 — 20 per person per minute; wait for `resetAt`) ·
|
|
954
|
+
payload_too_large (413 — 256 KB, unless the tool sets a smaller
|
|
955
|
+
cap) · invalid_body (400 — the body must be the provider's JSON
|
|
956
|
+
object, nested at most 32 levels; a `?provider=` that disagrees with
|
|
957
|
+
a named tool's provider is refused the same way) · session_required
|
|
958
|
+
(401 — sign in first) · scope_denied (403 — a server key; the route
|
|
959
|
+
is for the browser) · provider_unreachable (502 — no answer before
|
|
960
|
+
the first byte; the credit is refunded; retry) · provider_error
|
|
961
|
+
(`g.ai.text` only — the provider's own non-2xx, its status and
|
|
962
|
+
message). Numbers: the credits the owner set for that tool, one by
|
|
963
|
+
default · ≤ 50 tools per environment · name ≤ 40 chars · label ≤ 60
|
|
964
|
+
chars · 20/min/person · 256 KB (a tool may set a smaller
|
|
965
|
+
`bounds.maxBodyBytes`, up to 262,144) · 170 s in all, and on a
|
|
966
|
+
stream 10 s to the first response headers.
|
|
967
|
+
Facts: `?provider=` and `?stream=1` on the URL do what the body
|
|
968
|
+
fields do (a named tool's own provider always wins); the owner may
|
|
969
|
+
list up to 20 allowed models for the default tool and any tool with
|
|
970
|
+
no pinned model (the Keys room's test call uses the first); every
|
|
971
|
+
`/ai/chat` call counts toward the app's api_requests band like any
|
|
972
|
+
other request; a provider that echoes the key in a refusal reaches
|
|
973
|
+
you as `***<hint>`. The owner's Usage room counts the calls; the
|
|
974
|
+
provider bills the tokens on the owner's own account. `gemmein dev`
|
|
975
|
+
answers a fake provider without a key (header `x-gemmein-ai: fake`),
|
|
976
|
+
so the loop runs locally; set GEMMEIN_AI_KEY_OPENAI / _ANTHROPIC /
|
|
977
|
+
_GOOGLE there for a real call.
|
|
868
978
|
- Drafts on PUBLIC collections (public_read, community): create with the
|
|
869
979
|
OPTION `{ published: false }` → hidden from every reader except its
|
|
870
980
|
author and the owner, server-enforced; publish with
|
|
@@ -914,8 +1024,8 @@ contents.
|
|
|
914
1024
|
conversation and customer replies land; holding the billing band;
|
|
915
1025
|
running a relay: an inbound webhook from any provider that
|
|
916
1026
|
signs its calls, a schedule, or a record change → write a record,
|
|
917
|
-
grant or revoke access, grant credits,
|
|
918
|
-
URL, with
|
|
1027
|
+
grant or revoke access, grant credits, fulfil or refund a product,
|
|
1028
|
+
email the person, or call your URL, with
|
|
919
1029
|
retries and a replay button (the definition is a file you write —
|
|
920
1030
|
see Relays — and the dashboard is where it is read, paused,
|
|
921
1031
|
replayed and its secrets rotated). None
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gemmein/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Gemmein SDK \u2014 passwordless auth, safe storage, and Stripe-driven record flips for AI-built apps. Small enough that one prompt teaches the whole API.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|