@k2b/nessi 0.11.0 → 0.12.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/README.md CHANGED
@@ -265,11 +265,62 @@ for await (const event of provider.stream({ messages })) {
265
265
  }
266
266
  ```
267
267
 
268
+ ## Audio transcription
269
+
270
+ Use `openAICompatibleTranscription()` to upload an audio file to a service that
271
+ implements the OpenAI-compatible `/audio/transcriptions` endpoint. Configure
272
+ the service URL, API key and transcription model explicitly:
273
+
274
+ ```ts
275
+ import { openAICompatibleTranscription } from "@k2b/nessi/ai";
276
+
277
+ const speech = openAICompatibleTranscription("whisper-large-v3", {
278
+ baseURL: "https://api.scaleway.ai/v1",
279
+ apiKey: process.env.SCW_SECRET_KEY,
280
+ });
281
+
282
+ const result = await speech.transcribe({
283
+ file: Bun.file("./aufnahme.mp3"),
284
+ filename: "aufnahme.mp3",
285
+ language: "de",
286
+ signal: AbortSignal.timeout(120_000),
287
+ });
288
+
289
+ console.log(result.text);
290
+ ```
291
+
292
+ For OpenAI, set `baseURL: "https://api.openai.com/v1"`, use your OpenAI key and
293
+ a supported transcription model such as `whisper-1`. Local compatible services
294
+ can omit `apiKey`. No environment variable is read automatically. Optional
295
+ `headers` support gateways; `apiKey` overrides their Authorization header.
296
+
297
+ `file` accepts a `Blob`, `File` or `Bun.file()`. Use `filename` to supply an
298
+ extension for an unnamed Blob or override the filename. Automatically derived
299
+ filenames omit local directories. Omit `language`
300
+ for automatic detection. Optional `prompt` supplies vocabulary or context
301
+ when supported by the model. File formats, size limits and optional parameter
302
+ support depend on the service; Nessi does not convert or split audio.
303
+
304
+ The result is `{ text: string }`, including an empty string for an empty
305
+ transcript. HTTP, connection and malformed-response errors reject the promise.
306
+ Pass `signal` for cancellation or a timeout; cancellation preserves the signal's
307
+ reason. There are no automatic retries or default timeout.
308
+
309
+ Transcription uses its own `TranscriptionProvider` contract with `name`, `model`
310
+ and `transcribe(request)`. Custom adapters can implement that interface for other
311
+ protocols. It is separate from the chat provider passed to `nessi()`; pass the
312
+ resulting text into the agent when needed. Streaming transcription, timestamps
313
+ and speaker identification are not exposed.
314
+
315
+ The example follows [Scaleway's audio API documentation](https://www.scaleway.com/en/docs/generative-apis/how-to/query-audio-models/).
316
+ Keep API keys on the server when integrating a browser application.
317
+
268
318
  ## Focused provider imports
269
319
 
270
320
  ```ts
271
321
  import { anthropic } from "@k2b/nessi/ai/providers/anthropic";
272
322
  import { openai } from "@k2b/nessi/ai/providers/openai";
323
+ import { openAICompatibleTranscription } from "@k2b/nessi/ai/providers/openai-compatible-transcription";
273
324
  ```
274
325
 
275
326
  ## Features
@@ -290,6 +341,7 @@ import { openai } from "@k2b/nessi/ai/providers/openai";
290
341
  - Standalone `compact()` loop with `loop_start`, `compaction_start`, `compaction_end`, `issue`, and `loop_end` events
291
342
  - Optional token-credit budgeting
292
343
  - Provider adapters with shared `complete()` and `stream()` APIs
344
+ - Audio transcription through configurable OpenAI-compatible services
293
345
  - Native adapters for OpenAI, OpenRouter, vLLM, Ollama, Anthropic, Mistral, and Gemini
294
346
 
295
347
  ## Package layout
@@ -299,7 +351,7 @@ import { openai } from "@k2b/nessi/ai/providers/openai";
299
351
  Agent loop, structured task helper, tools, stores, compaction, shared types
300
352
 
301
353
  @k2b/nessi/ai
302
- Provider factories, provider types, complete(), stream(), responseFormat
354
+ Provider factories, provider types, complete(), stream(), responseFormat, transcribe()
303
355
 
304
356
  @k2b/nessi/ai/providers/*
305
357
  Focused provider entrypoints
package/ai/index.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  export { completeFromStream } from "./complete-from-stream.js";
2
+ export { openAICompatibleTranscription } from "./providers/openai-compatible-transcription.js";
3
+ export type { OpenAICompatibleTranscriptionOptions } from "./providers/openai-compatible-transcription.js";
4
+ export type { TranscriptionProvider, TranscriptionRequest, TranscriptionResult } from "./transcription.js";
2
5
  export { openAICompatible } from "./providers/openai-compatible.js";
3
6
  export { openai } from "./providers/openai.js";
4
7
  export { openrouter } from "./providers/openrouter.js";
package/ai/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { completeFromStream } from "./complete-from-stream.js";
2
+ export { openAICompatibleTranscription } from "./providers/openai-compatible-transcription.js";
2
3
  export { openAICompatible } from "./providers/openai-compatible.js";
3
4
  export { openai } from "./providers/openai.js";
4
5
  export { openrouter } from "./providers/openrouter.js";
@@ -0,0 +1,8 @@
1
+ import type { TranscriptionProvider } from "../transcription.js";
2
+ export type OpenAICompatibleTranscriptionOptions = {
3
+ baseURL: string;
4
+ apiKey?: string;
5
+ headers?: Record<string, string>;
6
+ name?: string;
7
+ };
8
+ export declare const openAICompatibleTranscription: (model: string, options: OpenAICompatibleTranscriptionOptions) => TranscriptionProvider;
@@ -0,0 +1,60 @@
1
+ import { formatConnectionError, normalizeHttpError } from "../shared/errors.js";
2
+ export const openAICompatibleTranscription = (model, options) => {
3
+ const baseURL = options.baseURL.replace(/\/+$/, "");
4
+ const name = options.name ?? "openai-compatible";
5
+ const headers = new Headers(options.headers);
6
+ // Fetch must supply the multipart boundary, including with custom headers.
7
+ headers.delete("Content-Type");
8
+ if (options.apiKey)
9
+ headers.set("Authorization", `Bearer ${options.apiKey}`);
10
+ return {
11
+ name,
12
+ model,
13
+ async transcribe(request) {
14
+ request.signal?.throwIfAborted();
15
+ const body = new FormData();
16
+ // Bun.file().name can contain a local path; only upload the basename.
17
+ const fileName = "name" in request.file && typeof request.file.name === "string"
18
+ ? request.file.name.split(/[\\/]/).at(-1)
19
+ : undefined;
20
+ const filename = request.filename ?? fileName;
21
+ if (filename !== undefined)
22
+ body.append("file", request.file, filename);
23
+ else
24
+ body.append("file", request.file);
25
+ body.append("model", model);
26
+ body.append("response_format", "json");
27
+ if (request.language !== undefined)
28
+ body.append("language", request.language);
29
+ if (request.prompt !== undefined)
30
+ body.append("prompt", request.prompt);
31
+ const response = await fetch(`${baseURL}/audio/transcriptions`, {
32
+ method: "POST",
33
+ headers,
34
+ body,
35
+ signal: request.signal,
36
+ }).catch((error) => {
37
+ request.signal?.throwIfAborted();
38
+ throw new Error(formatConnectionError(name, error));
39
+ });
40
+ if (!response.ok) {
41
+ const normalized = await normalizeHttpError(name, response);
42
+ request.signal?.throwIfAborted();
43
+ throw new Error(normalized.error);
44
+ }
45
+ const raw = await response.text();
46
+ let payload;
47
+ try {
48
+ payload = JSON.parse(raw);
49
+ }
50
+ catch {
51
+ throw new Error(`${name} returned invalid transcription JSON.`);
52
+ }
53
+ if (typeof payload !== "object" || payload === null
54
+ || !("text" in payload) || typeof payload.text !== "string") {
55
+ throw new Error(`${name} returned a transcription without a string text field.`);
56
+ }
57
+ return { text: payload.text };
58
+ },
59
+ };
60
+ };
@@ -0,0 +1,18 @@
1
+ export type TranscriptionRequest = {
2
+ file: Blob;
3
+ /** Override the file name, or supply one for an unnamed Blob. */
4
+ filename?: string;
5
+ /** ISO-639-1 language code, for example "de". Omit for automatic detection. */
6
+ language?: string;
7
+ /** Optional vocabulary or context hint, subject to model support. */
8
+ prompt?: string;
9
+ signal?: AbortSignal;
10
+ };
11
+ export type TranscriptionResult = {
12
+ text: string;
13
+ };
14
+ export type TranscriptionProvider = {
15
+ name: string;
16
+ model: string;
17
+ transcribe(request: TranscriptionRequest): Promise<TranscriptionResult>;
18
+ };
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@k2b/nessi",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Minimal agent loop and provider adapters for nessi.",
5
5
  "main": "index.js",
6
6
  "module": "index.js",
@@ -16,6 +16,10 @@
16
16
  "import": "./ai/index.js",
17
17
  "types": "./ai/index.d.ts"
18
18
  },
19
+ "./ai/providers/openai-compatible-transcription": {
20
+ "import": "./ai/providers/openai-compatible-transcription.js",
21
+ "types": "./ai/providers/openai-compatible-transcription.d.ts"
22
+ },
19
23
  "./ai/providers/openai-compatible": {
20
24
  "import": "./ai/providers/openai-compatible.js",
21
25
  "types": "./ai/providers/openai-compatible.d.ts"