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