@gemmein/sdk 0.7.0 → 0.9.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/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.7.0"; // synced from package.json — do not edit by hand
12
+ exports.SDK_VERSION = "0.9.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
@@ -391,14 +391,24 @@ class CreditsClient {
391
391
  }
392
392
  exports.CreditsClient = CreditsClient;
393
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 one credit,
398
- * adds the owner's key, forwards, and passes status and bytes back.
399
- * Response headers: `x-gemmein-credits-remaining` on every answer that
400
- * passed the spend; `x-gemmein-credit: refunded` when the provider failed
401
- * before its first byte.
394
+ * The AI route. The primary path is a NAMED TOOL defined on the server:
395
+ * `run(name, inputs)` sends a name and inputs, the server composes the
396
+ * provider request from the tool's own instructions and template (never
397
+ * the browser), gates it, spends the tool's credits and streams the
398
+ * answer back; `runText` is the same call collected to one string;
399
+ * `calls()` is the signed-in person's own history. `chat` is the RAW
400
+ * call: it takes the provider's own request body exactly what you would
401
+ * POST to OpenAI's /v1/chat/completions, Anthropic's /v1/messages or
402
+ * Google's generateContent — and answers with the fetch `Response`
403
+ * untouched, streaming intact (SSE stays SSE). Raw calls are off by
404
+ * default for every provider key (`raw_calls_off`, 403) until the founder
405
+ * switches them on for that key on the AI tools page. Gemmein spends a
406
+ * credit, adds the owner's key, forwards, and passes status and bytes
407
+ * back. Pass `tool` (W9.3b) to a raw call to price and gate it as a named
408
+ * tool instead of the implicit default (one credit, any allowed model, no
409
+ * gate). Response headers: `x-gemmein-credits-remaining` on every answer
410
+ * that passed the spend; `x-gemmein-credit: refunded` when the provider
411
+ * failed before its first byte.
402
412
  */
403
413
  class AiClient {
404
414
  constructor(config) {
@@ -409,23 +419,34 @@ class AiClient {
409
419
  * for await (const chunk of res.body) { … }
410
420
  *
411
421
  * Browser sessions only — a server key is refused (`scope_denied`, 403).
412
- * Refusals, all `GemmeinError`: `session_required` (401) ·
422
+ * Refusals, all `GemmeinError`: `raw_calls_off` (403 — raw calls are off
423
+ * for this provider until the founder switches them on for its key on
424
+ * the AI tools page; call a named tool with `run` instead) ·
425
+ * `session_required` (401) ·
413
426
  * `credits_exhausted` (402 — the message carries the balance; show your
414
427
  * own "buy more" door, which is a product checkout) · `ai_not_configured`
415
428
  * (409 — the owner has set no key) · `provider_required` (400) ·
416
429
  * `model_not_allowed` (403 — the owner's models list) · `ai_capped`
417
430
  * (429 — 20 calls a minute per person; `err.resetAt`) ·
418
431
  * `payload_too_large` (413 — 256 KB) · `provider_unreachable` (502,
419
- * before the first byte, refunded). Those are GEMMEIN's refusals. The
420
- * PROVIDER's own answer 2xx or not is returned as it came: an answer
421
- * that carries `x-gemmein-credits-remaining` (or `x-gemmein-credit`)
422
- * passed the spend, so its status and body are the provider's; read
423
- * `res.ok` / `res.status` yourself (a provider 4xx before the first byte
424
- * is refunded, header `x-gemmein-credit: refunded`).
432
+ * before the first byte, refunded). W9.3b, `tool` only: `unknown_tool`
433
+ * (404no tool by that name in this environment) · `tool_disabled`
434
+ * (403) · `entitlement_required` (403 — the message names the plan or
435
+ * product that unlocks it) · `model_pinned` (403 the tool's model is
436
+ * fixed; leave `model` out of the body). Those are GEMMEIN's refusals.
437
+ * The PROVIDER's own answer — 2xx or not — is returned as it came: an
438
+ * answer that carries `x-gemmein-credits-remaining` passed the spend, so
439
+ * its status and body are the provider's; read `res.ok` / `res.status`
440
+ * yourself (a provider 4xx before the first byte is refunded, header
441
+ * `x-gemmein-credit: refunded`). `x-gemmein-tool` names the tool; absent
442
+ * on the implicit default.
425
443
  */
426
444
  async chat(body, options = {}) {
427
445
  const payload = options.provider ? { provider: options.provider, ...body } : body;
428
- const response = await fetch(new URL("/ai/chat", this.config.apiUrl), {
446
+ const url = new URL("/ai/chat", this.config.apiUrl);
447
+ if (options.tool)
448
+ url.searchParams.set("tool", options.tool);
449
+ const response = await fetch(url, {
429
450
  method: "POST",
430
451
  body: JSON.stringify(payload),
431
452
  headers: await runtimeHeaders(this.config, { "content-type": "application/json" }),
@@ -447,6 +468,91 @@ class AiClient {
447
468
  }
448
469
  return response;
449
470
  }
471
+ /**
472
+ * W9.6: run a named tool with INPUTS — the server composes the provider
473
+ * request from the tool's own instructions and template (never the
474
+ * browser), gates it, spends its credits and streams the answer back.
475
+ * The answer is the provider's own shape for the tool's provider (SSE
476
+ * when `stream`), so read it as you would `chat()`'s.
477
+ *
478
+ * const res = await g.ai.run("summarise", { text }, { stream: true });
479
+ *
480
+ * Browser sessions only — a server key is refused (`scope_denied`, 403).
481
+ * Refusals, all `GemmeinError`: `session_required` (401 — sign in
482
+ * first) · `ai_capped` (429 — 20 calls a minute per person;
483
+ * `err.resetAt`) · `unknown_tool` (404 — no tool by that name in this
484
+ * environment) · `tool_disabled` (403 — the owner switched it off) ·
485
+ * `entitlement_required` (403 — the message names the plan or product
486
+ * it needs) · `payload_too_large` (413 — inputs over 64 KB) ·
487
+ * `invalid_body` (400 — the body must be a JSON object
488
+ * `{ inputs, stream? }`) · `invalid_inputs` (400 — the message names the
489
+ * input and the rule) · `tool_incomplete` (409 — the tool composes
490
+ * nothing; a founder's fix) · `ai_not_configured` (409 — the tool's
491
+ * provider has no key set; the owner pastes one) · `credits_exhausted`
492
+ * (402 — the message names the tool, its price and the balance) ·
493
+ * `provider_unreachable` (502 — no answer before the first byte; the
494
+ * tool's credits are refunded, header `x-gemmein-credit: refunded`).
495
+ * The provider's own answer — 2xx or not — is returned as it came; read
496
+ * `res.ok` yourself. `x-gemmein-tool` names the tool.
497
+ */
498
+ async run(tool, inputs = {}, options = {}) {
499
+ const url = new URL(`/ai/run/${encodeURIComponent(tool)}`, this.config.apiUrl);
500
+ const response = await fetch(url, {
501
+ method: "POST",
502
+ body: JSON.stringify({ inputs, ...(options.stream ? { stream: true } : {}) }),
503
+ headers: await runtimeHeaders(this.config, { "content-type": "application/json" }),
504
+ ...(options.signal ? { signal: options.signal } : {}),
505
+ });
506
+ if (!response.ok) {
507
+ if (isForwardedAnswer(response)) {
508
+ const peek = (await response.clone().json().catch(() => null));
509
+ if (peek?.code !== "provider_unreachable")
510
+ return response;
511
+ }
512
+ const errorBody = await readErrorBody(response);
513
+ if (errorBody.code === "auth_expired")
514
+ await this.config.tokenStore.clear();
515
+ throw new GemmeinError({ status: response.status, ...errorBody });
516
+ }
517
+ return response;
518
+ }
519
+ /**
520
+ * W9.6: `run()` without a stream, as one string — the text lifted out
521
+ * of the tool's provider's answer (the same readers `text()` uses).
522
+ * `run()`'s refusals, plus: a provider's own non-2xx throws
523
+ * `provider_error` with the provider's status and message; an answer
524
+ * with no text to lift out throws `invalid_response` (status 0).
525
+ *
526
+ * const summary = await g.ai.runText("summarise", { text });
527
+ */
528
+ async runText(tool, inputs = {}, options = {}) {
529
+ const response = await this.run(tool, inputs, options);
530
+ if (!response.ok) {
531
+ throw new GemmeinError({ status: response.status, code: "provider_error", message: await providerErrorMessage(response) });
532
+ }
533
+ const data = (await response.json());
534
+ const text = extractAiText(data);
535
+ if (text === null) {
536
+ throw new GemmeinError({ status: 0, code: "invalid_response", message: "the provider answered without any text — use g.ai.run() with { stream: true } and read the stream" });
537
+ }
538
+ return text;
539
+ }
540
+ /**
541
+ * W9.6 §16: the signed-in person's OWN AI calls, newest first — what they
542
+ * ran, when, what it cost, how it ended; the prompt and answer only where
543
+ * the tool keeps them. Session required.
544
+ *
545
+ * const { calls, nextCursor } = await g.ai.calls();
546
+ */
547
+ async calls(options = {}) {
548
+ const params = new URLSearchParams();
549
+ if (options.limit)
550
+ params.set("limit", String(options.limit));
551
+ if (options.before)
552
+ params.set("before", options.before);
553
+ const query = params.toString();
554
+ return runtimeRequest(this.config, `/auth/ai-calls${query ? `?${query}` : ""}`);
555
+ }
450
556
  /**
451
557
  * The non-streaming convenience: one call, one string. Pass a body that
452
558
  * does NOT stream (`stream` unset or false); the provider's JSON answer is
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.7.0";
123
+ export declare const SDK_VERSION = "0.9.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.7.0";
129
+ export declare const CLIENT_INFO = "gemmein-sdk/0.9.0";
130
130
  export declare class GemmeinError extends Error {
131
131
  readonly status: number;
132
132
  readonly code: string;
@@ -387,10 +387,37 @@ export declare class AccountClient {
387
387
  delete(): Promise<unknown>;
388
388
  }
389
389
  export type AiProvider = "openai" | "anthropic" | "google";
390
+ /** W9.6 §16: one of the person's own AI calls, as `g.ai.calls()` lists them. */
391
+ export type AiCallRecord = {
392
+ id: string;
393
+ tool: string;
394
+ kind: string;
395
+ provider: string;
396
+ model: string | null;
397
+ tokensIn: number | null;
398
+ tokensOut: number | null;
399
+ credits: number;
400
+ outcome: "ok" | "refused" | "provider_error" | "unreachable" | "client_closed" | "stream_ended";
401
+ refusalCode: string | null;
402
+ latencyMs: number | null;
403
+ prompt: string | null;
404
+ answer: string | null;
405
+ createdAt: string;
406
+ };
390
407
  export type AiChatOptions = {
391
408
  /** Which configured provider answers. Optional when exactly one key is
392
- * set; refused `provider_required` (400) when it is ambiguous. */
409
+ * set; refused `provider_required` (400) when it is ambiguous. Refused
410
+ * `invalid_body` when `tool` is also set and disagrees with the named
411
+ * tool's own provider — leave `provider` out when you pass `tool`. */
393
412
  provider?: AiProvider;
413
+ /** W9.3b: a named AI tool (owner-configured in the console — credits,
414
+ * gate and provider/model are the tool's, not this call's). Sent as
415
+ * `?tool=`, never in the body. Omitted → the implicit default tool: one
416
+ * credit, any allowed model, no gate. With or without `tool`, `chat` is
417
+ * a RAW call — off by default (`raw_calls_off`, 403) until the founder
418
+ * switches raw calls on for that provider's key on the AI tools page;
419
+ * the normal path to a named tool is `run(name, inputs)`. */
420
+ tool?: string;
394
421
  /** Abort the call — the stream closes; a call that dies mid-stream is
395
422
  * not refunded. */
396
423
  signal?: AbortSignal;
@@ -426,14 +453,24 @@ export declare class CreditsClient {
426
453
  }>;
427
454
  }
428
455
  /**
429
- * The AI route. `chat` takes the provider's own request body exactly what
430
- * you would POST to OpenAI's /v1/chat/completions, Anthropic's /v1/messages
431
- * or Google's generateContent and answers with the fetch `Response`
432
- * untouched, streaming intact (SSE stays SSE). Gemmein spends one credit,
433
- * adds the owner's key, forwards, and passes status and bytes back.
434
- * Response headers: `x-gemmein-credits-remaining` on every answer that
435
- * passed the spend; `x-gemmein-credit: refunded` when the provider failed
436
- * before its first byte.
456
+ * The AI route. The primary path is a NAMED TOOL defined on the server:
457
+ * `run(name, inputs)` sends a name and inputs, the server composes the
458
+ * provider request from the tool's own instructions and template (never
459
+ * the browser), gates it, spends the tool's credits and streams the
460
+ * answer back; `runText` is the same call collected to one string;
461
+ * `calls()` is the signed-in person's own history. `chat` is the RAW
462
+ * call: it takes the provider's own request body exactly what you would
463
+ * POST to OpenAI's /v1/chat/completions, Anthropic's /v1/messages or
464
+ * Google's generateContent — and answers with the fetch `Response`
465
+ * untouched, streaming intact (SSE stays SSE). Raw calls are off by
466
+ * default for every provider key (`raw_calls_off`, 403) until the founder
467
+ * switches them on for that key on the AI tools page. Gemmein spends a
468
+ * credit, adds the owner's key, forwards, and passes status and bytes
469
+ * back. Pass `tool` (W9.3b) to a raw call to price and gate it as a named
470
+ * tool instead of the implicit default (one credit, any allowed model, no
471
+ * gate). Response headers: `x-gemmein-credits-remaining` on every answer
472
+ * that passed the spend; `x-gemmein-credit: refunded` when the provider
473
+ * failed before its first byte.
437
474
  */
438
475
  export declare class AiClient {
439
476
  private readonly config;
@@ -443,21 +480,86 @@ export declare class AiClient {
443
480
  * for await (const chunk of res.body) { … }
444
481
  *
445
482
  * Browser sessions only — a server key is refused (`scope_denied`, 403).
446
- * Refusals, all `GemmeinError`: `session_required` (401) ·
483
+ * Refusals, all `GemmeinError`: `raw_calls_off` (403 — raw calls are off
484
+ * for this provider until the founder switches them on for its key on
485
+ * the AI tools page; call a named tool with `run` instead) ·
486
+ * `session_required` (401) ·
447
487
  * `credits_exhausted` (402 — the message carries the balance; show your
448
488
  * own "buy more" door, which is a product checkout) · `ai_not_configured`
449
489
  * (409 — the owner has set no key) · `provider_required` (400) ·
450
490
  * `model_not_allowed` (403 — the owner's models list) · `ai_capped`
451
491
  * (429 — 20 calls a minute per person; `err.resetAt`) ·
452
492
  * `payload_too_large` (413 — 256 KB) · `provider_unreachable` (502,
453
- * before the first byte, refunded). Those are GEMMEIN's refusals. The
454
- * PROVIDER's own answer 2xx or not is returned as it came: an answer
455
- * that carries `x-gemmein-credits-remaining` (or `x-gemmein-credit`)
456
- * passed the spend, so its status and body are the provider's; read
457
- * `res.ok` / `res.status` yourself (a provider 4xx before the first byte
458
- * is refunded, header `x-gemmein-credit: refunded`).
493
+ * before the first byte, refunded). W9.3b, `tool` only: `unknown_tool`
494
+ * (404no tool by that name in this environment) · `tool_disabled`
495
+ * (403) · `entitlement_required` (403 — the message names the plan or
496
+ * product that unlocks it) · `model_pinned` (403 the tool's model is
497
+ * fixed; leave `model` out of the body). Those are GEMMEIN's refusals.
498
+ * The PROVIDER's own answer — 2xx or not — is returned as it came: an
499
+ * answer that carries `x-gemmein-credits-remaining` passed the spend, so
500
+ * its status and body are the provider's; read `res.ok` / `res.status`
501
+ * yourself (a provider 4xx before the first byte is refunded, header
502
+ * `x-gemmein-credit: refunded`). `x-gemmein-tool` names the tool; absent
503
+ * on the implicit default.
459
504
  */
460
505
  chat(body: Record<string, unknown>, options?: AiChatOptions): Promise<Response>;
506
+ /**
507
+ * W9.6: run a named tool with INPUTS — the server composes the provider
508
+ * request from the tool's own instructions and template (never the
509
+ * browser), gates it, spends its credits and streams the answer back.
510
+ * The answer is the provider's own shape for the tool's provider (SSE
511
+ * when `stream`), so read it as you would `chat()`'s.
512
+ *
513
+ * const res = await g.ai.run("summarise", { text }, { stream: true });
514
+ *
515
+ * Browser sessions only — a server key is refused (`scope_denied`, 403).
516
+ * Refusals, all `GemmeinError`: `session_required` (401 — sign in
517
+ * first) · `ai_capped` (429 — 20 calls a minute per person;
518
+ * `err.resetAt`) · `unknown_tool` (404 — no tool by that name in this
519
+ * environment) · `tool_disabled` (403 — the owner switched it off) ·
520
+ * `entitlement_required` (403 — the message names the plan or product
521
+ * it needs) · `payload_too_large` (413 — inputs over 64 KB) ·
522
+ * `invalid_body` (400 — the body must be a JSON object
523
+ * `{ inputs, stream? }`) · `invalid_inputs` (400 — the message names the
524
+ * input and the rule) · `tool_incomplete` (409 — the tool composes
525
+ * nothing; a founder's fix) · `ai_not_configured` (409 — the tool's
526
+ * provider has no key set; the owner pastes one) · `credits_exhausted`
527
+ * (402 — the message names the tool, its price and the balance) ·
528
+ * `provider_unreachable` (502 — no answer before the first byte; the
529
+ * tool's credits are refunded, header `x-gemmein-credit: refunded`).
530
+ * The provider's own answer — 2xx or not — is returned as it came; read
531
+ * `res.ok` yourself. `x-gemmein-tool` names the tool.
532
+ */
533
+ run(tool: string, inputs?: Record<string, string | number | boolean>, options?: {
534
+ stream?: boolean;
535
+ signal?: AbortSignal;
536
+ }): Promise<Response>;
537
+ /**
538
+ * W9.6: `run()` without a stream, as one string — the text lifted out
539
+ * of the tool's provider's answer (the same readers `text()` uses).
540
+ * `run()`'s refusals, plus: a provider's own non-2xx throws
541
+ * `provider_error` with the provider's status and message; an answer
542
+ * with no text to lift out throws `invalid_response` (status 0).
543
+ *
544
+ * const summary = await g.ai.runText("summarise", { text });
545
+ */
546
+ runText(tool: string, inputs?: Record<string, string | number | boolean>, options?: {
547
+ signal?: AbortSignal;
548
+ }): Promise<string>;
549
+ /**
550
+ * W9.6 §16: the signed-in person's OWN AI calls, newest first — what they
551
+ * ran, when, what it cost, how it ended; the prompt and answer only where
552
+ * the tool keeps them. Session required.
553
+ *
554
+ * const { calls, nextCursor } = await g.ai.calls();
555
+ */
556
+ calls(options?: {
557
+ limit?: number;
558
+ before?: string | null;
559
+ }): Promise<{
560
+ calls: AiCallRecord[];
561
+ nextCursor: string | null;
562
+ }>;
461
563
  /**
462
564
  * The non-streaming convenience: one call, one string. Pass a body that
463
565
  * does NOT stream (`stream` unset or false); the provider's JSON answer is
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.7.0";
123
+ export declare const SDK_VERSION = "0.9.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.7.0";
129
+ export declare const CLIENT_INFO = "gemmein-sdk/0.9.0";
130
130
  export declare class GemmeinError extends Error {
131
131
  readonly status: number;
132
132
  readonly code: string;
@@ -387,10 +387,37 @@ export declare class AccountClient {
387
387
  delete(): Promise<unknown>;
388
388
  }
389
389
  export type AiProvider = "openai" | "anthropic" | "google";
390
+ /** W9.6 §16: one of the person's own AI calls, as `g.ai.calls()` lists them. */
391
+ export type AiCallRecord = {
392
+ id: string;
393
+ tool: string;
394
+ kind: string;
395
+ provider: string;
396
+ model: string | null;
397
+ tokensIn: number | null;
398
+ tokensOut: number | null;
399
+ credits: number;
400
+ outcome: "ok" | "refused" | "provider_error" | "unreachable" | "client_closed" | "stream_ended";
401
+ refusalCode: string | null;
402
+ latencyMs: number | null;
403
+ prompt: string | null;
404
+ answer: string | null;
405
+ createdAt: string;
406
+ };
390
407
  export type AiChatOptions = {
391
408
  /** Which configured provider answers. Optional when exactly one key is
392
- * set; refused `provider_required` (400) when it is ambiguous. */
409
+ * set; refused `provider_required` (400) when it is ambiguous. Refused
410
+ * `invalid_body` when `tool` is also set and disagrees with the named
411
+ * tool's own provider — leave `provider` out when you pass `tool`. */
393
412
  provider?: AiProvider;
413
+ /** W9.3b: a named AI tool (owner-configured in the console — credits,
414
+ * gate and provider/model are the tool's, not this call's). Sent as
415
+ * `?tool=`, never in the body. Omitted → the implicit default tool: one
416
+ * credit, any allowed model, no gate. With or without `tool`, `chat` is
417
+ * a RAW call — off by default (`raw_calls_off`, 403) until the founder
418
+ * switches raw calls on for that provider's key on the AI tools page;
419
+ * the normal path to a named tool is `run(name, inputs)`. */
420
+ tool?: string;
394
421
  /** Abort the call — the stream closes; a call that dies mid-stream is
395
422
  * not refunded. */
396
423
  signal?: AbortSignal;
@@ -426,14 +453,24 @@ export declare class CreditsClient {
426
453
  }>;
427
454
  }
428
455
  /**
429
- * The AI route. `chat` takes the provider's own request body exactly what
430
- * you would POST to OpenAI's /v1/chat/completions, Anthropic's /v1/messages
431
- * or Google's generateContent and answers with the fetch `Response`
432
- * untouched, streaming intact (SSE stays SSE). Gemmein spends one credit,
433
- * adds the owner's key, forwards, and passes status and bytes back.
434
- * Response headers: `x-gemmein-credits-remaining` on every answer that
435
- * passed the spend; `x-gemmein-credit: refunded` when the provider failed
436
- * before its first byte.
456
+ * The AI route. The primary path is a NAMED TOOL defined on the server:
457
+ * `run(name, inputs)` sends a name and inputs, the server composes the
458
+ * provider request from the tool's own instructions and template (never
459
+ * the browser), gates it, spends the tool's credits and streams the
460
+ * answer back; `runText` is the same call collected to one string;
461
+ * `calls()` is the signed-in person's own history. `chat` is the RAW
462
+ * call: it takes the provider's own request body exactly what you would
463
+ * POST to OpenAI's /v1/chat/completions, Anthropic's /v1/messages or
464
+ * Google's generateContent — and answers with the fetch `Response`
465
+ * untouched, streaming intact (SSE stays SSE). Raw calls are off by
466
+ * default for every provider key (`raw_calls_off`, 403) until the founder
467
+ * switches them on for that key on the AI tools page. Gemmein spends a
468
+ * credit, adds the owner's key, forwards, and passes status and bytes
469
+ * back. Pass `tool` (W9.3b) to a raw call to price and gate it as a named
470
+ * tool instead of the implicit default (one credit, any allowed model, no
471
+ * gate). Response headers: `x-gemmein-credits-remaining` on every answer
472
+ * that passed the spend; `x-gemmein-credit: refunded` when the provider
473
+ * failed before its first byte.
437
474
  */
438
475
  export declare class AiClient {
439
476
  private readonly config;
@@ -443,21 +480,86 @@ export declare class AiClient {
443
480
  * for await (const chunk of res.body) { … }
444
481
  *
445
482
  * Browser sessions only — a server key is refused (`scope_denied`, 403).
446
- * Refusals, all `GemmeinError`: `session_required` (401) ·
483
+ * Refusals, all `GemmeinError`: `raw_calls_off` (403 — raw calls are off
484
+ * for this provider until the founder switches them on for its key on
485
+ * the AI tools page; call a named tool with `run` instead) ·
486
+ * `session_required` (401) ·
447
487
  * `credits_exhausted` (402 — the message carries the balance; show your
448
488
  * own "buy more" door, which is a product checkout) · `ai_not_configured`
449
489
  * (409 — the owner has set no key) · `provider_required` (400) ·
450
490
  * `model_not_allowed` (403 — the owner's models list) · `ai_capped`
451
491
  * (429 — 20 calls a minute per person; `err.resetAt`) ·
452
492
  * `payload_too_large` (413 — 256 KB) · `provider_unreachable` (502,
453
- * before the first byte, refunded). Those are GEMMEIN's refusals. The
454
- * PROVIDER's own answer 2xx or not is returned as it came: an answer
455
- * that carries `x-gemmein-credits-remaining` (or `x-gemmein-credit`)
456
- * passed the spend, so its status and body are the provider's; read
457
- * `res.ok` / `res.status` yourself (a provider 4xx before the first byte
458
- * is refunded, header `x-gemmein-credit: refunded`).
493
+ * before the first byte, refunded). W9.3b, `tool` only: `unknown_tool`
494
+ * (404no tool by that name in this environment) · `tool_disabled`
495
+ * (403) · `entitlement_required` (403 — the message names the plan or
496
+ * product that unlocks it) · `model_pinned` (403 the tool's model is
497
+ * fixed; leave `model` out of the body). Those are GEMMEIN's refusals.
498
+ * The PROVIDER's own answer — 2xx or not — is returned as it came: an
499
+ * answer that carries `x-gemmein-credits-remaining` passed the spend, so
500
+ * its status and body are the provider's; read `res.ok` / `res.status`
501
+ * yourself (a provider 4xx before the first byte is refunded, header
502
+ * `x-gemmein-credit: refunded`). `x-gemmein-tool` names the tool; absent
503
+ * on the implicit default.
459
504
  */
460
505
  chat(body: Record<string, unknown>, options?: AiChatOptions): Promise<Response>;
506
+ /**
507
+ * W9.6: run a named tool with INPUTS — the server composes the provider
508
+ * request from the tool's own instructions and template (never the
509
+ * browser), gates it, spends its credits and streams the answer back.
510
+ * The answer is the provider's own shape for the tool's provider (SSE
511
+ * when `stream`), so read it as you would `chat()`'s.
512
+ *
513
+ * const res = await g.ai.run("summarise", { text }, { stream: true });
514
+ *
515
+ * Browser sessions only — a server key is refused (`scope_denied`, 403).
516
+ * Refusals, all `GemmeinError`: `session_required` (401 — sign in
517
+ * first) · `ai_capped` (429 — 20 calls a minute per person;
518
+ * `err.resetAt`) · `unknown_tool` (404 — no tool by that name in this
519
+ * environment) · `tool_disabled` (403 — the owner switched it off) ·
520
+ * `entitlement_required` (403 — the message names the plan or product
521
+ * it needs) · `payload_too_large` (413 — inputs over 64 KB) ·
522
+ * `invalid_body` (400 — the body must be a JSON object
523
+ * `{ inputs, stream? }`) · `invalid_inputs` (400 — the message names the
524
+ * input and the rule) · `tool_incomplete` (409 — the tool composes
525
+ * nothing; a founder's fix) · `ai_not_configured` (409 — the tool's
526
+ * provider has no key set; the owner pastes one) · `credits_exhausted`
527
+ * (402 — the message names the tool, its price and the balance) ·
528
+ * `provider_unreachable` (502 — no answer before the first byte; the
529
+ * tool's credits are refunded, header `x-gemmein-credit: refunded`).
530
+ * The provider's own answer — 2xx or not — is returned as it came; read
531
+ * `res.ok` yourself. `x-gemmein-tool` names the tool.
532
+ */
533
+ run(tool: string, inputs?: Record<string, string | number | boolean>, options?: {
534
+ stream?: boolean;
535
+ signal?: AbortSignal;
536
+ }): Promise<Response>;
537
+ /**
538
+ * W9.6: `run()` without a stream, as one string — the text lifted out
539
+ * of the tool's provider's answer (the same readers `text()` uses).
540
+ * `run()`'s refusals, plus: a provider's own non-2xx throws
541
+ * `provider_error` with the provider's status and message; an answer
542
+ * with no text to lift out throws `invalid_response` (status 0).
543
+ *
544
+ * const summary = await g.ai.runText("summarise", { text });
545
+ */
546
+ runText(tool: string, inputs?: Record<string, string | number | boolean>, options?: {
547
+ signal?: AbortSignal;
548
+ }): Promise<string>;
549
+ /**
550
+ * W9.6 §16: the signed-in person's OWN AI calls, newest first — what they
551
+ * ran, when, what it cost, how it ended; the prompt and answer only where
552
+ * the tool keeps them. Session required.
553
+ *
554
+ * const { calls, nextCursor } = await g.ai.calls();
555
+ */
556
+ calls(options?: {
557
+ limit?: number;
558
+ before?: string | null;
559
+ }): Promise<{
560
+ calls: AiCallRecord[];
561
+ nextCursor: string | null;
562
+ }>;
461
563
  /**
462
564
  * The non-streaming convenience: one call, one string. Pass a body that
463
565
  * does NOT stream (`stream` unset or false); the provider's JSON answer is