@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.
- package/LICENSE +21 -0
- package/README.md +76 -0
- package/dist/Mistral.d.mts +48 -0
- package/dist/Mistral.d.mts.map +1 -0
- package/dist/Mistral.mjs +88 -0
- package/dist/Mistral.mjs.map +1 -0
- package/dist/MistralRealtimeTranscriber.d.mts +15 -0
- package/dist/MistralRealtimeTranscriber.d.mts.map +1 -0
- package/dist/MistralRealtimeTranscriber.mjs +34 -0
- package/dist/MistralRealtimeTranscriber.mjs.map +1 -0
- package/dist/MistralSynthesizer.d.mts +43 -0
- package/dist/MistralSynthesizer.d.mts.map +1 -0
- package/dist/MistralSynthesizer.mjs +121 -0
- package/dist/MistralSynthesizer.mjs.map +1 -0
- package/dist/MistralTranscriber.d.mts +44 -0
- package/dist/MistralTranscriber.d.mts.map +1 -0
- package/dist/MistralTranscriber.mjs +111 -0
- package/dist/MistralTranscriber.mjs.map +1 -0
- package/dist/audioCodec.d.mts +27 -0
- package/dist/audioCodec.d.mts.map +1 -0
- package/dist/audioCodec.mjs +82 -0
- package/dist/audioCodec.mjs.map +1 -0
- package/dist/chunk-CfYAbeIz.mjs +13 -0
- package/dist/codec.d.mts +100 -0
- package/dist/codec.d.mts.map +1 -0
- package/dist/codec.mjs +244 -0
- package/dist/codec.mjs.map +1 -0
- package/dist/codec.test.d.mts +1 -0
- package/dist/codec.test.mjs +13800 -0
- package/dist/codec.test.mjs.map +1 -0
- package/dist/http.d.mts +9 -0
- package/dist/http.d.mts.map +1 -0
- package/dist/http.mjs +41 -0
- package/dist/http.mjs.map +1 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +7 -0
- package/dist/magic-string.es-PAZgLX4h.mjs +1013 -0
- package/dist/magic-string.es-PAZgLX4h.mjs.map +1 -0
- package/dist/models-DnDmylpj.d.mts +22 -0
- package/dist/models-DnDmylpj.d.mts.map +1 -0
- package/dist/models.d.mts +2 -0
- package/dist/models.mjs +1 -0
- package/dist/realtimeStt-ClXWXbXX.d.mts +21 -0
- package/dist/realtimeStt-ClXWXbXX.d.mts.map +1 -0
- package/dist/realtimeStt.d.mts +2 -0
- package/dist/realtimeStt.mjs +136 -0
- package/dist/realtimeStt.mjs.map +1 -0
- package/package.json +82 -0
- package/src/Mistral.ts +189 -0
- package/src/MistralRealtimeTranscriber.ts +60 -0
- package/src/MistralSynthesizer.ts +285 -0
- package/src/MistralTranscriber.ts +224 -0
- package/src/audioCodec.ts +123 -0
- package/src/codec.test.ts +121 -0
- package/src/codec.ts +338 -0
- package/src/http.ts +46 -0
- package/src/index.ts +6 -0
- package/src/models.ts +47 -0
- 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 };
|
package/dist/codec.d.mts
ADDED
|
@@ -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
|