@gemmein/sdk 0.6.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 +120 -0
- package/REFERENCE.md +229 -11
- package/dist/index.cjs +214 -2
- package/dist/index.d.cts +144 -5
- package/dist/index.d.ts +144 -5
- package/dist/index.js +211 -1
- package/llms.txt +257 -21
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.GemmeinServer = exports.CollectionClient = exports.StorageClient = exports.AccountClient = exports.PaymentsClient = exports.SubscriptionsClient = exports.FilesClient = exports.PurchasesClient = exports.AuthClient = exports.Gemmein = exports.BrowserTokenStore = exports.MemoryTokenStore = exports.GemmeinError = exports.CLIENT_INFO = exports.SDK_VERSION = void 0;
|
|
3
|
+
exports.GemmeinServer = exports.CollectionClient = exports.StorageClient = exports.AiClient = exports.CreditsClient = exports.AccountClient = exports.PaymentsClient = exports.SubscriptionsClient = exports.FilesClient = exports.PurchasesClient = exports.AuthClient = exports.Gemmein = exports.BrowserTokenStore = exports.MemoryTokenStore = exports.GemmeinError = exports.CLIENT_INFO = exports.SDK_VERSION = void 0;
|
|
4
4
|
exports.gemmein = gemmein;
|
|
5
5
|
exports.gemmeinServer = gemmeinServer;
|
|
6
6
|
/** This build's version — the value `package.json` carries. Kept inline
|
|
@@ -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
|
|
@@ -124,6 +124,8 @@ class Gemmein {
|
|
|
124
124
|
this.purchases = new PurchasesClient(config);
|
|
125
125
|
this.account = new AccountClient(config);
|
|
126
126
|
this.files = new FilesClient(config);
|
|
127
|
+
this.credits = new CreditsClient(config);
|
|
128
|
+
this.ai = new AiClient(config);
|
|
127
129
|
}
|
|
128
130
|
/**
|
|
129
131
|
* Your app's data — `g.collection<{ title: string }>("notes")`. The
|
|
@@ -372,6 +374,179 @@ class AccountClient {
|
|
|
372
374
|
}
|
|
373
375
|
}
|
|
374
376
|
exports.AccountClient = AccountClient;
|
|
377
|
+
/** The signed-in person's own credits — so an app can draw its own meter. */
|
|
378
|
+
class CreditsClient {
|
|
379
|
+
constructor(config) {
|
|
380
|
+
this.config = config;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* The balance RIGHT NOW, server-resolved — the number that refuses at
|
|
384
|
+
* zero, never client math. Session required (`session_required`, 401).
|
|
385
|
+
*
|
|
386
|
+
* const { balance } = await g.credits.balance();
|
|
387
|
+
*/
|
|
388
|
+
async balance() {
|
|
389
|
+
return runtimeRequest(this.config, "/auth/credits");
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
exports.CreditsClient = CreditsClient;
|
|
393
|
+
/**
|
|
394
|
+
* The AI route. `chat` takes the provider's own request body — exactly what
|
|
395
|
+
* you would POST to OpenAI's /v1/chat/completions, Anthropic's /v1/messages
|
|
396
|
+
* or Google's generateContent — and answers with the fetch `Response`
|
|
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).
|
|
401
|
+
* Response headers: `x-gemmein-credits-remaining` on every answer that
|
|
402
|
+
* passed the spend; `x-gemmein-credit: refunded` when the provider failed
|
|
403
|
+
* before its first byte.
|
|
404
|
+
*/
|
|
405
|
+
class AiClient {
|
|
406
|
+
constructor(config) {
|
|
407
|
+
this.config = config;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* const res = await g.ai.chat({ model: "gpt-4o-mini", messages, stream: true });
|
|
411
|
+
* for await (const chunk of res.body) { … }
|
|
412
|
+
*
|
|
413
|
+
* Browser sessions only — a server key is refused (`scope_denied`, 403).
|
|
414
|
+
* Refusals, all `GemmeinError`: `session_required` (401) ·
|
|
415
|
+
* `credits_exhausted` (402 — the message carries the balance; show your
|
|
416
|
+
* own "buy more" door, which is a product checkout) · `ai_not_configured`
|
|
417
|
+
* (409 — the owner has set no key) · `provider_required` (400) ·
|
|
418
|
+
* `model_not_allowed` (403 — the owner's models list) · `ai_capped`
|
|
419
|
+
* (429 — 20 calls a minute per person; `err.resetAt`) ·
|
|
420
|
+
* `payload_too_large` (413 — 256 KB) · `provider_unreachable` (502,
|
|
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.
|
|
432
|
+
*/
|
|
433
|
+
async chat(body, options = {}) {
|
|
434
|
+
const payload = options.provider ? { provider: options.provider, ...body } : body;
|
|
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, {
|
|
439
|
+
method: "POST",
|
|
440
|
+
body: JSON.stringify(payload),
|
|
441
|
+
headers: await runtimeHeaders(this.config, { "content-type": "application/json" }),
|
|
442
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
443
|
+
});
|
|
444
|
+
if (!response.ok) {
|
|
445
|
+
if (isForwardedAnswer(response)) {
|
|
446
|
+
// Past the spend: the provider answered. `provider_unreachable` is
|
|
447
|
+
// the one Gemmein refusal written after the spend (it carries the
|
|
448
|
+
// refund header) — peek without consuming so it stays typed.
|
|
449
|
+
const peek = (await response.clone().json().catch(() => null));
|
|
450
|
+
if (peek?.code !== "provider_unreachable")
|
|
451
|
+
return response;
|
|
452
|
+
}
|
|
453
|
+
const errorBody = await readErrorBody(response);
|
|
454
|
+
if (errorBody.code === "auth_expired")
|
|
455
|
+
await this.config.tokenStore.clear();
|
|
456
|
+
throw new GemmeinError({ status: response.status, ...errorBody });
|
|
457
|
+
}
|
|
458
|
+
return response;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* The non-streaming convenience: one call, one string. Pass a body that
|
|
462
|
+
* does NOT stream (`stream` unset or false); the provider's JSON answer is
|
|
463
|
+
* read whole and the text is lifted out per provider — OpenAI
|
|
464
|
+
* `choices[0].message.content`, Anthropic `content[].text` joined,
|
|
465
|
+
* Google `candidates[0].content.parts[].text` joined. An answer with no
|
|
466
|
+
* text in any of those places throws `invalid_response` (status 0). A
|
|
467
|
+
* provider's own non-2xx throws `provider_error` with the provider's
|
|
468
|
+
* status and the provider's message (the owner's key masked to its
|
|
469
|
+
* hint if the provider echoed it).
|
|
470
|
+
*
|
|
471
|
+
* const answer = await g.ai.text({ model: "claude-sonnet-4-5", max_tokens: 400, messages });
|
|
472
|
+
*/
|
|
473
|
+
async text(body, options = {}) {
|
|
474
|
+
const response = await this.chat(body, options);
|
|
475
|
+
if (!response.ok) {
|
|
476
|
+
throw new GemmeinError({
|
|
477
|
+
status: response.status,
|
|
478
|
+
code: "provider_error",
|
|
479
|
+
message: await providerErrorMessage(response),
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
const data = (await response.json());
|
|
483
|
+
const text = extractAiText(data);
|
|
484
|
+
if (text === null) {
|
|
485
|
+
throw new GemmeinError({
|
|
486
|
+
status: 0,
|
|
487
|
+
code: "invalid_response",
|
|
488
|
+
message: "the provider answered without any text — for a streaming body use g.ai.chat() and read the stream",
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
return text;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
exports.AiClient = AiClient;
|
|
495
|
+
/** An answer that passed the spend: the engine stamps the balance (and, on
|
|
496
|
+
* a refund, `x-gemmein-credit`) only after the credit moved — a refusal
|
|
497
|
+
* before the spend carries neither. */
|
|
498
|
+
function isForwardedAnswer(response) {
|
|
499
|
+
return response.headers.has("x-gemmein-credits-remaining") || response.headers.has("x-gemmein-credit");
|
|
500
|
+
}
|
|
501
|
+
/** The provider's own reason, whichever shape it used — OpenAI, Anthropic
|
|
502
|
+
* and Google all nest it as `error.message`; anything else is the text. */
|
|
503
|
+
async function providerErrorMessage(response) {
|
|
504
|
+
const text = await response.text().catch(() => "");
|
|
505
|
+
try {
|
|
506
|
+
const parsed = JSON.parse(text);
|
|
507
|
+
if (parsed && typeof parsed === "object") {
|
|
508
|
+
const nested = typeof parsed.error === "object" && parsed.error !== null ? parsed.error.message : parsed.error;
|
|
509
|
+
const message = typeof nested === "string" ? nested : typeof parsed.message === "string" ? parsed.message : null;
|
|
510
|
+
if (message)
|
|
511
|
+
return message;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
// not JSON — the text is the reason
|
|
516
|
+
}
|
|
517
|
+
return text.trim().slice(0, 500) || `the provider answered ${response.status}`;
|
|
518
|
+
}
|
|
519
|
+
/** The per-provider lift, by shape (a provider's answer is unmistakable). */
|
|
520
|
+
function extractAiText(data) {
|
|
521
|
+
if (!data || typeof data !== "object")
|
|
522
|
+
return null;
|
|
523
|
+
const d = data;
|
|
524
|
+
// OpenAI: choices[0].message.content (a string, or content parts)
|
|
525
|
+
if (Array.isArray(d.choices)) {
|
|
526
|
+
const message = d.choices[0]?.message;
|
|
527
|
+
const content = message?.content;
|
|
528
|
+
if (typeof content === "string")
|
|
529
|
+
return content;
|
|
530
|
+
if (Array.isArray(content))
|
|
531
|
+
return joinTextParts(content);
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
// Anthropic: content[] blocks, the text ones joined
|
|
535
|
+
if (Array.isArray(d.content))
|
|
536
|
+
return joinTextParts(d.content);
|
|
537
|
+
// Google: candidates[0].content.parts[].text joined
|
|
538
|
+
if (Array.isArray(d.candidates)) {
|
|
539
|
+
const parts = d.candidates[0]?.content?.parts;
|
|
540
|
+
return Array.isArray(parts) ? joinTextParts(parts) : null;
|
|
541
|
+
}
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
function joinTextParts(parts) {
|
|
545
|
+
const texts = parts
|
|
546
|
+
.map((p) => (p && typeof p === "object" && typeof p.text === "string" ? p.text : null))
|
|
547
|
+
.filter((t) => t !== null);
|
|
548
|
+
return texts.length > 0 ? texts.join("") : null;
|
|
549
|
+
}
|
|
375
550
|
function isSessionResponse(value) {
|
|
376
551
|
return (typeof value === "object" &&
|
|
377
552
|
value !== null &&
|
|
@@ -952,6 +1127,43 @@ class GemmeinServer {
|
|
|
952
1127
|
}),
|
|
953
1128
|
});
|
|
954
1129
|
}
|
|
1130
|
+
/**
|
|
1131
|
+
* W9.3 — spend a person's credits from YOUR server, with a reason:
|
|
1132
|
+
*
|
|
1133
|
+
* const { balance } = await g.spendCredits(personId, {
|
|
1134
|
+
* amount: 5, // default 1; 1..10,000 per call
|
|
1135
|
+
* reason: "render:4k", // what the owner reads in the ledger
|
|
1136
|
+
* key: `render:${jobId}`, // your retry key — a repeat is deduped
|
|
1137
|
+
* });
|
|
1138
|
+
* // { ok: true, spent: 5, deduped: false, balance: { before: 20, after: 15 },
|
|
1139
|
+
* // event: { id: "cev_…", reason: "render:4k", actor: "<your key's name>" } }
|
|
1140
|
+
*
|
|
1141
|
+
* ONE conditional update, floored at 0: the spend succeeds whole or not at
|
|
1142
|
+
* all, and `402 credits_exhausted` says "this person has {balance} credits
|
|
1143
|
+
* — the spend needs {amount}". Pass `key` when the caller can retry — the
|
|
1144
|
+
* same key answers the first spend again with `deduped: true`, `spent: 0`,
|
|
1145
|
+
* the same `event` and the balance untouched. The key is scoped to the
|
|
1146
|
+
* person: one order id reused for two people charges both. Credits come
|
|
1147
|
+
* from a pack they bought, a comp in the back office, or a relay; a key
|
|
1148
|
+
* cannot mint them.
|
|
1149
|
+
*
|
|
1150
|
+
* Needs the key's "Spend a person's credits" box ticked by your human.
|
|
1151
|
+
* Refusals: `capability_required` (403 — "this key can't spend credits —
|
|
1152
|
+
* mint a key with 'Spend a person's credits' ticked") ·
|
|
1153
|
+
* `credits_exhausted` (402) · `person_not_found` (404) · `invalid_amount` /
|
|
1154
|
+
* `invalid_reason` / `invalid_key` (400) · `dedupe_conflict` (409 — the
|
|
1155
|
+
* key already names a different movement).
|
|
1156
|
+
*/
|
|
1157
|
+
async spendCredits(personId, input) {
|
|
1158
|
+
return this.gate(`/server/people/${encodeURIComponent(personId)}/credits/spend`, {
|
|
1159
|
+
method: "POST",
|
|
1160
|
+
body: JSON.stringify({
|
|
1161
|
+
...(input.amount !== undefined ? { amount: input.amount } : {}),
|
|
1162
|
+
reason: input.reason,
|
|
1163
|
+
...(input.key !== undefined ? { key: input.key } : {}),
|
|
1164
|
+
}),
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
955
1167
|
/**
|
|
956
1168
|
* End one grant — the reversibility law in one call:
|
|
957
1169
|
*
|
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;
|
|
@@ -183,6 +183,10 @@ export declare class Gemmein {
|
|
|
183
183
|
readonly purchases: PurchasesClient;
|
|
184
184
|
readonly account: AccountClient;
|
|
185
185
|
readonly files: FilesClient;
|
|
186
|
+
/** W9.3 — the signed-in person's own credit balance (engine 0.8.0). */
|
|
187
|
+
readonly credits: CreditsClient;
|
|
188
|
+
/** W9.3 — the AI route: OpenAI / Anthropic / Google on the owner's key. */
|
|
189
|
+
readonly ai: AiClient;
|
|
186
190
|
constructor(options: GemmeinOptions);
|
|
187
191
|
/**
|
|
188
192
|
* Your app's data — `g.collection<{ title: string }>("notes")`. The
|
|
@@ -382,6 +386,107 @@ export declare class AccountClient {
|
|
|
382
386
|
*/
|
|
383
387
|
delete(): Promise<unknown>;
|
|
384
388
|
}
|
|
389
|
+
export type AiProvider = "openai" | "anthropic" | "google";
|
|
390
|
+
export type AiChatOptions = {
|
|
391
|
+
/** Which configured provider answers. Optional when exactly one key is
|
|
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`. */
|
|
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;
|
|
401
|
+
/** Abort the call — the stream closes; a call that dies mid-stream is
|
|
402
|
+
* not refunded. */
|
|
403
|
+
signal?: AbortSignal;
|
|
404
|
+
};
|
|
405
|
+
/** `spendCredits`'s answer: `spent` is the amount taken (0 on a deduped
|
|
406
|
+
* repeat); `event` is the ledger line — the same one on a repeat. */
|
|
407
|
+
export type SpendCreditsResult = {
|
|
408
|
+
ok: true;
|
|
409
|
+
spent: number;
|
|
410
|
+
deduped: boolean;
|
|
411
|
+
balance: {
|
|
412
|
+
before: number;
|
|
413
|
+
after: number;
|
|
414
|
+
};
|
|
415
|
+
event: {
|
|
416
|
+
id: string;
|
|
417
|
+
reason: string | null;
|
|
418
|
+
actor: string;
|
|
419
|
+
};
|
|
420
|
+
};
|
|
421
|
+
/** The signed-in person's own credits — so an app can draw its own meter. */
|
|
422
|
+
export declare class CreditsClient {
|
|
423
|
+
private readonly config;
|
|
424
|
+
constructor(config: ClientConfig);
|
|
425
|
+
/**
|
|
426
|
+
* The balance RIGHT NOW, server-resolved — the number that refuses at
|
|
427
|
+
* zero, never client math. Session required (`session_required`, 401).
|
|
428
|
+
*
|
|
429
|
+
* const { balance } = await g.credits.balance();
|
|
430
|
+
*/
|
|
431
|
+
balance(): Promise<{
|
|
432
|
+
balance: number;
|
|
433
|
+
}>;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* The AI route. `chat` takes the provider's own request body — exactly what
|
|
437
|
+
* you would POST to OpenAI's /v1/chat/completions, Anthropic's /v1/messages
|
|
438
|
+
* or Google's generateContent — and answers with the fetch `Response`
|
|
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).
|
|
443
|
+
* Response headers: `x-gemmein-credits-remaining` on every answer that
|
|
444
|
+
* passed the spend; `x-gemmein-credit: refunded` when the provider failed
|
|
445
|
+
* before its first byte.
|
|
446
|
+
*/
|
|
447
|
+
export declare class AiClient {
|
|
448
|
+
private readonly config;
|
|
449
|
+
constructor(config: ClientConfig);
|
|
450
|
+
/**
|
|
451
|
+
* const res = await g.ai.chat({ model: "gpt-4o-mini", messages, stream: true });
|
|
452
|
+
* for await (const chunk of res.body) { … }
|
|
453
|
+
*
|
|
454
|
+
* Browser sessions only — a server key is refused (`scope_denied`, 403).
|
|
455
|
+
* Refusals, all `GemmeinError`: `session_required` (401) ·
|
|
456
|
+
* `credits_exhausted` (402 — the message carries the balance; show your
|
|
457
|
+
* own "buy more" door, which is a product checkout) · `ai_not_configured`
|
|
458
|
+
* (409 — the owner has set no key) · `provider_required` (400) ·
|
|
459
|
+
* `model_not_allowed` (403 — the owner's models list) · `ai_capped`
|
|
460
|
+
* (429 — 20 calls a minute per person; `err.resetAt`) ·
|
|
461
|
+
* `payload_too_large` (413 — 256 KB) · `provider_unreachable` (502,
|
|
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.
|
|
473
|
+
*/
|
|
474
|
+
chat(body: Record<string, unknown>, options?: AiChatOptions): Promise<Response>;
|
|
475
|
+
/**
|
|
476
|
+
* The non-streaming convenience: one call, one string. Pass a body that
|
|
477
|
+
* does NOT stream (`stream` unset or false); the provider's JSON answer is
|
|
478
|
+
* read whole and the text is lifted out per provider — OpenAI
|
|
479
|
+
* `choices[0].message.content`, Anthropic `content[].text` joined,
|
|
480
|
+
* Google `candidates[0].content.parts[].text` joined. An answer with no
|
|
481
|
+
* text in any of those places throws `invalid_response` (status 0). A
|
|
482
|
+
* provider's own non-2xx throws `provider_error` with the provider's
|
|
483
|
+
* status and the provider's message (the owner's key masked to its
|
|
484
|
+
* hint if the provider echoed it).
|
|
485
|
+
*
|
|
486
|
+
* const answer = await g.ai.text({ model: "claude-sonnet-4-5", max_tokens: 400, messages });
|
|
487
|
+
*/
|
|
488
|
+
text(body: Record<string, unknown>, options?: AiChatOptions): Promise<string>;
|
|
489
|
+
}
|
|
385
490
|
export declare class StorageClient {
|
|
386
491
|
private readonly config;
|
|
387
492
|
constructor(config: ClientConfig);
|
|
@@ -562,9 +667,11 @@ export type Grant = {
|
|
|
562
667
|
/**
|
|
563
668
|
* What a person holds RIGHT NOW — never what they pay. `access` is the
|
|
564
669
|
* union of live grants' keys; `grants` lists those live grants (revoked
|
|
565
|
-
* and expired ones are gone, not flagged). `credits` is
|
|
566
|
-
*
|
|
567
|
-
*
|
|
670
|
+
* and expired ones are gone, not flagged). `credits` is `{ balance }` —
|
|
671
|
+
* filled from engine 0.8.0 (W9.3): the person's credit balance, floored
|
|
672
|
+
* at 0, the same number `g.credits.balance()` answers and the one the AI
|
|
673
|
+
* route and `spendCredits` refuse against. An engine before 0.8.0 answers
|
|
674
|
+
* `null` here — read it as "not carried", never as zero.
|
|
568
675
|
*/
|
|
569
676
|
export type Holdings = {
|
|
570
677
|
access: string[];
|
|
@@ -760,6 +867,38 @@ export declare class GemmeinServer {
|
|
|
760
867
|
grant: Grant;
|
|
761
868
|
holdings: Holdings;
|
|
762
869
|
}>;
|
|
870
|
+
/**
|
|
871
|
+
* W9.3 — spend a person's credits from YOUR server, with a reason:
|
|
872
|
+
*
|
|
873
|
+
* const { balance } = await g.spendCredits(personId, {
|
|
874
|
+
* amount: 5, // default 1; 1..10,000 per call
|
|
875
|
+
* reason: "render:4k", // what the owner reads in the ledger
|
|
876
|
+
* key: `render:${jobId}`, // your retry key — a repeat is deduped
|
|
877
|
+
* });
|
|
878
|
+
* // { ok: true, spent: 5, deduped: false, balance: { before: 20, after: 15 },
|
|
879
|
+
* // event: { id: "cev_…", reason: "render:4k", actor: "<your key's name>" } }
|
|
880
|
+
*
|
|
881
|
+
* ONE conditional update, floored at 0: the spend succeeds whole or not at
|
|
882
|
+
* all, and `402 credits_exhausted` says "this person has {balance} credits
|
|
883
|
+
* — the spend needs {amount}". Pass `key` when the caller can retry — the
|
|
884
|
+
* same key answers the first spend again with `deduped: true`, `spent: 0`,
|
|
885
|
+
* the same `event` and the balance untouched. The key is scoped to the
|
|
886
|
+
* person: one order id reused for two people charges both. Credits come
|
|
887
|
+
* from a pack they bought, a comp in the back office, or a relay; a key
|
|
888
|
+
* cannot mint them.
|
|
889
|
+
*
|
|
890
|
+
* Needs the key's "Spend a person's credits" box ticked by your human.
|
|
891
|
+
* Refusals: `capability_required` (403 — "this key can't spend credits —
|
|
892
|
+
* mint a key with 'Spend a person's credits' ticked") ·
|
|
893
|
+
* `credits_exhausted` (402) · `person_not_found` (404) · `invalid_amount` /
|
|
894
|
+
* `invalid_reason` / `invalid_key` (400) · `dedupe_conflict` (409 — the
|
|
895
|
+
* key already names a different movement).
|
|
896
|
+
*/
|
|
897
|
+
spendCredits(personId: string, input: {
|
|
898
|
+
amount?: number;
|
|
899
|
+
reason: string;
|
|
900
|
+
key?: string;
|
|
901
|
+
}): Promise<SpendCreditsResult>;
|
|
763
902
|
/**
|
|
764
903
|
* End one grant — the reversibility law in one call:
|
|
765
904
|
*
|
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;
|
|
@@ -183,6 +183,10 @@ export declare class Gemmein {
|
|
|
183
183
|
readonly purchases: PurchasesClient;
|
|
184
184
|
readonly account: AccountClient;
|
|
185
185
|
readonly files: FilesClient;
|
|
186
|
+
/** W9.3 — the signed-in person's own credit balance (engine 0.8.0). */
|
|
187
|
+
readonly credits: CreditsClient;
|
|
188
|
+
/** W9.3 — the AI route: OpenAI / Anthropic / Google on the owner's key. */
|
|
189
|
+
readonly ai: AiClient;
|
|
186
190
|
constructor(options: GemmeinOptions);
|
|
187
191
|
/**
|
|
188
192
|
* Your app's data — `g.collection<{ title: string }>("notes")`. The
|
|
@@ -382,6 +386,107 @@ export declare class AccountClient {
|
|
|
382
386
|
*/
|
|
383
387
|
delete(): Promise<unknown>;
|
|
384
388
|
}
|
|
389
|
+
export type AiProvider = "openai" | "anthropic" | "google";
|
|
390
|
+
export type AiChatOptions = {
|
|
391
|
+
/** Which configured provider answers. Optional when exactly one key is
|
|
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`. */
|
|
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;
|
|
401
|
+
/** Abort the call — the stream closes; a call that dies mid-stream is
|
|
402
|
+
* not refunded. */
|
|
403
|
+
signal?: AbortSignal;
|
|
404
|
+
};
|
|
405
|
+
/** `spendCredits`'s answer: `spent` is the amount taken (0 on a deduped
|
|
406
|
+
* repeat); `event` is the ledger line — the same one on a repeat. */
|
|
407
|
+
export type SpendCreditsResult = {
|
|
408
|
+
ok: true;
|
|
409
|
+
spent: number;
|
|
410
|
+
deduped: boolean;
|
|
411
|
+
balance: {
|
|
412
|
+
before: number;
|
|
413
|
+
after: number;
|
|
414
|
+
};
|
|
415
|
+
event: {
|
|
416
|
+
id: string;
|
|
417
|
+
reason: string | null;
|
|
418
|
+
actor: string;
|
|
419
|
+
};
|
|
420
|
+
};
|
|
421
|
+
/** The signed-in person's own credits — so an app can draw its own meter. */
|
|
422
|
+
export declare class CreditsClient {
|
|
423
|
+
private readonly config;
|
|
424
|
+
constructor(config: ClientConfig);
|
|
425
|
+
/**
|
|
426
|
+
* The balance RIGHT NOW, server-resolved — the number that refuses at
|
|
427
|
+
* zero, never client math. Session required (`session_required`, 401).
|
|
428
|
+
*
|
|
429
|
+
* const { balance } = await g.credits.balance();
|
|
430
|
+
*/
|
|
431
|
+
balance(): Promise<{
|
|
432
|
+
balance: number;
|
|
433
|
+
}>;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* The AI route. `chat` takes the provider's own request body — exactly what
|
|
437
|
+
* you would POST to OpenAI's /v1/chat/completions, Anthropic's /v1/messages
|
|
438
|
+
* or Google's generateContent — and answers with the fetch `Response`
|
|
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).
|
|
443
|
+
* Response headers: `x-gemmein-credits-remaining` on every answer that
|
|
444
|
+
* passed the spend; `x-gemmein-credit: refunded` when the provider failed
|
|
445
|
+
* before its first byte.
|
|
446
|
+
*/
|
|
447
|
+
export declare class AiClient {
|
|
448
|
+
private readonly config;
|
|
449
|
+
constructor(config: ClientConfig);
|
|
450
|
+
/**
|
|
451
|
+
* const res = await g.ai.chat({ model: "gpt-4o-mini", messages, stream: true });
|
|
452
|
+
* for await (const chunk of res.body) { … }
|
|
453
|
+
*
|
|
454
|
+
* Browser sessions only — a server key is refused (`scope_denied`, 403).
|
|
455
|
+
* Refusals, all `GemmeinError`: `session_required` (401) ·
|
|
456
|
+
* `credits_exhausted` (402 — the message carries the balance; show your
|
|
457
|
+
* own "buy more" door, which is a product checkout) · `ai_not_configured`
|
|
458
|
+
* (409 — the owner has set no key) · `provider_required` (400) ·
|
|
459
|
+
* `model_not_allowed` (403 — the owner's models list) · `ai_capped`
|
|
460
|
+
* (429 — 20 calls a minute per person; `err.resetAt`) ·
|
|
461
|
+
* `payload_too_large` (413 — 256 KB) · `provider_unreachable` (502,
|
|
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.
|
|
473
|
+
*/
|
|
474
|
+
chat(body: Record<string, unknown>, options?: AiChatOptions): Promise<Response>;
|
|
475
|
+
/**
|
|
476
|
+
* The non-streaming convenience: one call, one string. Pass a body that
|
|
477
|
+
* does NOT stream (`stream` unset or false); the provider's JSON answer is
|
|
478
|
+
* read whole and the text is lifted out per provider — OpenAI
|
|
479
|
+
* `choices[0].message.content`, Anthropic `content[].text` joined,
|
|
480
|
+
* Google `candidates[0].content.parts[].text` joined. An answer with no
|
|
481
|
+
* text in any of those places throws `invalid_response` (status 0). A
|
|
482
|
+
* provider's own non-2xx throws `provider_error` with the provider's
|
|
483
|
+
* status and the provider's message (the owner's key masked to its
|
|
484
|
+
* hint if the provider echoed it).
|
|
485
|
+
*
|
|
486
|
+
* const answer = await g.ai.text({ model: "claude-sonnet-4-5", max_tokens: 400, messages });
|
|
487
|
+
*/
|
|
488
|
+
text(body: Record<string, unknown>, options?: AiChatOptions): Promise<string>;
|
|
489
|
+
}
|
|
385
490
|
export declare class StorageClient {
|
|
386
491
|
private readonly config;
|
|
387
492
|
constructor(config: ClientConfig);
|
|
@@ -562,9 +667,11 @@ export type Grant = {
|
|
|
562
667
|
/**
|
|
563
668
|
* What a person holds RIGHT NOW — never what they pay. `access` is the
|
|
564
669
|
* union of live grants' keys; `grants` lists those live grants (revoked
|
|
565
|
-
* and expired ones are gone, not flagged). `credits` is
|
|
566
|
-
*
|
|
567
|
-
*
|
|
670
|
+
* and expired ones are gone, not flagged). `credits` is `{ balance }` —
|
|
671
|
+
* filled from engine 0.8.0 (W9.3): the person's credit balance, floored
|
|
672
|
+
* at 0, the same number `g.credits.balance()` answers and the one the AI
|
|
673
|
+
* route and `spendCredits` refuse against. An engine before 0.8.0 answers
|
|
674
|
+
* `null` here — read it as "not carried", never as zero.
|
|
568
675
|
*/
|
|
569
676
|
export type Holdings = {
|
|
570
677
|
access: string[];
|
|
@@ -760,6 +867,38 @@ export declare class GemmeinServer {
|
|
|
760
867
|
grant: Grant;
|
|
761
868
|
holdings: Holdings;
|
|
762
869
|
}>;
|
|
870
|
+
/**
|
|
871
|
+
* W9.3 — spend a person's credits from YOUR server, with a reason:
|
|
872
|
+
*
|
|
873
|
+
* const { balance } = await g.spendCredits(personId, {
|
|
874
|
+
* amount: 5, // default 1; 1..10,000 per call
|
|
875
|
+
* reason: "render:4k", // what the owner reads in the ledger
|
|
876
|
+
* key: `render:${jobId}`, // your retry key — a repeat is deduped
|
|
877
|
+
* });
|
|
878
|
+
* // { ok: true, spent: 5, deduped: false, balance: { before: 20, after: 15 },
|
|
879
|
+
* // event: { id: "cev_…", reason: "render:4k", actor: "<your key's name>" } }
|
|
880
|
+
*
|
|
881
|
+
* ONE conditional update, floored at 0: the spend succeeds whole or not at
|
|
882
|
+
* all, and `402 credits_exhausted` says "this person has {balance} credits
|
|
883
|
+
* — the spend needs {amount}". Pass `key` when the caller can retry — the
|
|
884
|
+
* same key answers the first spend again with `deduped: true`, `spent: 0`,
|
|
885
|
+
* the same `event` and the balance untouched. The key is scoped to the
|
|
886
|
+
* person: one order id reused for two people charges both. Credits come
|
|
887
|
+
* from a pack they bought, a comp in the back office, or a relay; a key
|
|
888
|
+
* cannot mint them.
|
|
889
|
+
*
|
|
890
|
+
* Needs the key's "Spend a person's credits" box ticked by your human.
|
|
891
|
+
* Refusals: `capability_required` (403 — "this key can't spend credits —
|
|
892
|
+
* mint a key with 'Spend a person's credits' ticked") ·
|
|
893
|
+
* `credits_exhausted` (402) · `person_not_found` (404) · `invalid_amount` /
|
|
894
|
+
* `invalid_reason` / `invalid_key` (400) · `dedupe_conflict` (409 — the
|
|
895
|
+
* key already names a different movement).
|
|
896
|
+
*/
|
|
897
|
+
spendCredits(personId: string, input: {
|
|
898
|
+
amount?: number;
|
|
899
|
+
reason: string;
|
|
900
|
+
key?: string;
|
|
901
|
+
}): Promise<SpendCreditsResult>;
|
|
763
902
|
/**
|
|
764
903
|
* End one grant — the reversibility law in one call:
|
|
765
904
|
*
|