@effect-uai/mistral 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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +76 -0
  3. package/dist/Mistral.d.mts +48 -0
  4. package/dist/Mistral.d.mts.map +1 -0
  5. package/dist/Mistral.mjs +88 -0
  6. package/dist/Mistral.mjs.map +1 -0
  7. package/dist/MistralRealtimeTranscriber.d.mts +15 -0
  8. package/dist/MistralRealtimeTranscriber.d.mts.map +1 -0
  9. package/dist/MistralRealtimeTranscriber.mjs +34 -0
  10. package/dist/MistralRealtimeTranscriber.mjs.map +1 -0
  11. package/dist/MistralSynthesizer.d.mts +43 -0
  12. package/dist/MistralSynthesizer.d.mts.map +1 -0
  13. package/dist/MistralSynthesizer.mjs +121 -0
  14. package/dist/MistralSynthesizer.mjs.map +1 -0
  15. package/dist/MistralTranscriber.d.mts +44 -0
  16. package/dist/MistralTranscriber.d.mts.map +1 -0
  17. package/dist/MistralTranscriber.mjs +111 -0
  18. package/dist/MistralTranscriber.mjs.map +1 -0
  19. package/dist/audioCodec.d.mts +27 -0
  20. package/dist/audioCodec.d.mts.map +1 -0
  21. package/dist/audioCodec.mjs +82 -0
  22. package/dist/audioCodec.mjs.map +1 -0
  23. package/dist/chunk-CfYAbeIz.mjs +13 -0
  24. package/dist/codec.d.mts +100 -0
  25. package/dist/codec.d.mts.map +1 -0
  26. package/dist/codec.mjs +244 -0
  27. package/dist/codec.mjs.map +1 -0
  28. package/dist/codec.test.d.mts +1 -0
  29. package/dist/codec.test.mjs +13800 -0
  30. package/dist/codec.test.mjs.map +1 -0
  31. package/dist/http.d.mts +9 -0
  32. package/dist/http.d.mts.map +1 -0
  33. package/dist/http.mjs +41 -0
  34. package/dist/http.mjs.map +1 -0
  35. package/dist/index.d.mts +7 -0
  36. package/dist/index.mjs +7 -0
  37. package/dist/magic-string.es-PAZgLX4h.mjs +1013 -0
  38. package/dist/magic-string.es-PAZgLX4h.mjs.map +1 -0
  39. package/dist/models-DnDmylpj.d.mts +22 -0
  40. package/dist/models-DnDmylpj.d.mts.map +1 -0
  41. package/dist/models.d.mts +2 -0
  42. package/dist/models.mjs +1 -0
  43. package/dist/realtimeStt-ClXWXbXX.d.mts +21 -0
  44. package/dist/realtimeStt-ClXWXbXX.d.mts.map +1 -0
  45. package/dist/realtimeStt.d.mts +2 -0
  46. package/dist/realtimeStt.mjs +136 -0
  47. package/dist/realtimeStt.mjs.map +1 -0
  48. package/package.json +82 -0
  49. package/src/Mistral.ts +189 -0
  50. package/src/MistralRealtimeTranscriber.ts +60 -0
  51. package/src/MistralSynthesizer.ts +285 -0
  52. package/src/MistralTranscriber.ts +224 -0
  53. package/src/audioCodec.ts +123 -0
  54. package/src/codec.test.ts +121 -0
  55. package/src/codec.ts +338 -0
  56. package/src/http.ts +46 -0
  57. package/src/index.ts +6 -0
  58. package/src/models.ts +47 -0
  59. package/src/realtimeStt.ts +256 -0
@@ -0,0 +1,111 @@
1
+ import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
2
+ import { httpStatusError, transportFailure } from "./http.mjs";
3
+ import { audioToBlob, defaultFileName } from "./audioCodec.mjs";
4
+ import { Array, Context, Effect, Layer, Schema, Stream } from "effect";
5
+ import { HttpClient, HttpClientRequest } from "effect/unstable/http";
6
+ import * as AiError from "@effect-uai/core/AiError";
7
+ import { Transcriber } from "@effect-uai/core/Transcriber";
8
+ import * as Capabilities from "@effect-uai/core/Capabilities";
9
+ import { isAudioUrl } from "@effect-uai/core/Audio";
10
+ //#region src/MistralTranscriber.ts
11
+ var MistralTranscriber_exports = /* @__PURE__ */ __exportAll({
12
+ MistralTranscriber: () => MistralTranscriber,
13
+ layer: () => layer,
14
+ make: () => make,
15
+ transcribeImpl: () => transcribeImpl
16
+ });
17
+ var MistralTranscriber = class extends Context.Service()("@betalyra/effect-uai/providers/mistral/MistralTranscriber") {};
18
+ const when = (condition, part) => condition ? [part] : [];
19
+ const appendPart = (fd, part) => {
20
+ if (part.length === 3) fd.append(part[0], part[1], part[2]);
21
+ else fd.append(part[0], part[1]);
22
+ return fd;
23
+ };
24
+ const buildFormData = (request) => Effect.gen(function* () {
25
+ yield* Capabilities.warnDroppedWhen(request.prompt, {
26
+ provider: "mistral",
27
+ capability: "prompt",
28
+ field: "prompt",
29
+ reason: "Voxtral transcription has no prose prompt field; use `biasingTerms` for vocabulary hints."
30
+ });
31
+ const parts = [
32
+ isAudioUrl(request.audio) ? ["file_url", request.audio.url] : yield* audioToBlob(request.audio).pipe(Effect.map((blob) => [
33
+ "file",
34
+ blob,
35
+ request.fileName ?? defaultFileName(blob.type)
36
+ ])),
37
+ ["model", request.model],
38
+ ...when(request.language !== void 0, ["language", request.language ?? ""]),
39
+ ...when(request.diarization === true, ["diarize", "true"]),
40
+ ...when(request.wordTimestamps === true, ["timestamp_granularities[]", "word"]),
41
+ ...when(request.temperature !== void 0, ["temperature", String(request.temperature)]),
42
+ ...(request.biasingTerms ?? []).map((term) => ["context_bias", term])
43
+ ];
44
+ return Array.reduce(parts, new FormData(), appendPart);
45
+ });
46
+ const WireWord = Schema.Struct({
47
+ text: Schema.optional(Schema.String),
48
+ word: Schema.optional(Schema.String),
49
+ start: Schema.Number,
50
+ end: Schema.Number,
51
+ speaker: Schema.optional(Schema.NullOr(Schema.String))
52
+ });
53
+ const WireResponse = Schema.Struct({
54
+ text: Schema.String,
55
+ language: Schema.optional(Schema.NullOr(Schema.String)),
56
+ words: Schema.optional(Schema.NullOr(Schema.Array(WireWord)))
57
+ });
58
+ const decodeResponse = Schema.decodeUnknownEffect(WireResponse);
59
+ const wireWordToCommon = (w) => ({
60
+ text: w.text ?? w.word ?? "",
61
+ startSeconds: w.start,
62
+ endSeconds: w.end,
63
+ ...w.speaker != null && { speakerId: w.speaker }
64
+ });
65
+ const toResult = (raw, decoded) => ({
66
+ text: decoded.text,
67
+ ...decoded.language != null && { languageCode: decoded.language },
68
+ ...decoded.words != null && { words: decoded.words.map(wireWordToCommon) },
69
+ raw
70
+ });
71
+ /** Exported for reuse by `MistralRealtimeTranscriber` (same sync path). */
72
+ const transcribeImpl = (cfg) => (request) => Effect.gen(function* () {
73
+ const client = yield* HttpClient.HttpClient;
74
+ const baseUrl = cfg.baseUrl ?? "https://api.mistral.ai";
75
+ const formData = yield* buildFormData(request);
76
+ const httpRequest = HttpClientRequest.post(`${baseUrl}/v1/audio/transcriptions`).pipe(HttpClientRequest.bearerToken(cfg.apiKey), HttpClientRequest.bodyFormData(formData));
77
+ const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure));
78
+ if (response.status >= 400) {
79
+ const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""));
80
+ return yield* Effect.fail(httpStatusError(response.status, text));
81
+ }
82
+ const json = yield* response.json.pipe(Effect.mapError(transportFailure));
83
+ return toResult(json, yield* decodeResponse(json).pipe(Effect.mapError(transportFailure)));
84
+ });
85
+ /**
86
+ * Sync-only Layer's streaming impl. For live transcription import the
87
+ * `MistralRealtimeTranscriber` subpath, which registers `SttStreaming` and
88
+ * wires the Voxtral Realtime WebSocket endpoint.
89
+ */
90
+ const streamUnsupported = (_audioIn, _request) => Stream.fail(new AiError.Unsupported({
91
+ provider: "mistral",
92
+ capability: "streamTranscriptionFrom",
93
+ reason: "This Layer is sync-only. Use `@effect-uai/mistral/MistralRealtimeTranscriber` for live Voxtral transcription."
94
+ }));
95
+ const make = (cfg) => Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({
96
+ transcribe: (request) => transcribeImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),
97
+ streamTranscriptionFrom: streamUnsupported
98
+ }));
99
+ /**
100
+ * Sync-only Layer. Registers `MistralTranscriber` + the generic `Transcriber`
101
+ * tag. Does NOT register `SttStreaming` — for live transcription use
102
+ * `@effect-uai/mistral/MistralRealtimeTranscriber`.
103
+ */
104
+ const layer = (cfg) => Layer.merge(Layer.effect(MistralTranscriber, make(cfg)), Layer.effect(Transcriber, Effect.map(make(cfg), (s) => ({
105
+ transcribe: (req) => s.transcribe(req),
106
+ streamTranscriptionFrom: s.streamTranscriptionFrom
107
+ }))));
108
+ //#endregion
109
+ export { MistralTranscriber, layer, make, MistralTranscriber_exports as t, transcribeImpl };
110
+
111
+ //# sourceMappingURL=MistralTranscriber.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MistralTranscriber.mjs","names":["Arr"],"sources":["../src/MistralTranscriber.ts"],"sourcesContent":["import { Array as Arr, Context, Effect, Layer, Redacted, Schema, Stream } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport * as Capabilities from \"@effect-uai/core/Capabilities\"\nimport { isAudioUrl } from \"@effect-uai/core/Audio\"\nimport type { TranscriptEvent, TranscriptResult, WordTimestamp } from \"@effect-uai/core/Transcript\"\nimport {\n type CommonStreamTranscribeRequest,\n type CommonTranscribeRequest,\n Transcriber,\n type TranscriberService,\n} from \"@effect-uai/core/Transcriber\"\nimport { audioToBlob, defaultFileName } from \"./audioCodec.js\"\nimport { httpStatusError, transportFailure } from \"./http.js\"\nimport type { MistralTranscribeModel } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Mistral-typed transcribe request. `model` narrows to the typed\n * `MistralTranscribeModel` union; `fileName` overrides the multipart upload's\n * filename. Voxtral's batch endpoint supports diarization, word timestamps,\n * and context biasing.\n */\nexport type MistralTranscribeRequest = Omit<CommonTranscribeRequest, \"model\"> & {\n readonly model: MistralTranscribeModel\n readonly temperature?: number\n readonly fileName?: string\n}\n\nexport type MistralTranscriberService = {\n readonly transcribe: (\n request: MistralTranscribeRequest,\n ) => Effect.Effect<TranscriptResult, AiError.AiError>\n readonly streamTranscriptionFrom: <E, R>(\n audioIn: Stream.Stream<Uint8Array, E, R>,\n request: CommonStreamTranscribeRequest,\n ) => Stream.Stream<TranscriptEvent, AiError.AiError | E, R>\n}\n\nexport class MistralTranscriber extends Context.Service<\n MistralTranscriber,\n MistralTranscriberService\n>()(\"@betalyra/effect-uai/providers/mistral/MistralTranscriber\") {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n}\n\n// ---------------------------------------------------------------------------\n// Codec — request → FormData\n//\n// Each field is described as a data `Part`, then the whole list is folded into\n// one FormData. A `Part` is either a scalar field or a file field (with an\n// upload filename).\n// ---------------------------------------------------------------------------\n\ntype Part =\n | readonly [name: string, value: string]\n | readonly [name: string, file: Blob, filename: string]\n\nconst when = (condition: boolean, part: Part): ReadonlyArray<Part> => (condition ? [part] : [])\n\nconst appendPart = (fd: FormData, part: Part): FormData => {\n if (part.length === 3) fd.append(part[0], part[1], part[2])\n else fd.append(part[0], part[1])\n return fd\n}\n\nconst buildFormData = (\n request: MistralTranscribeRequest,\n): Effect.Effect<FormData, AiError.AiError> =>\n Effect.gen(function* () {\n // Voxtral transcription has no Whisper-style prose `prompt` field.\n yield* Capabilities.warnDroppedWhen(request.prompt, {\n provider: \"mistral\",\n capability: \"prompt\",\n field: \"prompt\",\n reason:\n \"Voxtral transcription has no prose prompt field; use `biasingTerms` for vocabulary hints.\",\n })\n // URL audio rides the `file_url` field; inline bytes/base64 upload directly.\n const filePart: Part = isAudioUrl(request.audio)\n ? [\"file_url\", request.audio.url]\n : yield* audioToBlob(request.audio).pipe(\n Effect.map(\n (blob): Part => [\"file\", blob, request.fileName ?? defaultFileName(blob.type)],\n ),\n )\n const parts: ReadonlyArray<Part> = [\n filePart,\n [\"model\", request.model],\n ...when(request.language !== undefined, [\"language\", request.language ?? \"\"]),\n ...when(request.diarization === true, [\"diarize\", \"true\"]),\n ...when(request.wordTimestamps === true, [\"timestamp_granularities[]\", \"word\"]),\n ...when(request.temperature !== undefined, [\"temperature\", String(request.temperature)]),\n // Voxtral biases on a `context_bias` term list; one entry per term.\n ...(request.biasingTerms ?? []).map((term): Part => [\"context_bias\", term]),\n ]\n return Arr.reduce(parts, new FormData(), appendPart)\n })\n\n// ---------------------------------------------------------------------------\n// Codec — response → TranscriptResult\n// ---------------------------------------------------------------------------\n\nconst WireWord = Schema.Struct({\n text: Schema.optional(Schema.String),\n word: Schema.optional(Schema.String),\n start: Schema.Number,\n end: Schema.Number,\n speaker: Schema.optional(Schema.NullOr(Schema.String)),\n})\n\nconst WireResponse = Schema.Struct({\n text: Schema.String,\n language: Schema.optional(Schema.NullOr(Schema.String)),\n words: Schema.optional(Schema.NullOr(Schema.Array(WireWord))),\n})\n\nconst decodeResponse = Schema.decodeUnknownEffect(WireResponse)\n\nconst wireWordToCommon = (w: typeof WireWord.Type): WordTimestamp => ({\n text: w.text ?? w.word ?? \"\",\n startSeconds: w.start,\n endSeconds: w.end,\n ...(w.speaker != null && { speakerId: w.speaker }),\n})\n\nconst toResult = (raw: unknown, decoded: typeof WireResponse.Type): TranscriptResult => ({\n text: decoded.text,\n ...(decoded.language != null && { languageCode: decoded.language }),\n ...(decoded.words != null && { words: decoded.words.map(wireWordToCommon) }),\n raw,\n})\n\n// ---------------------------------------------------------------------------\n// HTTP plumbing\n// ---------------------------------------------------------------------------\n\n/** Exported for reuse by `MistralRealtimeTranscriber` (same sync path). */\nexport const transcribeImpl =\n (cfg: Config) =>\n (\n request: MistralTranscribeRequest,\n ): Effect.Effect<TranscriptResult, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const baseUrl = cfg.baseUrl ?? \"https://api.mistral.ai\"\n const formData = yield* buildFormData(request)\n const httpRequest = HttpClientRequest.post(`${baseUrl}/v1/audio/transcriptions`).pipe(\n HttpClientRequest.bearerToken(cfg.apiKey),\n HttpClientRequest.bodyFormData(formData),\n )\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* Effect.fail(httpStatusError(response.status, text))\n }\n const json = yield* response.json.pipe(Effect.mapError(transportFailure))\n const decoded = yield* decodeResponse(json).pipe(Effect.mapError(transportFailure))\n return toResult(json, decoded)\n })\n\n// ---------------------------------------------------------------------------\n// streamTranscriptionFrom — Unsupported on the sync-only Layer\n// ---------------------------------------------------------------------------\n\n/**\n * Sync-only Layer's streaming impl. For live transcription import the\n * `MistralRealtimeTranscriber` subpath, which registers `SttStreaming` and\n * wires the Voxtral Realtime WebSocket endpoint.\n */\nconst streamUnsupported = <E, R>(\n _audioIn: Stream.Stream<Uint8Array, E, R>,\n _request: CommonStreamTranscribeRequest,\n): Stream.Stream<TranscriptEvent, AiError.AiError | E, R> =>\n Stream.fail(\n new AiError.Unsupported({\n provider: \"mistral\",\n capability: \"streamTranscriptionFrom\",\n reason:\n \"This Layer is sync-only. Use `@effect-uai/mistral/MistralRealtimeTranscriber` for live Voxtral transcription.\",\n }),\n )\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\nexport const make = (\n cfg: Config,\n): Effect.Effect<MistralTranscriberService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({\n transcribe: (request) =>\n transcribeImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n streamTranscriptionFrom: streamUnsupported,\n }))\n\n/**\n * Sync-only Layer. Registers `MistralTranscriber` + the generic `Transcriber`\n * tag. Does NOT register `SttStreaming` — for live transcription use\n * `@effect-uai/mistral/MistralRealtimeTranscriber`.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<MistralTranscriber | Transcriber, never, HttpClient.HttpClient> =>\n Layer.merge(\n Layer.effect(MistralTranscriber, make(cfg)),\n Layer.effect(\n Transcriber,\n Effect.map(\n make(cfg),\n (s): TranscriberService => ({\n transcribe: (req: CommonTranscribeRequest) =>\n s.transcribe(req as MistralTranscribeRequest),\n streamTranscriptionFrom: s.streamTranscriptionFrom,\n }),\n ),\n ),\n )\n"],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import { Effect } from "effect";
2
+ import * as AiError from "@effect-uai/core/AiError";
3
+ import { AudioFormat, AudioSource } from "@effect-uai/core/Audio";
4
+
5
+ //#region src/audioCodec.d.ts
6
+ /**
7
+ * Build a `Blob` from any `AudioSource` variant. `url` variants pass through
8
+ * to the `file_url` field upstream, so this only handles inline bytes/base64.
9
+ */
10
+ declare const audioToBlob: (audio: AudioSource) => Effect.Effect<Blob, AiError.AiError>;
11
+ /** Derive a default upload filename from a MIME type, e.g. `audio/mpeg` -> `audio.mp3`. */
12
+ declare const defaultFileName: (mimeType: string) => string;
13
+ type MistralTtsFormat = "mp3" | "wav" | "pcm" | "flac" | "opus";
14
+ /**
15
+ * Map an `AudioFormat.container` to Voxtral TTS's `response_format`. Voxtral
16
+ * produces mp3 / wav / pcm / flac / opus; aac / ogg / webm are unsupported.
17
+ */
18
+ declare const containerToTtsFormat: (container: AudioFormat["container"]) => Effect.Effect<MistralTtsFormat, AiError.AiError>;
19
+ /**
20
+ * The on-wire `AudioFormat` Voxtral actually returns for each
21
+ * `response_format`. Voxtral's `pcm` is float32 LE at 24 kHz; the caller's
22
+ * requested sample rate is not honoured for raw output.
23
+ */
24
+ declare const realizedTtsFormat: (rf: MistralTtsFormat) => AudioFormat;
25
+ //#endregion
26
+ export { MistralTtsFormat, audioToBlob, containerToTtsFormat, defaultFileName, realizedTtsFormat };
27
+ //# sourceMappingURL=audioCodec.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audioCodec.d.mts","names":[],"sources":["../src/audioCodec.ts"],"mappings":";;;;;;;AA2BA;;cAAa,WAAA,GAAc,KAAA,EAAO,WAAA,KAAgB,MAAA,CAAO,MAAA,CAAO,IAAA,EAAM,OAAA,CAAQ,OAAA;;cAgCjE,eAAA,GAAmB,QAAA;AAAA,KASpB,gBAAA;;;;;cAMC,oBAAA,GACX,SAAA,EAAW,WAAA,kBACR,MAAA,CAAO,MAAA,CAAO,gBAAA,EAAkB,OAAA,CAAQ,OAAA;;;;;;cAuBhC,iBAAA,GAAoB,EAAA,EAAI,gBAAA,KAAqB,WAAA"}
@@ -0,0 +1,82 @@
1
+ import { Array, Effect, Encoding, Match, Option, Result } from "effect";
2
+ import * as AiError from "@effect-uai/core/AiError";
3
+ //#region src/audioCodec.ts
4
+ const decodeBase64ToBytes = (b64) => Result.match(Encoding.decodeBase64(b64), {
5
+ onSuccess: Effect.succeed,
6
+ onFailure: (cause) => Effect.fail(new AiError.InvalidRequest({
7
+ provider: "mistral",
8
+ param: "audio",
9
+ raw: cause
10
+ }))
11
+ });
12
+ /**
13
+ * TS 6's `Blob` constructor wants `Uint8Array<ArrayBuffer>`; our domain carries
14
+ * the broader `Uint8Array<ArrayBufferLike>`. The cast is sound: `Blob()` does
15
+ * not mutate the buffer and the runtime accepts both.
16
+ */
17
+ const bytesToBlob = (bytes, mimeType) => new Blob([bytes], { type: mimeType });
18
+ /**
19
+ * Build a `Blob` from any `AudioSource` variant. `url` variants pass through
20
+ * to the `file_url` field upstream, so this only handles inline bytes/base64.
21
+ */
22
+ const audioToBlob = Match.type().pipe(Match.tag("bytes", (a) => Effect.succeed(bytesToBlob(a.bytes, a.mimeType))), Match.tag("base64", (a) => decodeBase64ToBytes(a.base64).pipe(Effect.map((bytes) => bytesToBlob(bytes, a.mimeType)))), Match.tag("url", (a) => Effect.fail(new AiError.InvalidRequest({
23
+ provider: "mistral",
24
+ param: "audio",
25
+ raw: `Pass URL audio via the request's url source; got ${a.url} where inline bytes were expected.`
26
+ }))), Match.exhaustive);
27
+ const EXTENSION_BY_MIME = [
28
+ ["mpeg", "mp3"],
29
+ ["mp3", "mp3"],
30
+ ["wav", "wav"],
31
+ ["ogg", "ogg"],
32
+ ["opus", "ogg"],
33
+ ["flac", "flac"],
34
+ ["aac", "aac"],
35
+ ["mp4", "m4a"],
36
+ ["m4a", "m4a"],
37
+ ["webm", "webm"]
38
+ ];
39
+ /** Derive a default upload filename from a MIME type, e.g. `audio/mpeg` -> `audio.mp3`. */
40
+ const defaultFileName = (mimeType) => Array.findFirst(EXTENSION_BY_MIME, ([pattern]) => mimeType.includes(pattern)).pipe(Option.match({
41
+ onNone: () => "audio",
42
+ onSome: ([, ext]) => `audio.${ext}`
43
+ }));
44
+ /**
45
+ * Map an `AudioFormat.container` to Voxtral TTS's `response_format`. Voxtral
46
+ * produces mp3 / wav / pcm / flac / opus; aac / ogg / webm are unsupported.
47
+ */
48
+ const containerToTtsFormat = Match.type().pipe(Match.when("mp3", () => Effect.succeed("mp3")), Match.when("wav", () => Effect.succeed("wav")), Match.when("raw", () => Effect.succeed("pcm")), Match.when("flac", () => Effect.succeed("flac")), Match.when("opus", () => Effect.succeed("opus")), Match.whenOr("aac", "ogg", "webm", (c) => Effect.fail(new AiError.Unsupported({
49
+ provider: "mistral",
50
+ capability: "outputFormat",
51
+ reason: `Voxtral TTS does not produce ${c} output; supported: mp3 | wav | pcm (raw) | flac | opus.`
52
+ }))), Match.exhaustive);
53
+ /**
54
+ * The on-wire `AudioFormat` Voxtral actually returns for each
55
+ * `response_format`. Voxtral's `pcm` is float32 LE at 24 kHz; the caller's
56
+ * requested sample rate is not honoured for raw output.
57
+ */
58
+ const realizedTtsFormat = Match.type().pipe(Match.when("mp3", () => ({
59
+ container: "mp3",
60
+ encoding: "mp3",
61
+ sampleRate: 24e3
62
+ })), Match.when("wav", () => ({
63
+ container: "wav",
64
+ encoding: "pcm_s16le",
65
+ sampleRate: 24e3
66
+ })), Match.when("pcm", () => ({
67
+ container: "raw",
68
+ encoding: "pcm_f32le",
69
+ sampleRate: 24e3
70
+ })), Match.when("flac", () => ({
71
+ container: "flac",
72
+ encoding: "flac",
73
+ sampleRate: 24e3
74
+ })), Match.when("opus", () => ({
75
+ container: "opus",
76
+ encoding: "opus",
77
+ sampleRate: 24e3
78
+ })), Match.exhaustive);
79
+ //#endregion
80
+ export { audioToBlob, containerToTtsFormat, defaultFileName, realizedTtsFormat };
81
+
82
+ //# sourceMappingURL=audioCodec.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audioCodec.mjs","names":["Arr"],"sources":["../src/audioCodec.ts"],"sourcesContent":["import { Array as Arr, Effect, Encoding, Match, Option, Result } from \"effect\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioFormat, AudioSource } from \"@effect-uai/core/Audio\"\n\n// ---------------------------------------------------------------------------\n// AudioSource → Blob (for multipart upload to /audio/transcriptions)\n// ---------------------------------------------------------------------------\n\nconst decodeBase64ToBytes = (b64: string): Effect.Effect<Uint8Array, AiError.AiError> =>\n Result.match(Encoding.decodeBase64(b64), {\n onSuccess: Effect.succeed,\n onFailure: (cause) =>\n Effect.fail(new AiError.InvalidRequest({ provider: \"mistral\", param: \"audio\", raw: cause })),\n })\n\n/**\n * TS 6's `Blob` constructor wants `Uint8Array<ArrayBuffer>`; our domain carries\n * the broader `Uint8Array<ArrayBufferLike>`. The cast is sound: `Blob()` does\n * not mutate the buffer and the runtime accepts both.\n */\nconst bytesToBlob = (bytes: Uint8Array, mimeType: string): Blob =>\n new Blob([bytes as Uint8Array<ArrayBuffer>], { type: mimeType })\n\n/**\n * Build a `Blob` from any `AudioSource` variant. `url` variants pass through\n * to the `file_url` field upstream, so this only handles inline bytes/base64.\n */\nexport const audioToBlob: (audio: AudioSource) => Effect.Effect<Blob, AiError.AiError> =\n Match.type<AudioSource>().pipe(\n Match.tag(\"bytes\", (a) => Effect.succeed(bytesToBlob(a.bytes, a.mimeType))),\n Match.tag(\"base64\", (a) =>\n decodeBase64ToBytes(a.base64).pipe(Effect.map((bytes) => bytesToBlob(bytes, a.mimeType))),\n ),\n Match.tag(\"url\", (a) =>\n Effect.fail(\n new AiError.InvalidRequest({\n provider: \"mistral\",\n param: \"audio\",\n raw: `Pass URL audio via the request's url source; got ${a.url} where inline bytes were expected.`,\n }),\n ),\n ),\n Match.exhaustive,\n )\n\nconst EXTENSION_BY_MIME: ReadonlyArray<readonly [pattern: string, ext: string]> = [\n [\"mpeg\", \"mp3\"],\n [\"mp3\", \"mp3\"],\n [\"wav\", \"wav\"],\n [\"ogg\", \"ogg\"],\n [\"opus\", \"ogg\"],\n [\"flac\", \"flac\"],\n [\"aac\", \"aac\"],\n [\"mp4\", \"m4a\"],\n [\"m4a\", \"m4a\"],\n [\"webm\", \"webm\"],\n]\n\n/** Derive a default upload filename from a MIME type, e.g. `audio/mpeg` -> `audio.mp3`. */\nexport const defaultFileName = (mimeType: string): string =>\n Arr.findFirst(EXTENSION_BY_MIME, ([pattern]) => mimeType.includes(pattern)).pipe(\n Option.match({ onNone: () => \"audio\", onSome: ([, ext]) => `audio.${ext}` }),\n )\n\n// ---------------------------------------------------------------------------\n// AudioFormat → Voxtral TTS `response_format`\n// ---------------------------------------------------------------------------\n\nexport type MistralTtsFormat = \"mp3\" | \"wav\" | \"pcm\" | \"flac\" | \"opus\"\n\n/**\n * Map an `AudioFormat.container` to Voxtral TTS's `response_format`. Voxtral\n * produces mp3 / wav / pcm / flac / opus; aac / ogg / webm are unsupported.\n */\nexport const containerToTtsFormat: (\n container: AudioFormat[\"container\"],\n) => Effect.Effect<MistralTtsFormat, AiError.AiError> = Match.type<AudioFormat[\"container\"]>().pipe(\n Match.when(\"mp3\", () => Effect.succeed<MistralTtsFormat>(\"mp3\")),\n Match.when(\"wav\", () => Effect.succeed<MistralTtsFormat>(\"wav\")),\n Match.when(\"raw\", () => Effect.succeed<MistralTtsFormat>(\"pcm\")),\n Match.when(\"flac\", () => Effect.succeed<MistralTtsFormat>(\"flac\")),\n Match.when(\"opus\", () => Effect.succeed<MistralTtsFormat>(\"opus\")),\n Match.whenOr(\"aac\", \"ogg\", \"webm\", (c) =>\n Effect.fail(\n new AiError.Unsupported({\n provider: \"mistral\",\n capability: \"outputFormat\",\n reason: `Voxtral TTS does not produce ${c} output; supported: mp3 | wav | pcm (raw) | flac | opus.`,\n }),\n ),\n ),\n Match.exhaustive,\n)\n\n/**\n * The on-wire `AudioFormat` Voxtral actually returns for each\n * `response_format`. Voxtral's `pcm` is float32 LE at 24 kHz; the caller's\n * requested sample rate is not honoured for raw output.\n */\nexport const realizedTtsFormat: (rf: MistralTtsFormat) => AudioFormat =\n Match.type<MistralTtsFormat>().pipe(\n Match.when(\n \"mp3\",\n (): AudioFormat => ({ container: \"mp3\", encoding: \"mp3\", sampleRate: 24000 }),\n ),\n Match.when(\n \"wav\",\n (): AudioFormat => ({ container: \"wav\", encoding: \"pcm_s16le\", sampleRate: 24000 }),\n ),\n Match.when(\n \"pcm\",\n (): AudioFormat => ({ container: \"raw\", encoding: \"pcm_f32le\", sampleRate: 24000 }),\n ),\n Match.when(\n \"flac\",\n (): AudioFormat => ({ container: \"flac\", encoding: \"flac\", sampleRate: 24000 }),\n ),\n Match.when(\n \"opus\",\n (): AudioFormat => ({ container: \"opus\", encoding: \"opus\", sampleRate: 24000 }),\n ),\n Match.exhaustive,\n )\n"],"mappings":";;;AAQA,MAAM,uBAAuB,QAC3B,OAAO,MAAM,SAAS,aAAa,IAAI,EAAE;CACvC,WAAW,OAAO;CAClB,YAAY,UACV,OAAO,KAAK,IAAI,QAAQ,eAAe;EAAE,UAAU;EAAW,OAAO;EAAS,KAAK;EAAO,CAAC,CAAC;CAC/F,CAAC;;;;;;AAOJ,MAAM,eAAe,OAAmB,aACtC,IAAI,KAAK,CAAC,MAAiC,EAAE,EAAE,MAAM,UAAU,CAAC;;;;;AAMlE,MAAa,cACX,MAAM,MAAmB,CAAC,KACxB,MAAM,IAAI,UAAU,MAAM,OAAO,QAAQ,YAAY,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,EAC3E,MAAM,IAAI,WAAW,MACnB,oBAAoB,EAAE,OAAO,CAAC,KAAK,OAAO,KAAK,UAAU,YAAY,OAAO,EAAE,SAAS,CAAC,CAAC,CAC1F,EACD,MAAM,IAAI,QAAQ,MAChB,OAAO,KACL,IAAI,QAAQ,eAAe;CACzB,UAAU;CACV,OAAO;CACP,KAAK,oDAAoD,EAAE,IAAI;CAChE,CAAC,CACH,CACF,EACD,MAAM,WACP;AAEH,MAAM,oBAA4E;CAChF,CAAC,QAAQ,MAAM;CACf,CAAC,OAAO,MAAM;CACd,CAAC,OAAO,MAAM;CACd,CAAC,OAAO,MAAM;CACd,CAAC,QAAQ,MAAM;CACf,CAAC,QAAQ,OAAO;CAChB,CAAC,OAAO,MAAM;CACd,CAAC,OAAO,MAAM;CACd,CAAC,OAAO,MAAM;CACd,CAAC,QAAQ,OAAO;CACjB;;AAGD,MAAa,mBAAmB,aAC9BA,MAAI,UAAU,oBAAoB,CAAC,aAAa,SAAS,SAAS,QAAQ,CAAC,CAAC,KAC1E,OAAO,MAAM;CAAE,cAAc;CAAS,SAAS,GAAG,SAAS,SAAS;CAAO,CAAC,CAC7E;;;;;AAYH,MAAa,uBAE2C,MAAM,MAAgC,CAAC,KAC7F,MAAM,KAAK,aAAa,OAAO,QAA0B,MAAM,CAAC,EAChE,MAAM,KAAK,aAAa,OAAO,QAA0B,MAAM,CAAC,EAChE,MAAM,KAAK,aAAa,OAAO,QAA0B,MAAM,CAAC,EAChE,MAAM,KAAK,cAAc,OAAO,QAA0B,OAAO,CAAC,EAClE,MAAM,KAAK,cAAc,OAAO,QAA0B,OAAO,CAAC,EAClE,MAAM,OAAO,OAAO,OAAO,SAAS,MAClC,OAAO,KACL,IAAI,QAAQ,YAAY;CACtB,UAAU;CACV,YAAY;CACZ,QAAQ,gCAAgC,EAAE;CAC3C,CAAC,CACH,CACF,EACD,MAAM,WACP;;;;;;AAOD,MAAa,oBACX,MAAM,MAAwB,CAAC,KAC7B,MAAM,KACJ,cACoB;CAAE,WAAW;CAAO,UAAU;CAAO,YAAY;CAAO,EAC7E,EACD,MAAM,KACJ,cACoB;CAAE,WAAW;CAAO,UAAU;CAAa,YAAY;CAAO,EACnF,EACD,MAAM,KACJ,cACoB;CAAE,WAAW;CAAO,UAAU;CAAa,YAAY;CAAO,EACnF,EACD,MAAM,KACJ,eACoB;CAAE,WAAW;CAAQ,UAAU;CAAQ,YAAY;CAAO,EAC/E,EACD,MAAM,KACJ,eACoB;CAAE,WAAW;CAAQ,UAAU;CAAQ,YAAY;CAAO,EAC/E,EACD,MAAM,WACP"}
@@ -0,0 +1,13 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __exportAll = (all, no_symbols) => {
4
+ let target = {};
5
+ for (var name in all) __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true
8
+ });
9
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
+ return target;
11
+ };
12
+ //#endregion
13
+ export { __exportAll as t };
@@ -0,0 +1,100 @@
1
+ import { Option, Schema } from "effect";
2
+ import { ToolDescriptor } from "@effect-uai/core/Tool";
3
+ import { Turn, TurnEvent } from "@effect-uai/core/Turn";
4
+ import * as _$effect_SchemaAST0 from "effect/SchemaAST";
5
+ import * as _$effect_Effect0 from "effect/Effect";
6
+ import { HistoryItem, Usage } from "@effect-uai/core/Items";
7
+ import { StructuredFormat } from "@effect-uai/core/StructuredFormat";
8
+
9
+ //#region src/codec.d.ts
10
+ declare namespace codec_d_exports {
11
+ export { Accumulator, WireChunk, accumulatorToTurn, applyChunk, decodeChunk, emptyAccumulator, itemsToMessages, responseFormatWire, toolChoiceWire, toolsWire };
12
+ }
13
+ type WireMessage = {
14
+ role: string;
15
+ content: string | ReadonlyArray<Record<string, unknown>>;
16
+ tool_calls?: ReadonlyArray<Record<string, unknown>>;
17
+ tool_call_id?: string;
18
+ };
19
+ /** Convert our `HistoryItem[]` history into Mistral chat `messages`. */
20
+ declare const itemsToMessages: (items: ReadonlyArray<HistoryItem>) => ReadonlyArray<WireMessage>;
21
+ declare const toolsWire: (descriptors: ReadonlyArray<ToolDescriptor>) => Option.Option<ReadonlyArray<Record<string, unknown>>>;
22
+ type ToolChoice = "auto" | "required" | "none" | {
23
+ readonly type: "function";
24
+ readonly name: string;
25
+ };
26
+ declare const toolChoiceWire: (choice: ToolChoice) => string | Record<string, unknown>;
27
+ declare const responseFormatWire: (structured: StructuredFormat<unknown>) => Record<string, unknown>;
28
+ declare const WireChunk: Schema.Struct<{
29
+ readonly choices: Schema.optional<Schema.$Array<Schema.Struct<{
30
+ readonly index: Schema.optional<Schema.Number>;
31
+ readonly delta: Schema.optional<Schema.Struct<{
32
+ readonly role: Schema.optional<Schema.NullOr<Schema.String>>;
33
+ readonly content: Schema.optional<Schema.NullOr<Schema.String>>;
34
+ readonly tool_calls: Schema.optional<Schema.NullOr<Schema.$Array<Schema.Struct<{
35
+ readonly index: Schema.optional<Schema.Number>;
36
+ readonly id: Schema.optional<Schema.NullOr<Schema.String>>;
37
+ readonly function: Schema.optional<Schema.Struct<{
38
+ readonly name: Schema.optional<Schema.NullOr<Schema.String>>;
39
+ readonly arguments: Schema.optional<Schema.NullOr<Schema.String>>;
40
+ }>>;
41
+ }>>>>;
42
+ }>>;
43
+ readonly finish_reason: Schema.optional<Schema.NullOr<Schema.String>>;
44
+ }>>>;
45
+ readonly usage: Schema.optional<Schema.NullOr<Schema.Struct<{
46
+ readonly prompt_tokens: Schema.optional<Schema.Number>;
47
+ readonly completion_tokens: Schema.optional<Schema.Number>;
48
+ readonly total_tokens: Schema.optional<Schema.Number>;
49
+ }>>>;
50
+ }>;
51
+ type WireChunk = typeof WireChunk.Type;
52
+ declare const decodeChunk: (input: unknown, options?: _$effect_SchemaAST0.ParseOptions) => _$effect_Effect0.Effect<{
53
+ readonly choices?: readonly {
54
+ readonly index?: number | undefined;
55
+ readonly delta?: {
56
+ readonly role?: string | null | undefined;
57
+ readonly content?: string | null | undefined;
58
+ readonly tool_calls?: readonly {
59
+ readonly function?: {
60
+ readonly name?: string | null | undefined;
61
+ readonly arguments?: string | null | undefined;
62
+ } | undefined;
63
+ readonly index?: number | undefined;
64
+ readonly id?: string | null | undefined;
65
+ }[] | null | undefined;
66
+ } | undefined;
67
+ readonly finish_reason?: string | null | undefined;
68
+ }[] | undefined;
69
+ readonly usage?: {
70
+ readonly prompt_tokens?: number | undefined;
71
+ readonly completion_tokens?: number | undefined;
72
+ readonly total_tokens?: number | undefined;
73
+ } | null | undefined;
74
+ }, Schema.SchemaError, never>;
75
+ type ToolAcc = {
76
+ readonly call_id: string;
77
+ readonly name: string;
78
+ readonly arguments: string;
79
+ };
80
+ type Accumulator = {
81
+ readonly text: string; /** Tool calls keyed by their wire `index` (defaults to position when absent). */
82
+ readonly tools: ReadonlyMap<number, ToolAcc>;
83
+ readonly order: ReadonlyArray<number>;
84
+ readonly usage: Usage;
85
+ readonly finishReason: Option.Option<string>;
86
+ };
87
+ declare const emptyAccumulator: Accumulator;
88
+ /** State threaded through the fold plus the events the step produced. */
89
+ type Step = readonly [Accumulator, ReadonlyArray<TurnEvent>];
90
+ /**
91
+ * Fold one decoded chunk into the accumulator, returning the next state and
92
+ * the `TurnEvent`s the chunk produced. Mirrors Anthropic's `deltasFromEvent`,
93
+ * but over chat-completions deltas.
94
+ */
95
+ declare const applyChunk: (acc: Accumulator, chunk: WireChunk) => Step;
96
+ /** Assemble the terminal `Turn` from the accumulated state. */
97
+ declare const accumulatorToTurn: (acc: Accumulator) => Turn;
98
+ //#endregion
99
+ export { Accumulator, WireChunk, accumulatorToTurn, applyChunk, decodeChunk, emptyAccumulator, itemsToMessages, responseFormatWire, codec_d_exports as t, toolChoiceWire, toolsWire };
100
+ //# sourceMappingURL=codec.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codec.d.mts","names":[],"sources":["../src/codec.ts"],"mappings":";;;;;;;;;;;;KAwBK,WAAA;EACH,IAAA;EACA,OAAA,WAAkB,aAAA,CAAc,MAAA;EAChC,UAAA,GAAa,aAAA,CAAc,MAAA;EAC3B,YAAA;AAAA;;cAwEW,eAAA,GAAmB,KAAA,EAAO,aAAA,CAAc,WAAA,MAAe,aAAA,CAAc,WAAA;AAAA,cAOrE,SAAA,GACX,WAAA,EAAa,aAAA,CAAc,cAAA,MAC1B,MAAA,CAAO,MAAA,CAAO,aAAA,CAAc,MAAA;AAAA,KAe1B,UAAA;EAAA,SAIU,IAAA;EAAA,SAA2B,IAAA;AAAA;AAAA,cAG7B,cAAA,GAAkB,MAAA,EAAQ,UAAA,cAAsB,MAAA;AAAA,cAQhD,kBAAA,GACX,UAAA,EAAY,gBAAA,cACX,MAAA;AAAA,cA0CU,SAAA,EAAS,MAAA,CAAA,MAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;KAIV,SAAA,UAAmB,SAAA,CAAU,IAAA;AAAA,cAE5B,WAAA,GAAW,KAAA,WAAA,OAAA,GAAwC,mBAAA,CAAxC,YAAA,sBAAA,MAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;KAMnB,OAAA;EAAA,SACM,OAAA;EAAA,SACA,IAAA;EAAA,SACA,SAAA;AAAA;AAAA,KAGC,WAAA;EAAA,SACD,IAAA,UA7F0B;EAAA,SA+F1B,KAAA,EAAO,WAAA,SAAoB,OAAA;EAAA,SAC3B,KAAA,EAAO,aAAA;EAAA,SACP,KAAA,EAAO,KAAA;EAAA,SACP,YAAA,EAAc,MAAA,CAAO,MAAA;AAAA;AAAA,cAGnB,gBAAA,EAAkB,WAAA;;KAS1B,IAAA,aAAiB,WAAA,EAAa,aAAA,CAAc,SAAA;;;;;;cAmFpC,UAAA,GAAc,GAAA,EAAK,WAAA,EAAa,KAAA,EAAO,SAAA,KAAY,IAAA;AAnKhE;AAAA,cA+Ka,iBAAA,GAAqB,GAAA,EAAK,WAAA,KAAc,IAAA"}
package/dist/codec.mjs ADDED
@@ -0,0 +1,244 @@
1
+ import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
2
+ import { Array, Encoding, Match, Option, Schema } from "effect";
3
+ import { TurnEvent } from "@effect-uai/core/Turn";
4
+ //#region src/codec.ts
5
+ var codec_exports = /* @__PURE__ */ __exportAll({
6
+ WireChunk: () => WireChunk,
7
+ accumulatorToTurn: () => accumulatorToTurn,
8
+ applyChunk: () => applyChunk,
9
+ decodeChunk: () => decodeChunk,
10
+ emptyAccumulator: () => emptyAccumulator,
11
+ itemsToMessages: () => itemsToMessages,
12
+ responseFormatWire: () => responseFormatWire,
13
+ toolChoiceWire: () => toolChoiceWire,
14
+ toolsWire: () => toolsWire
15
+ });
16
+ /** A `data:` URI for inline image bytes; URLs pass through untouched. */
17
+ const imageSourceToUrl = Match.type().pipe(Match.tag("url", (s) => s.url), Match.tag("base64", (s) => `data:${s.mimeType};base64,${s.base64}`), Match.tag("bytes", (s) => `data:${s.mimeType};base64,${Encoding.encodeBase64(s.bytes)}`), Match.exhaustive);
18
+ const blockToPart = Match.type().pipe(Match.discriminatorsExhaustive("type")({
19
+ input_text: (b) => ({
20
+ type: "text",
21
+ text: b.text
22
+ }),
23
+ output_text: (b) => ({
24
+ type: "text",
25
+ text: b.text
26
+ }),
27
+ refusal: (b) => ({
28
+ type: "text",
29
+ text: b.text
30
+ }),
31
+ input_image: (b) => ({
32
+ type: "image_url",
33
+ image_url: imageSourceToUrl(b.source)
34
+ })
35
+ }));
36
+ const isTextBlock = (b) => b.type === "input_text" || b.type === "output_text" || b.type === "refusal";
37
+ const textOfBlock = (b) => b.type === "input_image" ? "" : b.text;
38
+ /**
39
+ * Collapse a message's content to a plain string when it is text-only
40
+ * (the common case Mistral prefers), otherwise emit the multimodal parts
41
+ * array.
42
+ */
43
+ const encodeContent = (content) => Array.every(content, isTextBlock) ? content.map(textOfBlock).join("") : content.map(blockToPart);
44
+ const toolCallWire = (call_id, name, args) => ({
45
+ id: call_id,
46
+ type: "function",
47
+ function: {
48
+ name,
49
+ arguments: args
50
+ }
51
+ });
52
+ const appendToolCall = (acc, call_id, name, args) => {
53
+ const last = acc[acc.length - 1];
54
+ const wire = toolCallWire(call_id, name, args);
55
+ if (last !== void 0 && last.role === "assistant" && last.tool_call_id === void 0) return [...acc.slice(0, -1), {
56
+ ...last,
57
+ tool_calls: [...last.tool_calls ?? [], wire]
58
+ }];
59
+ return [...acc, {
60
+ role: "assistant",
61
+ content: "",
62
+ tool_calls: [wire]
63
+ }];
64
+ };
65
+ const foldItem = (acc, item) => Match.value(item).pipe(Match.discriminatorsExhaustive("type")({
66
+ message: (m) => [...acc, {
67
+ role: m.role,
68
+ content: encodeContent(m.content)
69
+ }],
70
+ function_call: (f) => appendToolCall(acc, f.call_id, f.name, f.arguments),
71
+ function_call_output: (o) => [...acc, {
72
+ role: "tool",
73
+ tool_call_id: o.call_id,
74
+ content: o.output
75
+ }],
76
+ reasoning: () => acc
77
+ }));
78
+ /** Convert our `HistoryItem[]` history into Mistral chat `messages`. */
79
+ const itemsToMessages = (items) => Array.reduce(items, [], foldItem);
80
+ const toolsWire = (descriptors) => descriptors.length > 0 ? Option.some(descriptors.map((t) => ({
81
+ type: "function",
82
+ function: {
83
+ name: t.name,
84
+ description: t.description,
85
+ parameters: t.inputSchema,
86
+ ...t.strict !== void 0 && { strict: t.strict }
87
+ }
88
+ }))) : Option.none();
89
+ const toolChoiceWire = (choice) => Match.value(choice).pipe(Match.when("auto", () => "auto"), Match.when("required", () => "any"), Match.when("none", () => "none"), Match.orElse((c) => ({
90
+ type: "function",
91
+ function: { name: c.name }
92
+ })));
93
+ const responseFormatWire = (structured) => ({
94
+ type: "json_schema",
95
+ json_schema: {
96
+ name: structured.name,
97
+ schema: structured.schema["~standard"].jsonSchema.input({ target: "draft-2020-12" }),
98
+ ...structured.strict !== void 0 && { strict: structured.strict }
99
+ }
100
+ });
101
+ const WireFunction = Schema.Struct({
102
+ name: Schema.optional(Schema.NullOr(Schema.String)),
103
+ arguments: Schema.optional(Schema.NullOr(Schema.String))
104
+ });
105
+ const WireToolCall = Schema.Struct({
106
+ index: Schema.optional(Schema.Number),
107
+ id: Schema.optional(Schema.NullOr(Schema.String)),
108
+ function: Schema.optional(WireFunction)
109
+ });
110
+ const WireDelta = Schema.Struct({
111
+ role: Schema.optional(Schema.NullOr(Schema.String)),
112
+ content: Schema.optional(Schema.NullOr(Schema.String)),
113
+ tool_calls: Schema.optional(Schema.NullOr(Schema.Array(WireToolCall)))
114
+ });
115
+ const WireChoice = Schema.Struct({
116
+ index: Schema.optional(Schema.Number),
117
+ delta: Schema.optional(WireDelta),
118
+ finish_reason: Schema.optional(Schema.NullOr(Schema.String))
119
+ });
120
+ const WireUsage = Schema.Struct({
121
+ prompt_tokens: Schema.optional(Schema.Number),
122
+ completion_tokens: Schema.optional(Schema.Number),
123
+ total_tokens: Schema.optional(Schema.Number)
124
+ });
125
+ const WireChunk = Schema.Struct({
126
+ choices: Schema.optional(Schema.Array(WireChoice)),
127
+ usage: Schema.optional(Schema.NullOr(WireUsage))
128
+ });
129
+ const decodeChunk = Schema.decodeUnknownEffect(WireChunk);
130
+ const emptyAccumulator = {
131
+ text: "",
132
+ tools: /* @__PURE__ */ new Map(),
133
+ order: [],
134
+ usage: {},
135
+ finishReason: Option.none()
136
+ };
137
+ const usageFrom = (wire) => ({
138
+ ...wire.prompt_tokens !== void 0 && { input_tokens: wire.prompt_tokens },
139
+ ...wire.completion_tokens !== void 0 && { output_tokens: wire.completion_tokens },
140
+ ...wire.total_tokens !== void 0 && { total_tokens: wire.total_tokens }
141
+ });
142
+ const reasonToStop = Match.type().pipe(Match.when("stop", () => "stop"), Match.when("length", () => "max_tokens"), Match.when("model_length", () => "max_tokens"), Match.when("tool_calls", () => "tool_calls"), Match.when("content_filter", () => "content_filter"), Match.orElse(() => "stop"));
143
+ const stopReasonOf = (acc) => Option.match(acc.finishReason, {
144
+ onNone: () => acc.tools.size > 0 ? "tool_calls" : "stop",
145
+ onSome: reasonToStop
146
+ });
147
+ const withTool = (acc, index, tool, isNew) => ({
148
+ ...acc,
149
+ tools: new Map(acc.tools).set(index, tool),
150
+ order: isNew ? [...acc.order, index] : acc.order
151
+ });
152
+ const applyContent = (acc, delta) => {
153
+ const content = delta?.content;
154
+ return content !== void 0 && content !== null && content.length > 0 ? [{
155
+ ...acc,
156
+ text: acc.text + content
157
+ }, [TurnEvent.TextDelta({ text: content })]] : [acc, []];
158
+ };
159
+ const applyToolCall = (acc, tc, position) => {
160
+ const index = tc.index ?? position;
161
+ const argsDelta = tc.function?.arguments ?? "";
162
+ const existing = acc.tools.get(index);
163
+ if (existing === void 0) {
164
+ const call_id = tc.id ?? `call_${index}`;
165
+ const name = tc.function?.name ?? "";
166
+ return [withTool(acc, index, {
167
+ call_id,
168
+ name,
169
+ arguments: argsDelta
170
+ }, true), [TurnEvent.ToolCallStart({
171
+ call_id,
172
+ name
173
+ }), ...argsDelta.length > 0 ? [TurnEvent.ToolCallArgsDelta({
174
+ call_id,
175
+ delta: argsDelta
176
+ })] : []]];
177
+ }
178
+ return argsDelta.length > 0 ? [withTool(acc, index, {
179
+ ...existing,
180
+ arguments: existing.arguments + argsDelta
181
+ }, false), [TurnEvent.ToolCallArgsDelta({
182
+ call_id: existing.call_id,
183
+ delta: argsDelta
184
+ })]] : [acc, []];
185
+ };
186
+ const chain = (step, next) => {
187
+ const [acc, events] = step;
188
+ const [acc2, more] = next(acc);
189
+ return [acc2, [...events, ...more]];
190
+ };
191
+ const applyChoice = (step, choice) => {
192
+ const withContent = chain(step, (acc) => applyContent(acc, choice.delta));
193
+ const withTools = Array.reduce(choice.delta?.tool_calls ?? [], withContent, (s, tc, i) => chain(s, (acc) => applyToolCall(acc, tc, i)));
194
+ const reason = choice.finish_reason;
195
+ return reason !== void 0 && reason !== null ? [{
196
+ ...withTools[0],
197
+ finishReason: Option.some(reason)
198
+ }, withTools[1]] : withTools;
199
+ };
200
+ /**
201
+ * Fold one decoded chunk into the accumulator, returning the next state and
202
+ * the `TurnEvent`s the chunk produced. Mirrors Anthropic's `deltasFromEvent`,
203
+ * but over chat-completions deltas.
204
+ */
205
+ const applyChunk = (acc, chunk) => {
206
+ const afterChoices = Array.reduce(chunk.choices ?? [], [acc, []], applyChoice);
207
+ const usage = chunk.usage;
208
+ return usage !== void 0 && usage !== null ? chain(afterChoices, (a) => {
209
+ const u = usageFrom(usage);
210
+ return [{
211
+ ...a,
212
+ usage: u
213
+ }, [TurnEvent.UsageUpdate({ usage: u })]];
214
+ }) : afterChoices;
215
+ };
216
+ /** Assemble the terminal `Turn` from the accumulated state. */
217
+ const accumulatorToTurn = (acc) => {
218
+ const message = acc.text.length > 0 ? [{
219
+ type: "message",
220
+ role: "assistant",
221
+ content: [{
222
+ type: "output_text",
223
+ text: acc.text
224
+ }]
225
+ }] : [];
226
+ const toolCalls = acc.order.flatMap((index) => {
227
+ const t = acc.tools.get(index);
228
+ return t === void 0 ? [] : [{
229
+ type: "function_call",
230
+ call_id: t.call_id,
231
+ name: t.name,
232
+ arguments: t.arguments
233
+ }];
234
+ });
235
+ return {
236
+ items: [...message, ...toolCalls],
237
+ usage: acc.usage,
238
+ stop_reason: stopReasonOf(acc)
239
+ };
240
+ };
241
+ //#endregion
242
+ export { WireChunk, accumulatorToTurn, applyChunk, decodeChunk, emptyAccumulator, itemsToMessages, responseFormatWire, codec_exports as t, toolChoiceWire, toolsWire };
243
+
244
+ //# sourceMappingURL=codec.mjs.map