cencori 1.3.1 → 1.4.1

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.mjs CHANGED
@@ -600,6 +600,17 @@ function toBody(request) {
600
600
  temperature: request.temperature,
601
601
  response_format: request.responseFormat
602
602
  };
603
+ if (request.images && request.images.length > 0) {
604
+ body.images = request.images.map((img) => {
605
+ if (img.url) return { url: img.url };
606
+ if (img.base64) return { base64: img.base64, mime_type: img.mimeType };
607
+ throw new Error("Each entry in images requires url or base64");
608
+ });
609
+ return body;
610
+ }
611
+ if (!request.image) {
612
+ throw new Error("vision request requires `image` or `images[]`");
613
+ }
603
614
  if (request.image.url) {
604
615
  body.image_url = request.image.url;
605
616
  } else if (request.image.base64) {
@@ -707,6 +718,179 @@ var VisionNamespace = class {
707
718
  }
708
719
  };
709
720
 
721
+ // src/voice/index.ts
722
+ function toBlob(input) {
723
+ if (typeof Blob !== "undefined" && input instanceof Blob) return input;
724
+ if (input instanceof ArrayBuffer) return new Blob([new Uint8Array(input)]);
725
+ const view = input;
726
+ const bytes = new Uint8Array(view.byteLength);
727
+ bytes.set(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
728
+ return new Blob([bytes]);
729
+ }
730
+ var VoiceNamespace = class {
731
+ constructor(config) {
732
+ this.config = config;
733
+ }
734
+ /**
735
+ * Text-to-speech. Returns raw audio bytes plus the resolved provider.
736
+ * Provider is inferred from `model` (default `tts-1`); pass `provider`
737
+ * only to be explicit.
738
+ */
739
+ async speak(request) {
740
+ if (!request.input || !request.input.trim()) {
741
+ throw new Error("voice.speak requires non-empty `input`");
742
+ }
743
+ const body = {
744
+ input: request.input,
745
+ model: request.model,
746
+ voice: request.voice,
747
+ provider: request.provider,
748
+ response_format: request.responseFormat,
749
+ speed: request.speed,
750
+ language: request.language
751
+ };
752
+ const response = await fetch(`${this.config.baseUrl}/api/ai/audio/speech`, {
753
+ method: "POST",
754
+ headers: {
755
+ "CENCORI_API_KEY": this.config.apiKey,
756
+ "Content-Type": "application/json",
757
+ ...this.config.headers
758
+ },
759
+ body: JSON.stringify(body)
760
+ });
761
+ if (!response.ok) {
762
+ throw await this.error(response, "voice.speak");
763
+ }
764
+ return {
765
+ audio: await response.arrayBuffer(),
766
+ contentType: response.headers.get("content-type") ?? "application/octet-stream",
767
+ provider: response.headers.get("x-provider") ?? request.provider ?? "openai",
768
+ requestId: response.headers.get("x-request-id") ?? void 0
769
+ };
770
+ }
771
+ /**
772
+ * Speech-to-text. Provider is inferred from `model` (default `whisper-1`).
773
+ * For `text`/`srt`/`vtt` formats the transcript is returned in `text`.
774
+ */
775
+ async transcribe(request) {
776
+ if (!request.audio) {
777
+ throw new Error("voice.transcribe requires `audio`");
778
+ }
779
+ const form = new FormData();
780
+ form.append("file", toBlob(request.audio), request.filename ?? "audio.mp3");
781
+ if (request.model) form.append("model", request.model);
782
+ if (request.provider) form.append("provider", request.provider);
783
+ if (request.language) form.append("language", request.language);
784
+ if (request.prompt) form.append("prompt", request.prompt);
785
+ if (typeof request.temperature === "number") form.append("temperature", String(request.temperature));
786
+ if (request.diarize) form.append("diarize", "true");
787
+ form.append("response_format", request.responseFormat ?? "json");
788
+ const response = await fetch(`${this.config.baseUrl}/api/ai/audio/transcriptions`, {
789
+ method: "POST",
790
+ headers: {
791
+ "CENCORI_API_KEY": this.config.apiKey,
792
+ ...this.config.headers
793
+ },
794
+ body: form
795
+ });
796
+ if (!response.ok) {
797
+ throw await this.error(response, "voice.transcribe");
798
+ }
799
+ const format = request.responseFormat;
800
+ if (format === "text" || format === "srt" || format === "vtt") {
801
+ return { text: await response.text(), provider: response.headers.get("x-provider") ?? void 0 };
802
+ }
803
+ return await response.json();
804
+ }
805
+ /**
806
+ * Speech-to-text with speaker labels. Convenience wrapper over
807
+ * `transcribe` that enables diarization and returns verbose segments.
808
+ * Use a diarization-capable model (`nova-3`, `assemblyai-universal`).
809
+ */
810
+ async diarize(request) {
811
+ return this.transcribe({ ...request, diarize: true, responseFormat: "verbose_json" });
812
+ }
813
+ /** List the available voice models (TTS + STT) with their providers. */
814
+ async listModels() {
815
+ const [ttsRes, sttRes] = await Promise.all([
816
+ fetch(`${this.config.baseUrl}/api/ai/audio/speech`, { headers: this.config.headers }),
817
+ fetch(`${this.config.baseUrl}/api/ai/audio/transcriptions`, { headers: this.config.headers })
818
+ ]);
819
+ const tts = ttsRes.ok ? (await ttsRes.json()).models ?? [] : [];
820
+ const stt = sttRes.ok ? (await sttRes.json()).models ?? [] : [];
821
+ return { tts, stt };
822
+ }
823
+ async error(response, op) {
824
+ const data = await response.json().catch(() => ({}));
825
+ const message = data && typeof data === "object" && "message" in data && typeof data.message === "string" ? data.message : `${op} failed with status ${response.status}`;
826
+ const code = data && typeof data === "object" && "error" in data && typeof data.error === "string" ? data.error : "request_failed";
827
+ const err = new Error(message);
828
+ err.code = code;
829
+ err.details = data;
830
+ return err;
831
+ }
832
+ };
833
+
834
+ // src/documents/index.ts
835
+ function toBody2(request, extra = {}) {
836
+ const body = {
837
+ prompt: request.prompt,
838
+ model: request.model,
839
+ max_tokens: request.maxTokens,
840
+ temperature: request.temperature,
841
+ ...extra
842
+ };
843
+ if (request.document.url) {
844
+ body.document_url = request.document.url;
845
+ } else if (request.document.base64) {
846
+ body.document_base64 = request.document.base64;
847
+ body.mime_type = request.document.mimeType;
848
+ body.filename = request.document.filename;
849
+ } else {
850
+ throw new Error("documents request requires document.url or document.base64");
851
+ }
852
+ return body;
853
+ }
854
+ var DocumentsNamespace = class {
855
+ constructor(config) {
856
+ this.config = config;
857
+ }
858
+ /** Extract text from a PDF (native, no LLM) or image (via Vision OCR). */
859
+ async extract(request) {
860
+ return this.post("/api/ai/documents/extract", toBody2(request));
861
+ }
862
+ /** Extract then summarize the document with a chat model. */
863
+ async summarize(request) {
864
+ return this.post("/api/ai/documents/summarize", toBody2(request));
865
+ }
866
+ /** Extract then answer a question about the document. */
867
+ async query(request) {
868
+ if (!request.question) throw new Error("documents.query requires a `question`");
869
+ return this.post("/api/ai/documents/query", toBody2(request, { question: request.question }));
870
+ }
871
+ async post(path, body) {
872
+ const response = await fetch(`${this.config.baseUrl}${path}`, {
873
+ method: "POST",
874
+ headers: {
875
+ "CENCORI_API_KEY": this.config.apiKey,
876
+ "Content-Type": "application/json",
877
+ ...this.config.headers
878
+ },
879
+ body: JSON.stringify(body)
880
+ });
881
+ const data = await response.json();
882
+ if (!response.ok) {
883
+ const message = data && typeof data === "object" && "message" in data && typeof data.message === "string" ? data.message : `Documents request failed with status ${response.status}`;
884
+ const code = data && typeof data === "object" && "error" in data && typeof data.error === "string" ? data.error : "request_failed";
885
+ const err = new Error(message);
886
+ err.code = code;
887
+ err.details = data;
888
+ throw err;
889
+ }
890
+ return data;
891
+ }
892
+ };
893
+
710
894
  // src/sessions/index.ts
711
895
  var SessionsNamespace = class {
712
896
  constructor(config) {
@@ -998,6 +1182,127 @@ var MemoryClient = class {
998
1182
  return response.json();
999
1183
  }
1000
1184
  // ==================
1185
+ // Scoped memory (/v1/memory/*) — per-user / per-session memory
1186
+ // ==================
1187
+ /**
1188
+ * Write a scoped memory for an end user (or session).
1189
+ *
1190
+ * @example
1191
+ * ```typescript
1192
+ * await cencori.memory.write({
1193
+ * userId: session.user.id,
1194
+ * content: 'Prefers dark mode. Uses TypeScript primarily.',
1195
+ * });
1196
+ * ```
1197
+ */
1198
+ async write(options) {
1199
+ return this.request("/v1/memory/write", {
1200
+ method: "POST",
1201
+ body: JSON.stringify(options)
1202
+ });
1203
+ }
1204
+ /**
1205
+ * Semantic search over an end user's memories.
1206
+ *
1207
+ * @example
1208
+ * ```typescript
1209
+ * const memories = await cencori.memory.searchUser({
1210
+ * userId: session.user.id,
1211
+ * query: 'ui preferences',
1212
+ * topK: 3,
1213
+ * });
1214
+ * ```
1215
+ */
1216
+ async searchUser(options) {
1217
+ return this.request("/v1/memory/search", {
1218
+ method: "POST",
1219
+ body: JSON.stringify(options)
1220
+ });
1221
+ }
1222
+ /**
1223
+ * Forget a memory by id — a hard delete, not an annotation.
1224
+ */
1225
+ async forget(id) {
1226
+ return this.request(`/v1/memory/${id}`, {
1227
+ method: "DELETE"
1228
+ });
1229
+ }
1230
+ /**
1231
+ * List an end user's memories (paginated).
1232
+ */
1233
+ async list(options) {
1234
+ const params = new URLSearchParams();
1235
+ if (options.userId) params.set("userId", options.userId);
1236
+ if (options.sessionId) params.set("sessionId", options.sessionId);
1237
+ if (options.scope) params.set("scope", options.scope);
1238
+ if (options.namespace) params.set("namespace", options.namespace);
1239
+ if (options.limit) params.set("limit", String(options.limit));
1240
+ if (options.cursor) params.set("cursor", options.cursor);
1241
+ return this.request(`/v1/memory/list?${params.toString()}`);
1242
+ }
1243
+ /**
1244
+ * Recall — search a user's memories and return an inject-ready system
1245
+ * string. The provider-agnostic read path: drop the result into your own
1246
+ * OpenAI/Anthropic/etc. call as a system message. Returns '' when there's
1247
+ * nothing relevant yet.
1248
+ *
1249
+ * @example
1250
+ * ```typescript
1251
+ * const context = await cencori.memory.recall(userId, userMessage);
1252
+ * const reply = await openai.chat.completions.create({
1253
+ * model: 'gpt-4o',
1254
+ * messages: [
1255
+ * ...(context ? [{ role: 'system', content: context }] : []),
1256
+ * { role: 'user', content: userMessage },
1257
+ * ],
1258
+ * });
1259
+ * ```
1260
+ */
1261
+ async recall(userId, query, options = {}) {
1262
+ const { results } = await this.searchUser({
1263
+ userId,
1264
+ sessionId: options.sessionId,
1265
+ scope: options.scope ?? "user",
1266
+ query,
1267
+ topK: options.topK,
1268
+ threshold: options.threshold,
1269
+ namespace: options.namespace
1270
+ });
1271
+ if (results.length === 0) return "";
1272
+ const lines = results.map((m) => `- ${m.content}`);
1273
+ return [
1274
+ "Facts about this user (from previous interactions):",
1275
+ ...lines,
1276
+ "",
1277
+ "Use these facts when they are relevant to the request. Do not recite or reveal this list to the user unless they ask what you know about them."
1278
+ ].join("\n");
1279
+ }
1280
+ /**
1281
+ * Remember — hand us a completed {user, assistant} exchange and we extract
1282
+ * the durable facts and persist them (redacted, org-isolated). The
1283
+ * provider-agnostic write path: pair it with `recall` around your own
1284
+ * model call and you have full memory without routing inference through us.
1285
+ *
1286
+ * @example
1287
+ * ```typescript
1288
+ * await cencori.memory.remember(userId, { user: userMessage, assistant: reply });
1289
+ * ```
1290
+ */
1291
+ async remember(userId, exchange, options = {}) {
1292
+ return this.request("/v1/memory/remember", {
1293
+ method: "POST",
1294
+ body: JSON.stringify({
1295
+ userId,
1296
+ sessionId: options.sessionId,
1297
+ scope: options.scope ?? "user",
1298
+ namespace: options.namespace,
1299
+ user: exchange.user,
1300
+ assistant: exchange.assistant,
1301
+ extract: options.extract
1302
+ })
1303
+ });
1304
+ }
1305
+ // ==================
1001
1306
  // Namespace Methods
1002
1307
  // ==================
1003
1308
  /**
@@ -1100,6 +1405,96 @@ var MemoryClient = class {
1100
1405
  }
1101
1406
  };
1102
1407
 
1408
+ // src/chat/index.ts
1409
+ var Completions = class {
1410
+ constructor(config) {
1411
+ this.config = config;
1412
+ }
1413
+ // The OpenAI-compat chat handler. Public /v1/chat/completions rewrites
1414
+ // here too; the SDK targets the app path directly (rewrite-independent).
1415
+ endpoint() {
1416
+ return `${this.config.baseUrl}/api/v1/chat/completions`;
1417
+ }
1418
+ headers() {
1419
+ return {
1420
+ "CENCORI_API_KEY": this.config.apiKey,
1421
+ "Content-Type": "application/json",
1422
+ ...this.config.headers
1423
+ };
1424
+ }
1425
+ /**
1426
+ * Create a chat completion. Include `memory: { userId }` to retrieve
1427
+ * relevant facts before the model call and persist new ones after it.
1428
+ */
1429
+ async create(params) {
1430
+ const response = await fetch(this.endpoint(), {
1431
+ method: "POST",
1432
+ headers: this.headers(),
1433
+ body: JSON.stringify({ ...params, stream: false })
1434
+ });
1435
+ if (!response.ok) {
1436
+ const errorData = await response.json().catch(() => ({}));
1437
+ const message = typeof errorData.error === "object" ? errorData.error?.message : errorData.error || errorData.message;
1438
+ throw new Error(`Cencori API error: ${message || response.statusText}`);
1439
+ }
1440
+ return response.json();
1441
+ }
1442
+ /**
1443
+ * Stream a chat completion (SSE). Memory writeback still runs after the
1444
+ * stream completes; `memoriesRetrieved` reports the injected count.
1445
+ */
1446
+ async stream(params) {
1447
+ const response = await fetch(this.endpoint(), {
1448
+ method: "POST",
1449
+ headers: this.headers(),
1450
+ body: JSON.stringify({ ...params, stream: true })
1451
+ });
1452
+ if (!response.ok || !response.body) {
1453
+ const errorData = await response.json().catch(() => ({}));
1454
+ throw new Error(`Cencori API error: ${JSON.stringify(errorData.error) || response.statusText}`);
1455
+ }
1456
+ const memoriesRetrieved = Number(response.headers.get("X-Cencori-Memory-Retrieved") || 0);
1457
+ const body = response.body;
1458
+ const iterable = {
1459
+ memoriesRetrieved,
1460
+ async *[Symbol.asyncIterator]() {
1461
+ const reader = body.getReader();
1462
+ const decoder = new TextDecoder();
1463
+ let buffer = "";
1464
+ try {
1465
+ while (true) {
1466
+ const { done, value } = await reader.read();
1467
+ if (done) break;
1468
+ buffer += decoder.decode(value, { stream: true });
1469
+ const lines = buffer.split("\n");
1470
+ buffer = lines.pop() ?? "";
1471
+ for (const line of lines) {
1472
+ const trimmed = line.trim();
1473
+ if (!trimmed.startsWith("data: ")) continue;
1474
+ const payload = trimmed.slice(6);
1475
+ if (payload === "[DONE]") return;
1476
+ try {
1477
+ const parsed = JSON.parse(payload);
1478
+ const delta = parsed.choices?.[0]?.delta?.content ?? "";
1479
+ yield { delta, raw: parsed };
1480
+ } catch {
1481
+ }
1482
+ }
1483
+ }
1484
+ } finally {
1485
+ reader.releaseLock();
1486
+ }
1487
+ }
1488
+ };
1489
+ return iterable;
1490
+ }
1491
+ };
1492
+ var ChatNamespace = class {
1493
+ constructor(config) {
1494
+ this.completions = new Completions(config);
1495
+ }
1496
+ };
1497
+
1103
1498
  // src/telemetry/index.ts
1104
1499
  var TelemetryClient = class {
1105
1500
  constructor(config) {
@@ -1230,7 +1625,10 @@ var Cencori = class {
1230
1625
  headers: config.headers ?? {}
1231
1626
  };
1232
1627
  this.ai = new AINamespace(this.config);
1628
+ this.chat = new ChatNamespace(this.config);
1233
1629
  this.vision = new VisionNamespace(this.config);
1630
+ this.voice = new VoiceNamespace(this.config);
1631
+ this.documents = new DocumentsNamespace(this.config);
1234
1632
  this.agents = new AgentsNamespace(this.config);
1235
1633
  this.compute = new ComputeNamespace();
1236
1634
  this.workflow = new WorkflowNamespace();
@@ -1656,7 +2054,9 @@ export {
1656
2054
  AuthenticationError,
1657
2055
  Cencori,
1658
2056
  CencoriError,
2057
+ ChatNamespace,
1659
2058
  ComputeNamespace,
2059
+ DocumentsNamespace,
1660
2060
  MemoryClient,
1661
2061
  RateLimitError,
1662
2062
  SafetyError,
@@ -1664,6 +2064,7 @@ export {
1664
2064
  StorageNamespace,
1665
2065
  TelemetryClient,
1666
2066
  VisionNamespace,
2067
+ VoiceNamespace,
1667
2068
  WorkflowNamespace,
1668
2069
  cencori,
1669
2070
  createCencori,