@effect-uai/mistral 0.13.0 → 0.15.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.
@@ -1 +1 @@
1
- {"version":3,"file":"Mistral.d.mts","names":[],"sources":["../src/Mistral.ts"],"mappings":";;;;;;;;;KA+BY,iBAAiB,KAAK;;;;;WAKvB,OAAO;;;;WAIP;;WAEA;;KAGC;;WAED,aAAa,SAAS,mBAAmB,OAAO,OAAO,WAAW,QAAQ;;WAE1E,OAAO,SAAS,mBAAmB,OAAO,OAAO,MAAM,QAAQ;;;;;;;;cAQ7D,gBAAgB;KAIjB;WACD,QAAQ,SAAS;WACjB;;;cAqGE,OAAI,KAAS,WAAS,OAAO,OAAO,uBAAuB,WAAW;;;;;cActE,QAAK,KACX,WACJ,MAAM,MAAM,UAAU,sBAAsB,WAAW"}
1
+ {"version":3,"file":"Mistral.d.mts","names":[],"sources":["../src/Mistral.ts"],"mappings":";;;;;;;;;KAgCY,iBAAiB,KAAK;;;;;WAKvB,OAAO;;;;WAIP;;WAEA;;KAGC;;WAED,aAAa,SAAS,mBAAmB,OAAO,OAAO,WAAW,QAAQ;;WAE1E,OAAO,SAAS,mBAAmB,OAAO,OAAO,MAAM,QAAQ;;;;;;;;cAQ7D,gBAAgB;KAIjB;WACD,QAAQ,SAAS;WACjB;;;cA2GE,OAAI,KAAS,WAAS,OAAO,OAAO,uBAAuB,WAAW;;;;;cActE,QAAK,KACX,WACJ,MAAM,MAAM,UAAU,sBAAsB,WAAW"}
package/dist/Mistral.mjs CHANGED
@@ -4,6 +4,7 @@ import { httpStatusError } from "./http.mjs";
4
4
  import { Context, Effect, Layer, Option, Redacted, Result, Schema, Stream } from "effect";
5
5
  import { HttpClient, HttpClientRequest } from "effect/unstable/http";
6
6
  import * as AiError from "@effect-uai/core/AiError";
7
+ import * as Capabilities from "@effect-uai/core/Capabilities";
7
8
  import { LanguageModel, turnFromStream } from "@effect-uai/core/LanguageModel";
8
9
  import * as SSE from "@effect-uai/core/SSE";
9
10
  import { descriptorsOf } from "@effect-uai/core/Tool";
@@ -47,6 +48,11 @@ const buildStream = (cfg) => {
47
48
  const url = `${cfg.baseUrl ?? "https://api.mistral.ai"}/v1/chat/completions`;
48
49
  return (request) => Stream.unwrap(Effect.gen(function* () {
49
50
  const client = yield* HttpClient.HttpClient;
51
+ yield* Capabilities.warnDroppedBlocks(request.history, "output_image", {
52
+ provider: "mistral",
53
+ capability: "output_image",
54
+ reason: "Assistant messages carry no image on this wire. Use `Turn.imagesAsInput` to resend it as user content."
55
+ });
50
56
  const httpRequest = HttpClientRequest.post(url).pipe(HttpClientRequest.setHeader("authorization", `Bearer ${Redacted.value(cfg.apiKey)}`), HttpClientRequest.bodyJsonUnsafe(buildRequestBody(request)), HttpClientRequest.accept("text/event-stream"));
51
57
  const response = yield* client.execute(httpRequest).pipe(Effect.mapError((cause) => new AiError.Unavailable({
52
58
  provider: "mistral",
@@ -1 +1 @@
1
- {"version":3,"file":"Mistral.mjs","names":[],"sources":["../src/Mistral.ts"],"sourcesContent":["import { Context, Effect, Layer, Option, Redacted, Result, Schema, Stream } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport {\n type CommonRequest,\n LanguageModel,\n type LanguageModelService,\n turnFromStream,\n} from \"@effect-uai/core/LanguageModel\"\nimport * as SSE from \"@effect-uai/core/SSE\"\nimport { descriptorsOf } from \"@effect-uai/core/Tool\"\nimport { type Turn, TurnEvent } from \"@effect-uai/core/Turn\"\nimport {\n type Accumulator,\n type WireChunk,\n accumulatorToTurn,\n applyChunk,\n decodeChunk,\n emptyAccumulator,\n itemsToMessages,\n responseFormatWire,\n toolChoiceWire,\n toolsWire,\n} from \"./codec.js\"\nimport { httpStatusError } from \"./http.js\"\nimport type { MistralModel } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport type MistralRequest = Omit<CommonRequest, \"model\"> & {\n /**\n * Narrows `CommonRequest.model` (`string`) to the typed `MistralModel`\n * literal union for autocomplete.\n */\n readonly model: MistralModel\n /**\n * Prepend a safety system prompt (`safe_prompt` on the wire). Off by default.\n */\n readonly safePrompt?: boolean\n /** Deterministic sampling seed (`random_seed` on the wire). */\n readonly randomSeed?: number\n}\n\nexport type MistralService = {\n /** Stream canonical `TurnEvent`s for one turn. */\n readonly streamTurn: (request: MistralRequest) => Stream.Stream<TurnEvent, AiError.AiError>\n /** Drain a single turn and return the assembled `Turn`. */\n readonly turn: (request: MistralRequest) => Effect.Effect<Turn, AiError.AiError>\n}\n\n/**\n * Provider-typed service tag. Yield this when you want Mistral-specific options\n * (`safePrompt`, `randomSeed`); yield the generic `LanguageModel` tag for\n * provider-portable code. Both are registered by `layer`.\n */\nexport class Mistral extends Context.Service<Mistral, MistralService>()(\n \"@betalyra/effect-uai/providers/mistral/Mistral\",\n) {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n}\n\n// ---------------------------------------------------------------------------\n// Request body\n// ---------------------------------------------------------------------------\n\nconst buildRequestBody = (request: MistralRequest): Record<string, unknown> => {\n const tools = toolsWire(descriptorsOf(request.tools))\n return {\n model: request.model,\n stream: true,\n messages: itemsToMessages(request.history),\n ...(request.temperature !== undefined && { temperature: request.temperature }),\n ...(request.topP !== undefined && { top_p: request.topP }),\n ...(request.maxOutputTokens !== undefined && { max_tokens: request.maxOutputTokens }),\n ...(request.safePrompt !== undefined && { safe_prompt: request.safePrompt }),\n ...(request.randomSeed !== undefined && { random_seed: request.randomSeed }),\n ...(Option.isSome(tools) && { tools: tools.value }),\n ...(request.toolChoice !== undefined && { tool_choice: toolChoiceWire(request.toolChoice) }),\n ...(request.structured !== undefined && {\n response_format: responseFormatWire(request.structured),\n }),\n }\n}\n\n// ---------------------------------------------------------------------------\n// SSE → WireChunk\n// ---------------------------------------------------------------------------\n\nconst parseJsonUnknown = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown))\n\n/**\n * Decode one SSE event's `data` payload into a `WireChunk`. Returns `None`\n * for the `[DONE]` sentinel and for any payload that isn't a chunk we model,\n * so a stray keep-alive never aborts the turn.\n */\nconst decodeEvent = (data: string): Effect.Effect<Option.Option<WireChunk>> =>\n data.trim() === \"[DONE]\"\n ? Effect.succeedNone\n : parseJsonUnknown(data).pipe(Effect.flatMap(decodeChunk), Effect.option)\n\n// ---------------------------------------------------------------------------\n// Stream\n// ---------------------------------------------------------------------------\n\nconst buildStream = (cfg: Config) => {\n const baseUrl = cfg.baseUrl ?? \"https://api.mistral.ai\"\n const url = `${baseUrl}/v1/chat/completions`\n return (\n request: MistralRequest,\n ): Stream.Stream<TurnEvent, AiError.AiError, HttpClient.HttpClient> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const httpRequest = HttpClientRequest.post(url).pipe(\n HttpClientRequest.setHeader(\"authorization\", `Bearer ${Redacted.value(cfg.apiKey)}`),\n HttpClientRequest.bodyJsonUnsafe(buildRequestBody(request)),\n HttpClientRequest.accept(\"text/event-stream\"),\n )\n const response = yield* client\n .execute(httpRequest)\n .pipe(\n Effect.mapError(\n (cause): AiError.AiError =>\n new AiError.Unavailable({ provider: \"mistral\", raw: cause }),\n ),\n )\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return Stream.fail(httpStatusError(response.status, text))\n }\n\n return response.stream.pipe(\n Stream.mapError(\n (cause): AiError.AiError =>\n new AiError.Unavailable({ provider: \"mistral\", raw: cause }),\n ),\n SSE.fromBytes,\n Stream.mapEffect((ev) => decodeEvent(ev.data)),\n Stream.filterMap((chunk) =>\n Option.isSome(chunk) ? Result.succeed(chunk.value) : Result.failVoid,\n ),\n Stream.mapAccum((): Accumulator => emptyAccumulator, applyChunk, {\n // `onHalt` also fires on upstream failure and truncated streams,\n // so only emit `TurnComplete` once a `finish_reason` was observed\n onHalt: (acc) =>\n Option.isSome(acc.finishReason)\n ? [TurnEvent.TurnComplete({ turn: accumulatorToTurn(acc) })]\n : [],\n }),\n )\n }),\n )\n}\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n/** Build a `MistralService` value. For Layer-based setup, prefer `layer`. */\nexport const make = (cfg: Config): Effect.Effect<MistralService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient, (client) => {\n const streamTurn: MistralService[\"streamTurn\"] = (request) =>\n buildStream(cfg)(request).pipe(Stream.provideService(HttpClient.HttpClient, client))\n return {\n streamTurn,\n turn: turnFromStream(streamTurn),\n }\n })\n\n/**\n * Layer that registers both the provider-specific `Mistral` tag and the\n * generic `LanguageModel` tag, sharing one underlying implementation.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<Mistral | LanguageModel, never, HttpClient.HttpClient> => {\n const typed = Layer.effect(Mistral, make(cfg))\n const generic = Layer.effect(\n LanguageModel,\n Effect.map(make(cfg), (s): LanguageModelService => ({\n streamTurn: (request) => s.streamTurn(request as MistralRequest),\n turn: (request) => s.turn(request as MistralRequest),\n })),\n )\n return Layer.merge(typed, generic)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"Mistral.mjs","names":[],"sources":["../src/Mistral.ts"],"sourcesContent":["import { Context, Effect, Layer, Option, Redacted, Result, 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 {\n type CommonRequest,\n LanguageModel,\n type LanguageModelService,\n turnFromStream,\n} from \"@effect-uai/core/LanguageModel\"\nimport * as SSE from \"@effect-uai/core/SSE\"\nimport { descriptorsOf } from \"@effect-uai/core/Tool\"\nimport { type Turn, TurnEvent } from \"@effect-uai/core/Turn\"\nimport {\n type Accumulator,\n type WireChunk,\n accumulatorToTurn,\n applyChunk,\n decodeChunk,\n emptyAccumulator,\n itemsToMessages,\n responseFormatWire,\n toolChoiceWire,\n toolsWire,\n} from \"./codec.js\"\nimport { httpStatusError } from \"./http.js\"\nimport type { MistralModel } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport type MistralRequest = Omit<CommonRequest, \"model\"> & {\n /**\n * Narrows `CommonRequest.model` (`string`) to the typed `MistralModel`\n * literal union for autocomplete.\n */\n readonly model: MistralModel\n /**\n * Prepend a safety system prompt (`safe_prompt` on the wire). Off by default.\n */\n readonly safePrompt?: boolean\n /** Deterministic sampling seed (`random_seed` on the wire). */\n readonly randomSeed?: number\n}\n\nexport type MistralService = {\n /** Stream canonical `TurnEvent`s for one turn. */\n readonly streamTurn: (request: MistralRequest) => Stream.Stream<TurnEvent, AiError.AiError>\n /** Drain a single turn and return the assembled `Turn`. */\n readonly turn: (request: MistralRequest) => Effect.Effect<Turn, AiError.AiError>\n}\n\n/**\n * Provider-typed service tag. Yield this when you want Mistral-specific options\n * (`safePrompt`, `randomSeed`); yield the generic `LanguageModel` tag for\n * provider-portable code. Both are registered by `layer`.\n */\nexport class Mistral extends Context.Service<Mistral, MistralService>()(\n \"@betalyra/effect-uai/providers/mistral/Mistral\",\n) {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n}\n\n// ---------------------------------------------------------------------------\n// Request body\n// ---------------------------------------------------------------------------\n\nconst buildRequestBody = (request: MistralRequest): Record<string, unknown> => {\n const tools = toolsWire(descriptorsOf(request.tools))\n return {\n model: request.model,\n stream: true,\n messages: itemsToMessages(request.history),\n ...(request.temperature !== undefined && { temperature: request.temperature }),\n ...(request.topP !== undefined && { top_p: request.topP }),\n ...(request.maxOutputTokens !== undefined && { max_tokens: request.maxOutputTokens }),\n ...(request.safePrompt !== undefined && { safe_prompt: request.safePrompt }),\n ...(request.randomSeed !== undefined && { random_seed: request.randomSeed }),\n ...(Option.isSome(tools) && { tools: tools.value }),\n ...(request.toolChoice !== undefined && { tool_choice: toolChoiceWire(request.toolChoice) }),\n ...(request.structured !== undefined && {\n response_format: responseFormatWire(request.structured),\n }),\n }\n}\n\n// ---------------------------------------------------------------------------\n// SSE → WireChunk\n// ---------------------------------------------------------------------------\n\nconst parseJsonUnknown = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown))\n\n/**\n * Decode one SSE event's `data` payload into a `WireChunk`. Returns `None`\n * for the `[DONE]` sentinel and for any payload that isn't a chunk we model,\n * so a stray keep-alive never aborts the turn.\n */\nconst decodeEvent = (data: string): Effect.Effect<Option.Option<WireChunk>> =>\n data.trim() === \"[DONE]\"\n ? Effect.succeedNone\n : parseJsonUnknown(data).pipe(Effect.flatMap(decodeChunk), Effect.option)\n\n// ---------------------------------------------------------------------------\n// Stream\n// ---------------------------------------------------------------------------\n\nconst buildStream = (cfg: Config) => {\n const baseUrl = cfg.baseUrl ?? \"https://api.mistral.ai\"\n const url = `${baseUrl}/v1/chat/completions`\n return (\n request: MistralRequest,\n ): Stream.Stream<TurnEvent, AiError.AiError, HttpClient.HttpClient> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n yield* Capabilities.warnDroppedBlocks(request.history, \"output_image\", {\n provider: \"mistral\",\n capability: \"output_image\",\n reason:\n \"Assistant messages carry no image on this wire. Use `Turn.imagesAsInput` to resend it as user content.\",\n })\n const httpRequest = HttpClientRequest.post(url).pipe(\n HttpClientRequest.setHeader(\"authorization\", `Bearer ${Redacted.value(cfg.apiKey)}`),\n HttpClientRequest.bodyJsonUnsafe(buildRequestBody(request)),\n HttpClientRequest.accept(\"text/event-stream\"),\n )\n const response = yield* client\n .execute(httpRequest)\n .pipe(\n Effect.mapError(\n (cause): AiError.AiError =>\n new AiError.Unavailable({ provider: \"mistral\", raw: cause }),\n ),\n )\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return Stream.fail(httpStatusError(response.status, text))\n }\n\n return response.stream.pipe(\n Stream.mapError(\n (cause): AiError.AiError =>\n new AiError.Unavailable({ provider: \"mistral\", raw: cause }),\n ),\n SSE.fromBytes,\n Stream.mapEffect((ev) => decodeEvent(ev.data)),\n Stream.filterMap((chunk) =>\n Option.isSome(chunk) ? Result.succeed(chunk.value) : Result.failVoid,\n ),\n Stream.mapAccum((): Accumulator => emptyAccumulator, applyChunk, {\n // `onHalt` also fires on upstream failure and truncated streams,\n // so only emit `TurnComplete` once a `finish_reason` was observed\n onHalt: (acc) =>\n Option.isSome(acc.finishReason)\n ? [TurnEvent.TurnComplete({ turn: accumulatorToTurn(acc) })]\n : [],\n }),\n )\n }),\n )\n}\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n/** Build a `MistralService` value. For Layer-based setup, prefer `layer`. */\nexport const make = (cfg: Config): Effect.Effect<MistralService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient, (client) => {\n const streamTurn: MistralService[\"streamTurn\"] = (request) =>\n buildStream(cfg)(request).pipe(Stream.provideService(HttpClient.HttpClient, client))\n return {\n streamTurn,\n turn: turnFromStream(streamTurn),\n }\n })\n\n/**\n * Layer that registers both the provider-specific `Mistral` tag and the\n * generic `LanguageModel` tag, sharing one underlying implementation.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<Mistral | LanguageModel, never, HttpClient.HttpClient> => {\n const typed = Layer.effect(Mistral, make(cfg))\n const generic = Layer.effect(\n LanguageModel,\n Effect.map(make(cfg), (s): LanguageModelService => ({\n streamTurn: (request) => s.streamTurn(request as MistralRequest),\n turn: (request) => s.turn(request as MistralRequest),\n })),\n )\n return Layer.merge(typed, generic)\n}\n"],"mappings":"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"}
@@ -1,11 +1,11 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
- import { httpStatusError, transportFailure } from "./http.mjs";
2
+ import { bodyMultipart, httpStatusError, transportFailure } from "./http.mjs";
3
3
  import { audioToBlob, defaultFileName } from "./audioCodec.mjs";
4
4
  import { Array, Context, Effect, Layer, Schema, Stream } from "effect";
5
5
  import { HttpClient, HttpClientRequest } from "effect/unstable/http";
6
6
  import * as AiError from "@effect-uai/core/AiError";
7
- import { Transcriber } from "@effect-uai/core/Transcriber";
8
7
  import * as Capabilities from "@effect-uai/core/Capabilities";
8
+ import { Transcriber } from "@effect-uai/core/Transcriber";
9
9
  import { isAudioUrl } from "@effect-uai/core/Audio";
10
10
  //#region src/MistralTranscriber.ts
11
11
  var MistralTranscriber_exports = /* @__PURE__ */ __exportAll({
@@ -72,8 +72,8 @@ const toResult = (raw, decoded) => ({
72
72
  const transcribeImpl = (cfg) => (request) => Effect.gen(function* () {
73
73
  const client = yield* HttpClient.HttpClient;
74
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));
75
+ const withBody = yield* Effect.flatMap(buildFormData(request), bodyMultipart);
76
+ const httpRequest = HttpClientRequest.post(`${baseUrl}/v1/audio/transcriptions`).pipe(HttpClientRequest.bearerToken(cfg.apiKey), withBody);
77
77
  const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure));
78
78
  if (response.status >= 400) {
79
79
  const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""));
@@ -1 +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((blob): Part => [\n \"file\",\n blob,\n 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, (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(make(cfg), (s): TranscriberService => ({\n transcribe: (req: CommonTranscribeRequest) => s.transcribe(req as MistralTranscribeRequest),\n streamTranscriptionFrom: s.streamTranscriptionFrom,\n })),\n ),\n )\n"],"mappings":"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"}
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 { bodyMultipart, 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((blob): Part => [\n \"file\",\n blob,\n 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 withBody = yield* Effect.flatMap(buildFormData(request), bodyMultipart)\n const httpRequest = HttpClientRequest.post(`${baseUrl}/v1/audio/transcriptions`).pipe(\n HttpClientRequest.bearerToken(cfg.apiKey),\n withBody,\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, (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(make(cfg), (s): TranscriberService => ({\n transcribe: (req: CommonTranscribeRequest) => s.transcribe(req as MistralTranscribeRequest),\n streamTranscriptionFrom: s.streamTranscriptionFrom,\n })),\n ),\n )\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"codec.d.mts","names":[],"sources":["../src/codec.ts"],"mappings":";;;;;;KA4BK;EACH;EACA,kBAAkB,cAAc;EAChC,aAAa,cAAc;EAC3B;;;cAyEW,kBAAe,OAAW,cAAc,iBAAe,cAAc;KAO7E;WAIU;WAA2B;;cAG7B,iBAAc,QAAY,wBAAsB;cAyBhD,cAAW,eAAiB,OAAO,OAAO"}
1
+ {"version":3,"file":"codec.d.mts","names":[],"sources":["../src/codec.ts"],"mappings":";;;;;;KA4BK;EACH;EACA,kBAAkB,cAAc;EAChC,aAAa,cAAc;EAC3B;;;cAkFW,kBAAe,OAAW,cAAc,iBAAe,cAAc;KAO7E;WAIU;WAA2B;;cAG7B,iBAAc,QAAY,wBAAsB;cAyBhD,cAAW,eAAiB,OAAO,OAAO"}
package/dist/codec.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
- import { Array, Effect, Encoding, Match } from "effect";
2
+ import { Array, Effect, Encoding, Match, Result } from "effect";
3
3
  import { accumulatorToTurn, applyChunk, decodeChunk as decodeChunk$1, emptyAccumulator, responseFormatWire, toolsWire } from "@effect-uai/chat-completions/codec";
4
4
  //#region src/codec.ts
5
5
  var codec_exports = /* @__PURE__ */ __exportAll({
@@ -30,16 +30,20 @@ const blockToPart = Match.type().pipe(Match.discriminatorsExhaustive("type")({
30
30
  input_image: (b) => ({
31
31
  type: "image_url",
32
32
  image_url: imageSourceToUrl(b.source)
33
- })
33
+ }),
34
+ output_image: () => void 0
34
35
  }));
35
36
  const isTextBlock = (b) => b.type === "input_text" || b.type === "output_text" || b.type === "refusal";
36
- const textOfBlock = (b) => b.type === "input_image" ? "" : b.text;
37
+ const textOfBlock = (b) => b.type === "input_image" || b.type === "output_image" ? "" : b.text;
37
38
  /**
38
39
  * Collapse a message's content to a plain string when it is text-only
39
40
  * (the common case Mistral prefers), otherwise emit the multimodal parts
40
41
  * array.
41
42
  */
42
- const encodeContent = (content) => Array.every(content, isTextBlock) ? content.map(textOfBlock).join("") : content.map(blockToPart);
43
+ const encodeContent = (content) => Array.every(content, isTextBlock) ? content.map(textOfBlock).join("") : Array.filterMap(content, (b) => {
44
+ const part = blockToPart(b);
45
+ return part === void 0 ? Result.failVoid : Result.succeed(part);
46
+ });
43
47
  const toolCallWire = (call_id, name, args) => ({
44
48
  id: call_id,
45
49
  type: "function",
@@ -1 +1 @@
1
- {"version":3,"file":"codec.mjs","names":["Arr","decodeChunkShared"],"sources":["../src/codec.ts"],"sourcesContent":["import { Array as Arr, Effect, Encoding, Match, Option } from \"effect\"\nimport type { ContentBlock, HistoryItem, InputImage } from \"@effect-uai/core/Items\"\n\n// The streaming decoder, tool encoding, and terminal-turn assembly are the\n// generic OpenAI chat-completions codec: Mistral speaks that dialect on the\n// wire, so it reuses them verbatim. Only request encoding diverges (below).\nexport {\n type Accumulator,\n type WireChunk,\n accumulatorToTurn,\n applyChunk,\n emptyAccumulator,\n responseFormatWire,\n toolsWire,\n} from \"@effect-uai/chat-completions/codec\"\nimport {\n type WireChunk,\n decodeChunk as decodeChunkShared,\n} from \"@effect-uai/chat-completions/codec\"\n\n// ---------------------------------------------------------------------------\n// History → Mistral chat `messages`\n//\n// Same shape as the generic codec, with one wire quirk: Mistral takes a bare\n// `image_url` string, not OpenAI's `image_url: { url }` object. That is the\n// reason this half stays local rather than importing `itemsToMessages`.\n// ---------------------------------------------------------------------------\n\ntype WireMessage = {\n role: string\n content: string | ReadonlyArray<Record<string, unknown>>\n tool_calls?: ReadonlyArray<Record<string, unknown>>\n tool_call_id?: string\n}\n\n/** A `data:` URI for inline image bytes; URLs pass through untouched. */\nconst imageSourceToUrl = Match.type<InputImage[\"source\"]>().pipe(\n Match.tag(\"url\", (s) => s.url),\n Match.tag(\"base64\", (s) => `data:${s.mimeType};base64,${s.base64}`),\n Match.tag(\"bytes\", (s) => `data:${s.mimeType};base64,${Encoding.encodeBase64(s.bytes)}`),\n Match.exhaustive,\n)\n\nconst blockToPart = Match.type<ContentBlock>().pipe(\n Match.discriminatorsExhaustive(\"type\")({\n input_text: (b) => ({ type: \"text\", text: b.text }),\n output_text: (b) => ({ type: \"text\", text: b.text }),\n refusal: (b) => ({ type: \"text\", text: b.text }),\n // Bare string, not `{ url }`: the Mistral divergence from OpenAI.\n input_image: (b) => ({ type: \"image_url\", image_url: imageSourceToUrl(b.source) }),\n }),\n)\n\nconst isTextBlock = (b: ContentBlock): boolean =>\n b.type === \"input_text\" || b.type === \"output_text\" || b.type === \"refusal\"\n\nconst textOfBlock = (b: ContentBlock): string => (b.type === \"input_image\" ? \"\" : b.text)\n\n/**\n * Collapse a message's content to a plain string when it is text-only\n * (the common case Mistral prefers), otherwise emit the multimodal parts\n * array.\n */\nconst encodeContent = (\n content: ReadonlyArray<ContentBlock>,\n): string | ReadonlyArray<Record<string, unknown>> =>\n Arr.every(content, isTextBlock) ? content.map(textOfBlock).join(\"\") : content.map(blockToPart)\n\nconst toolCallWire = (call_id: string, name: string, args: string): Record<string, unknown> => ({\n id: call_id,\n type: \"function\",\n function: { name, arguments: args },\n})\n\nconst appendToolCall = (\n acc: ReadonlyArray<WireMessage>,\n call_id: string,\n name: string,\n args: string,\n): ReadonlyArray<WireMessage> => {\n const last = acc[acc.length - 1]\n const wire = toolCallWire(call_id, name, args)\n // Attach to the immediately-preceding assistant message (not a tool result),\n // matching the OpenAI/Mistral shape where tool calls ride the assistant turn.\n if (last !== undefined && last.role === \"assistant\" && last.tool_call_id === undefined) {\n return [...acc.slice(0, -1), { ...last, tool_calls: [...(last.tool_calls ?? []), wire] }]\n }\n return [...acc, { role: \"assistant\", content: \"\", tool_calls: [wire] }]\n}\n\nconst foldItem = (acc: ReadonlyArray<WireMessage>, item: HistoryItem): ReadonlyArray<WireMessage> =>\n Match.value(item).pipe(\n Match.discriminatorsExhaustive(\"type\")({\n message: (m) => [...acc, { role: m.role, content: encodeContent(m.content) }],\n function_call: (f) => appendToolCall(acc, f.call_id, f.name, f.arguments),\n function_call_output: (o) => [\n ...acc,\n { role: \"tool\", tool_call_id: o.call_id, content: o.output },\n ],\n // Reasoning items are not replayed to the chat-completions endpoint.\n reasoning: () => acc,\n }),\n )\n\n/** Convert our `HistoryItem[]` history into Mistral chat `messages`. */\nexport const itemsToMessages = (items: ReadonlyArray<HistoryItem>): ReadonlyArray<WireMessage> =>\n Arr.reduce(items, [] as ReadonlyArray<WireMessage>, foldItem)\n\n// ---------------------------------------------------------------------------\n// tool_choice\n// ---------------------------------------------------------------------------\n\ntype ToolChoice =\n | \"auto\"\n | \"required\"\n | \"none\"\n | { readonly type: \"function\"; readonly name: string }\n\n// Mistral uses \"any\" for forced tool use (OpenAI's \"required\").\nexport const toolChoiceWire = (choice: ToolChoice): string | Record<string, unknown> =>\n Match.value(choice).pipe(\n Match.when(\"auto\", () => \"auto\" as const),\n Match.when(\"required\", () => \"any\" as const),\n Match.when(\"none\", () => \"none\" as const),\n Match.orElse((c) => ({ type: \"function\", function: { name: c.name } })),\n )\n\n// ---------------------------------------------------------------------------\n// Streaming decode\n// ---------------------------------------------------------------------------\n\n// Mistral emits `model_length` when it truncates on the context window; the\n// shared decoder only knows OpenAI's `length`. Rewrite it before folding so the\n// shared accumulator maps it to `max_tokens` rather than the `stop` fallback.\nconst normalizeChunk = (chunk: WireChunk): WireChunk =>\n chunk.choices === undefined\n ? chunk\n : {\n ...chunk,\n choices: chunk.choices.map((c) =>\n c.finish_reason === \"model_length\" ? { ...c, finish_reason: \"length\" } : c,\n ),\n }\n\nexport const decodeChunk = (u: unknown): Effect.Effect<WireChunk, unknown> =>\n decodeChunkShared(u).pipe(Effect.map(normalizeChunk))\n"],"mappings":";;;;;;;;;;;;;;;AAoCA,MAAM,mBAAmB,MAAM,KAA2B,CAAC,CAAC,KAC1D,MAAM,IAAI,QAAQ,MAAM,EAAE,GAAG,GAC7B,MAAM,IAAI,WAAW,MAAM,QAAQ,EAAE,SAAS,UAAU,EAAE,QAAQ,GAClE,MAAM,IAAI,UAAU,MAAM,QAAQ,EAAE,SAAS,UAAU,SAAS,aAAa,EAAE,KAAK,GAAG,GACvF,MAAM,UACR;AAEA,MAAM,cAAc,MAAM,KAAmB,CAAC,CAAC,KAC7C,MAAM,yBAAyB,MAAM,CAAC,CAAC;CACrC,aAAa,OAAO;EAAE,MAAM;EAAQ,MAAM,EAAE;CAAK;CACjD,cAAc,OAAO;EAAE,MAAM;EAAQ,MAAM,EAAE;CAAK;CAClD,UAAU,OAAO;EAAE,MAAM;EAAQ,MAAM,EAAE;CAAK;CAE9C,cAAc,OAAO;EAAE,MAAM;EAAa,WAAW,iBAAiB,EAAE,MAAM;CAAE;AAClF,CAAC,CACH;AAEA,MAAM,eAAe,MACnB,EAAE,SAAS,gBAAgB,EAAE,SAAS,iBAAiB,EAAE,SAAS;AAEpE,MAAM,eAAe,MAA6B,EAAE,SAAS,gBAAgB,KAAK,EAAE;;;;;;AAOpF,MAAM,iBACJ,YAEAA,MAAI,MAAM,SAAS,WAAW,IAAI,QAAQ,IAAI,WAAW,CAAC,CAAC,KAAK,EAAE,IAAI,QAAQ,IAAI,WAAW;AAE/F,MAAM,gBAAgB,SAAiB,MAAc,UAA2C;CAC9F,IAAI;CACJ,MAAM;CACN,UAAU;EAAE;EAAM,WAAW;CAAK;AACpC;AAEA,MAAM,kBACJ,KACA,SACA,MACA,SAC+B;CAC/B,MAAM,OAAO,IAAI,IAAI,SAAS;CAC9B,MAAM,OAAO,aAAa,SAAS,MAAM,IAAI;CAG7C,IAAI,SAAS,KAAA,KAAa,KAAK,SAAS,eAAe,KAAK,iBAAiB,KAAA,GAC3E,OAAO,CAAC,GAAG,IAAI,MAAM,GAAG,EAAE,GAAG;EAAE,GAAG;EAAM,YAAY,CAAC,GAAI,KAAK,cAAc,CAAC,GAAI,IAAI;CAAE,CAAC;CAE1F,OAAO,CAAC,GAAG,KAAK;EAAE,MAAM;EAAa,SAAS;EAAI,YAAY,CAAC,IAAI;CAAE,CAAC;AACxE;AAEA,MAAM,YAAY,KAAiC,SACjD,MAAM,MAAM,IAAI,CAAC,CAAC,KAChB,MAAM,yBAAyB,MAAM,CAAC,CAAC;CACrC,UAAU,MAAM,CAAC,GAAG,KAAK;EAAE,MAAM,EAAE;EAAM,SAAS,cAAc,EAAE,OAAO;CAAE,CAAC;CAC5E,gBAAgB,MAAM,eAAe,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS;CACxE,uBAAuB,MAAM,CAC3B,GAAG,KACH;EAAE,MAAM;EAAQ,cAAc,EAAE;EAAS,SAAS,EAAE;CAAO,CAC7D;CAEA,iBAAiB;AACnB,CAAC,CACH;;AAGF,MAAa,mBAAmB,UAC9BA,MAAI,OAAO,OAAO,CAAC,GAAiC,QAAQ;AAa9D,MAAa,kBAAkB,WAC7B,MAAM,MAAM,MAAM,CAAC,CAAC,KAClB,MAAM,KAAK,cAAc,MAAe,GACxC,MAAM,KAAK,kBAAkB,KAAc,GAC3C,MAAM,KAAK,cAAc,MAAe,GACxC,MAAM,QAAQ,OAAO;CAAE,MAAM;CAAY,UAAU,EAAE,MAAM,EAAE,KAAK;AAAE,EAAE,CACxE;AASF,MAAM,kBAAkB,UACtB,MAAM,YAAY,KAAA,IACd,QACA;CACE,GAAG;CACH,SAAS,MAAM,QAAQ,KAAK,MAC1B,EAAE,kBAAkB,iBAAiB;EAAE,GAAG;EAAG,eAAe;CAAS,IAAI,CAC3E;AACF;AAEN,MAAa,eAAe,MAC1BC,cAAkB,CAAC,CAAC,CAAC,KAAK,OAAO,IAAI,cAAc,CAAC"}
1
+ {"version":3,"file":"codec.mjs","names":["Arr","decodeChunkShared"],"sources":["../src/codec.ts"],"sourcesContent":["import { Array as Arr, Effect, Encoding, Match, Option, Result } from \"effect\"\nimport type { ContentBlock, HistoryItem, InputImage } from \"@effect-uai/core/Items\"\n\n// The streaming decoder, tool encoding, and terminal-turn assembly are the\n// generic OpenAI chat-completions codec: Mistral speaks that dialect on the\n// wire, so it reuses them verbatim. Only request encoding diverges (below).\nexport {\n type Accumulator,\n type WireChunk,\n accumulatorToTurn,\n applyChunk,\n emptyAccumulator,\n responseFormatWire,\n toolsWire,\n} from \"@effect-uai/chat-completions/codec\"\nimport {\n type WireChunk,\n decodeChunk as decodeChunkShared,\n} from \"@effect-uai/chat-completions/codec\"\n\n// ---------------------------------------------------------------------------\n// History → Mistral chat `messages`\n//\n// Same shape as the generic codec, with one wire quirk: Mistral takes a bare\n// `image_url` string, not OpenAI's `image_url: { url }` object. That is the\n// reason this half stays local rather than importing `itemsToMessages`.\n// ---------------------------------------------------------------------------\n\ntype WireMessage = {\n role: string\n content: string | ReadonlyArray<Record<string, unknown>>\n tool_calls?: ReadonlyArray<Record<string, unknown>>\n tool_call_id?: string\n}\n\n/** A `data:` URI for inline image bytes; URLs pass through untouched. */\nconst imageSourceToUrl = Match.type<InputImage[\"source\"]>().pipe(\n Match.tag(\"url\", (s) => s.url),\n Match.tag(\"base64\", (s) => `data:${s.mimeType};base64,${s.base64}`),\n Match.tag(\"bytes\", (s) => `data:${s.mimeType};base64,${Encoding.encodeBase64(s.bytes)}`),\n Match.exhaustive,\n)\n\nconst blockToPart = Match.type<ContentBlock>().pipe(\n Match.discriminatorsExhaustive(\"type\")({\n input_text: (b) => ({ type: \"text\", text: b.text }),\n output_text: (b) => ({ type: \"text\", text: b.text }),\n refusal: (b) => ({ type: \"text\", text: b.text }),\n // Bare string, not `{ url }`: the Mistral divergence from OpenAI.\n input_image: (b) => ({ type: \"image_url\", image_url: imageSourceToUrl(b.source) }),\n // No assistant-image part here. Dropped rather than relocated onto a\n // user message, which is the caller's call: `Turn.imagesAsInput`.\n output_image: () => undefined,\n }),\n)\n\nconst isTextBlock = (b: ContentBlock): boolean =>\n b.type === \"input_text\" || b.type === \"output_text\" || b.type === \"refusal\"\n\nconst textOfBlock = (b: ContentBlock): string =>\n b.type === \"input_image\" || b.type === \"output_image\" ? \"\" : b.text\n\n/**\n * Collapse a message's content to a plain string when it is text-only\n * (the common case Mistral prefers), otherwise emit the multimodal parts\n * array.\n */\nconst encodeContent = (\n content: ReadonlyArray<ContentBlock>,\n): string | ReadonlyArray<Record<string, unknown>> =>\n Arr.every(content, isTextBlock)\n ? content.map(textOfBlock).join(\"\")\n : Arr.filterMap(content, (b) => {\n const part = blockToPart(b)\n return part === undefined ? Result.failVoid : Result.succeed(part)\n })\n\nconst toolCallWire = (call_id: string, name: string, args: string): Record<string, unknown> => ({\n id: call_id,\n type: \"function\",\n function: { name, arguments: args },\n})\n\nconst appendToolCall = (\n acc: ReadonlyArray<WireMessage>,\n call_id: string,\n name: string,\n args: string,\n): ReadonlyArray<WireMessage> => {\n const last = acc[acc.length - 1]\n const wire = toolCallWire(call_id, name, args)\n // Attach to the immediately-preceding assistant message (not a tool result),\n // matching the OpenAI/Mistral shape where tool calls ride the assistant turn.\n if (last !== undefined && last.role === \"assistant\" && last.tool_call_id === undefined) {\n return [...acc.slice(0, -1), { ...last, tool_calls: [...(last.tool_calls ?? []), wire] }]\n }\n return [...acc, { role: \"assistant\", content: \"\", tool_calls: [wire] }]\n}\n\nconst foldItem = (acc: ReadonlyArray<WireMessage>, item: HistoryItem): ReadonlyArray<WireMessage> =>\n Match.value(item).pipe(\n Match.discriminatorsExhaustive(\"type\")({\n message: (m) => [...acc, { role: m.role, content: encodeContent(m.content) }],\n function_call: (f) => appendToolCall(acc, f.call_id, f.name, f.arguments),\n function_call_output: (o) => [\n ...acc,\n { role: \"tool\", tool_call_id: o.call_id, content: o.output },\n ],\n // Reasoning items are not replayed to the chat-completions endpoint.\n reasoning: () => acc,\n }),\n )\n\n/** Convert our `HistoryItem[]` history into Mistral chat `messages`. */\nexport const itemsToMessages = (items: ReadonlyArray<HistoryItem>): ReadonlyArray<WireMessage> =>\n Arr.reduce(items, [] as ReadonlyArray<WireMessage>, foldItem)\n\n// ---------------------------------------------------------------------------\n// tool_choice\n// ---------------------------------------------------------------------------\n\ntype ToolChoice =\n | \"auto\"\n | \"required\"\n | \"none\"\n | { readonly type: \"function\"; readonly name: string }\n\n// Mistral uses \"any\" for forced tool use (OpenAI's \"required\").\nexport const toolChoiceWire = (choice: ToolChoice): string | Record<string, unknown> =>\n Match.value(choice).pipe(\n Match.when(\"auto\", () => \"auto\" as const),\n Match.when(\"required\", () => \"any\" as const),\n Match.when(\"none\", () => \"none\" as const),\n Match.orElse((c) => ({ type: \"function\", function: { name: c.name } })),\n )\n\n// ---------------------------------------------------------------------------\n// Streaming decode\n// ---------------------------------------------------------------------------\n\n// Mistral emits `model_length` when it truncates on the context window; the\n// shared decoder only knows OpenAI's `length`. Rewrite it before folding so the\n// shared accumulator maps it to `max_tokens` rather than the `stop` fallback.\nconst normalizeChunk = (chunk: WireChunk): WireChunk =>\n chunk.choices === undefined\n ? chunk\n : {\n ...chunk,\n choices: chunk.choices.map((c) =>\n c.finish_reason === \"model_length\" ? { ...c, finish_reason: \"length\" } : c,\n ),\n }\n\nexport const decodeChunk = (u: unknown): Effect.Effect<WireChunk, unknown> =>\n decodeChunkShared(u).pipe(Effect.map(normalizeChunk))\n"],"mappings":"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"}
package/dist/http.d.mts CHANGED
@@ -1,8 +1,12 @@
1
+ import { Effect } from "effect";
2
+ import { HttpClientRequest } from "effect/unstable/http";
1
3
  import * as AiError from "@effect-uai/core/AiError";
2
4
  //#region src/http.d.ts
3
5
  /** Map an HTTP status from any Mistral endpoint to an `AiError` variant. */
4
6
  declare const httpStatusError: (status: number, body: string) => AiError.AiError;
5
7
  declare const transportFailure: (cause: unknown) => AiError.AiError;
8
+ /** Core's helper, with our transport error. Never pass `FormData` to the client. */
9
+ declare const bodyMultipart: (form: FormData) => Effect.Effect<(request: HttpClientRequest.HttpClientRequest) => HttpClientRequest.HttpClientRequest, AiError.AiError>;
6
10
  //#endregion
7
- export { httpStatusError, transportFailure };
11
+ export { bodyMultipart, httpStatusError, transportFailure };
8
12
  //# sourceMappingURL=http.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.mts","names":[],"sources":["../src/http.ts"],"mappings":";;;cAIa,kBAAkB,gBAAgB,iBAAiB,QAAQ;cAwC3D,mBAAgB,mBAAqB,QAAQ"}
1
+ {"version":3,"file":"http.d.mts","names":[],"sources":["../src/http.ts"],"mappings":";;;;;cAMa,kBAAkB,gBAAgB,iBAAiB,QAAQ;cAwC3D,mBAAgB,mBAAqB,QAAQ;;cAI7C,gBAAa,MAClB,aACL,OAAO,QACP,SAAS,kBAAkB,sBAAsB,kBAAkB,mBACpE,QAAQ"}
package/dist/http.mjs CHANGED
@@ -1,5 +1,6 @@
1
- import { Match } from "effect";
1
+ import { Effect, Match } from "effect";
2
2
  import * as AiError from "@effect-uai/core/AiError";
3
+ import * as Multipart from "@effect-uai/core/Multipart";
3
4
  //#region src/http.ts
4
5
  /** Map an HTTP status from any Mistral endpoint to an `AiError` variant. */
5
6
  const httpStatusError = (status, body) => Match.value(status).pipe(Match.when(429, () => new AiError.RateLimited({
@@ -35,7 +36,9 @@ const transportFailure = (cause) => new AiError.Unavailable({
35
36
  provider: "mistral",
36
37
  raw: cause
37
38
  });
39
+ /** Core's helper, with our transport error. Never pass `FormData` to the client. */
40
+ const bodyMultipart = (form) => Multipart.bodyMultipart(form).pipe(Effect.mapError(transportFailure));
38
41
  //#endregion
39
- export { httpStatusError, transportFailure };
42
+ export { bodyMultipart, httpStatusError, transportFailure };
40
43
 
41
44
  //# sourceMappingURL=http.mjs.map
package/dist/http.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"http.mjs","names":[],"sources":["../src/http.ts"],"sourcesContent":["import { Match } from \"effect\"\nimport * as AiError from \"@effect-uai/core/AiError\"\n\n/** Map an HTTP status from any Mistral endpoint to an `AiError` variant. */\nexport const httpStatusError: (status: number, body: string) => AiError.AiError = (status, body) =>\n Match.value(status).pipe(\n Match.when(\n 429,\n (): AiError.AiError => new AiError.RateLimited({ provider: \"mistral\", raw: body }),\n ),\n Match.whenOr(\n 408,\n 504,\n (): AiError.AiError => new AiError.Timeout({ provider: \"mistral\", raw: body }),\n ),\n Match.when(\n 401,\n (): AiError.AiError =>\n new AiError.AuthFailed({ provider: \"mistral\", subtype: \"auth\", raw: body }),\n ),\n Match.when(\n 403,\n (): AiError.AiError =>\n new AiError.AuthFailed({ provider: \"mistral\", subtype: \"permission\", raw: body }),\n ),\n Match.when(\n 402,\n (): AiError.AiError =>\n new AiError.AuthFailed({ provider: \"mistral\", subtype: \"billing\", raw: body }),\n ),\n Match.when(\n 413,\n (): AiError.AiError => new AiError.ContextLengthExceeded({ provider: \"mistral\", raw: body }),\n ),\n Match.when(\n (n) => n >= 500,\n (n): AiError.AiError =>\n new AiError.Unavailable({ provider: \"mistral\", status: n, raw: body }),\n ),\n Match.orElse(\n (): AiError.AiError => new AiError.InvalidRequest({ provider: \"mistral\", raw: body }),\n ),\n )\n\nexport const transportFailure = (cause: unknown): AiError.AiError =>\n new AiError.Unavailable({ provider: \"mistral\", raw: cause })\n"],"mappings":";;;;AAIA,MAAa,mBAAsE,QAAQ,SACzF,MAAM,MAAM,MAAM,CAAC,CAAC,KAClB,MAAM,KACJ,WACuB,IAAI,QAAQ,YAAY;CAAE,UAAU;CAAW,KAAK;AAAK,CAAC,CACnF,GACA,MAAM,OACJ,KACA,WACuB,IAAI,QAAQ,QAAQ;CAAE,UAAU;CAAW,KAAK;AAAK,CAAC,CAC/E,GACA,MAAM,KACJ,WAEE,IAAI,QAAQ,WAAW;CAAE,UAAU;CAAW,SAAS;CAAQ,KAAK;AAAK,CAAC,CAC9E,GACA,MAAM,KACJ,WAEE,IAAI,QAAQ,WAAW;CAAE,UAAU;CAAW,SAAS;CAAc,KAAK;AAAK,CAAC,CACpF,GACA,MAAM,KACJ,WAEE,IAAI,QAAQ,WAAW;CAAE,UAAU;CAAW,SAAS;CAAW,KAAK;AAAK,CAAC,CACjF,GACA,MAAM,KACJ,WACuB,IAAI,QAAQ,sBAAsB;CAAE,UAAU;CAAW,KAAK;AAAK,CAAC,CAC7F,GACA,MAAM,MACH,MAAM,KAAK,MACX,MACC,IAAI,QAAQ,YAAY;CAAE,UAAU;CAAW,QAAQ;CAAG,KAAK;AAAK,CAAC,CACzE,GACA,MAAM,aACmB,IAAI,QAAQ,eAAe;CAAE,UAAU;CAAW,KAAK;AAAK,CAAC,CACtF,CACF;AAEF,MAAa,oBAAoB,UAC/B,IAAI,QAAQ,YAAY;CAAE,UAAU;CAAW,KAAK;AAAM,CAAC"}
1
+ {"version":3,"file":"http.mjs","names":[],"sources":["../src/http.ts"],"sourcesContent":["import { Effect, Match } from \"effect\"\nimport { HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport * as Multipart from \"@effect-uai/core/Multipart\"\n\n/** Map an HTTP status from any Mistral endpoint to an `AiError` variant. */\nexport const httpStatusError: (status: number, body: string) => AiError.AiError = (status, body) =>\n Match.value(status).pipe(\n Match.when(\n 429,\n (): AiError.AiError => new AiError.RateLimited({ provider: \"mistral\", raw: body }),\n ),\n Match.whenOr(\n 408,\n 504,\n (): AiError.AiError => new AiError.Timeout({ provider: \"mistral\", raw: body }),\n ),\n Match.when(\n 401,\n (): AiError.AiError =>\n new AiError.AuthFailed({ provider: \"mistral\", subtype: \"auth\", raw: body }),\n ),\n Match.when(\n 403,\n (): AiError.AiError =>\n new AiError.AuthFailed({ provider: \"mistral\", subtype: \"permission\", raw: body }),\n ),\n Match.when(\n 402,\n (): AiError.AiError =>\n new AiError.AuthFailed({ provider: \"mistral\", subtype: \"billing\", raw: body }),\n ),\n Match.when(\n 413,\n (): AiError.AiError => new AiError.ContextLengthExceeded({ provider: \"mistral\", raw: body }),\n ),\n Match.when(\n (n) => n >= 500,\n (n): AiError.AiError =>\n new AiError.Unavailable({ provider: \"mistral\", status: n, raw: body }),\n ),\n Match.orElse(\n (): AiError.AiError => new AiError.InvalidRequest({ provider: \"mistral\", raw: body }),\n ),\n )\n\nexport const transportFailure = (cause: unknown): AiError.AiError =>\n new AiError.Unavailable({ provider: \"mistral\", raw: cause })\n\n/** Core's helper, with our transport error. Never pass `FormData` to the client. */\nexport const bodyMultipart = (\n form: FormData,\n): Effect.Effect<\n (request: HttpClientRequest.HttpClientRequest) => HttpClientRequest.HttpClientRequest,\n AiError.AiError\n> => Multipart.bodyMultipart(form).pipe(Effect.mapError(transportFailure))\n"],"mappings":";;;;;AAMA,MAAa,mBAAsE,QAAQ,SACzF,MAAM,MAAM,MAAM,CAAC,CAAC,KAClB,MAAM,KACJ,WACuB,IAAI,QAAQ,YAAY;CAAE,UAAU;CAAW,KAAK;AAAK,CAAC,CACnF,GACA,MAAM,OACJ,KACA,WACuB,IAAI,QAAQ,QAAQ;CAAE,UAAU;CAAW,KAAK;AAAK,CAAC,CAC/E,GACA,MAAM,KACJ,WAEE,IAAI,QAAQ,WAAW;CAAE,UAAU;CAAW,SAAS;CAAQ,KAAK;AAAK,CAAC,CAC9E,GACA,MAAM,KACJ,WAEE,IAAI,QAAQ,WAAW;CAAE,UAAU;CAAW,SAAS;CAAc,KAAK;AAAK,CAAC,CACpF,GACA,MAAM,KACJ,WAEE,IAAI,QAAQ,WAAW;CAAE,UAAU;CAAW,SAAS;CAAW,KAAK;AAAK,CAAC,CACjF,GACA,MAAM,KACJ,WACuB,IAAI,QAAQ,sBAAsB;CAAE,UAAU;CAAW,KAAK;AAAK,CAAC,CAC7F,GACA,MAAM,MACH,MAAM,KAAK,MACX,MACC,IAAI,QAAQ,YAAY;CAAE,UAAU;CAAW,QAAQ;CAAG,KAAK;AAAK,CAAC,CACzE,GACA,MAAM,aACmB,IAAI,QAAQ,eAAe;CAAE,UAAU;CAAW,KAAK;AAAK,CAAC,CACtF,CACF;AAEF,MAAa,oBAAoB,UAC/B,IAAI,QAAQ,YAAY;CAAE,UAAU;CAAW,KAAK;AAAM,CAAC;;AAG7D,MAAa,iBACX,SAIG,UAAU,cAAc,IAAI,CAAC,CAAC,KAAK,OAAO,SAAS,gBAAgB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-uai/mistral",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Mistral provider for @effect-uai/core: chat-completions plus Voxtral speech-to-text and text-to-speech.",
5
5
  "keywords": [
6
6
  "ai",
@@ -59,14 +59,14 @@
59
59
  "access": "public"
60
60
  },
61
61
  "dependencies": {
62
- "@effect-uai/chat-completions": "0.13.0"
62
+ "@effect-uai/chat-completions": "0.15.0"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@types/ws": "^8.5.13",
66
66
  "effect": "4.0.0-rc.111",
67
67
  "typescript": "^7.0.2",
68
68
  "ws": "^8.18.0",
69
- "@effect-uai/core": "0.13.0"
69
+ "@effect-uai/core": "0.15.0"
70
70
  },
71
71
  "peerDependencies": {
72
72
  "@effect-uai/core": ">=0.2.0 <1",
package/src/Mistral.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Context, Effect, Layer, Option, Redacted, Result, Schema, Stream } from "effect"
2
2
  import { HttpClient, HttpClientRequest } from "effect/unstable/http"
3
3
  import * as AiError from "@effect-uai/core/AiError"
4
+ import * as Capabilities from "@effect-uai/core/Capabilities"
4
5
  import {
5
6
  type CommonRequest,
6
7
  LanguageModel,
@@ -116,6 +117,12 @@ const buildStream = (cfg: Config) => {
116
117
  Stream.unwrap(
117
118
  Effect.gen(function* () {
118
119
  const client = yield* HttpClient.HttpClient
120
+ yield* Capabilities.warnDroppedBlocks(request.history, "output_image", {
121
+ provider: "mistral",
122
+ capability: "output_image",
123
+ reason:
124
+ "Assistant messages carry no image on this wire. Use `Turn.imagesAsInput` to resend it as user content.",
125
+ })
119
126
  const httpRequest = HttpClientRequest.post(url).pipe(
120
127
  HttpClientRequest.setHeader("authorization", `Bearer ${Redacted.value(cfg.apiKey)}`),
121
128
  HttpClientRequest.bodyJsonUnsafe(buildRequestBody(request)),
@@ -11,7 +11,7 @@ import {
11
11
  type TranscriberService,
12
12
  } from "@effect-uai/core/Transcriber"
13
13
  import { audioToBlob, defaultFileName } from "./audioCodec.js"
14
- import { httpStatusError, transportFailure } from "./http.js"
14
+ import { bodyMultipart, httpStatusError, transportFailure } from "./http.js"
15
15
  import type { MistralTranscribeModel } from "./models.js"
16
16
 
17
17
  // ---------------------------------------------------------------------------
@@ -152,10 +152,10 @@ export const transcribeImpl =
152
152
  Effect.gen(function* () {
153
153
  const client = yield* HttpClient.HttpClient
154
154
  const baseUrl = cfg.baseUrl ?? "https://api.mistral.ai"
155
- const formData = yield* buildFormData(request)
155
+ const withBody = yield* Effect.flatMap(buildFormData(request), bodyMultipart)
156
156
  const httpRequest = HttpClientRequest.post(`${baseUrl}/v1/audio/transcriptions`).pipe(
157
157
  HttpClientRequest.bearerToken(cfg.apiKey),
158
- HttpClientRequest.bodyFormData(formData),
158
+ withBody,
159
159
  )
160
160
  const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))
161
161
  if (response.status >= 400) {
package/src/codec.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Array as Arr, Effect, Encoding, Match, Option } from "effect"
1
+ import { Array as Arr, Effect, Encoding, Match, Option, Result } from "effect"
2
2
  import type { ContentBlock, HistoryItem, InputImage } from "@effect-uai/core/Items"
3
3
 
4
4
  // The streaming decoder, tool encoding, and terminal-turn assembly are the
@@ -48,13 +48,17 @@ const blockToPart = Match.type<ContentBlock>().pipe(
48
48
  refusal: (b) => ({ type: "text", text: b.text }),
49
49
  // Bare string, not `{ url }`: the Mistral divergence from OpenAI.
50
50
  input_image: (b) => ({ type: "image_url", image_url: imageSourceToUrl(b.source) }),
51
+ // No assistant-image part here. Dropped rather than relocated onto a
52
+ // user message, which is the caller's call: `Turn.imagesAsInput`.
53
+ output_image: () => undefined,
51
54
  }),
52
55
  )
53
56
 
54
57
  const isTextBlock = (b: ContentBlock): boolean =>
55
58
  b.type === "input_text" || b.type === "output_text" || b.type === "refusal"
56
59
 
57
- const textOfBlock = (b: ContentBlock): string => (b.type === "input_image" ? "" : b.text)
60
+ const textOfBlock = (b: ContentBlock): string =>
61
+ b.type === "input_image" || b.type === "output_image" ? "" : b.text
58
62
 
59
63
  /**
60
64
  * Collapse a message's content to a plain string when it is text-only
@@ -64,7 +68,12 @@ const textOfBlock = (b: ContentBlock): string => (b.type === "input_image" ? ""
64
68
  const encodeContent = (
65
69
  content: ReadonlyArray<ContentBlock>,
66
70
  ): string | ReadonlyArray<Record<string, unknown>> =>
67
- Arr.every(content, isTextBlock) ? content.map(textOfBlock).join("") : content.map(blockToPart)
71
+ Arr.every(content, isTextBlock)
72
+ ? content.map(textOfBlock).join("")
73
+ : Arr.filterMap(content, (b) => {
74
+ const part = blockToPart(b)
75
+ return part === undefined ? Result.failVoid : Result.succeed(part)
76
+ })
68
77
 
69
78
  const toolCallWire = (call_id: string, name: string, args: string): Record<string, unknown> => ({
70
79
  id: call_id,
package/src/http.ts CHANGED
@@ -1,5 +1,7 @@
1
- import { Match } from "effect"
1
+ import { Effect, Match } from "effect"
2
+ import { HttpClientRequest } from "effect/unstable/http"
2
3
  import * as AiError from "@effect-uai/core/AiError"
4
+ import * as Multipart from "@effect-uai/core/Multipart"
3
5
 
4
6
  /** Map an HTTP status from any Mistral endpoint to an `AiError` variant. */
5
7
  export const httpStatusError: (status: number, body: string) => AiError.AiError = (status, body) =>
@@ -44,3 +46,11 @@ export const httpStatusError: (status: number, body: string) => AiError.AiError
44
46
 
45
47
  export const transportFailure = (cause: unknown): AiError.AiError =>
46
48
  new AiError.Unavailable({ provider: "mistral", raw: cause })
49
+
50
+ /** Core's helper, with our transport error. Never pass `FormData` to the client. */
51
+ export const bodyMultipart = (
52
+ form: FormData,
53
+ ): Effect.Effect<
54
+ (request: HttpClientRequest.HttpClientRequest) => HttpClientRequest.HttpClientRequest,
55
+ AiError.AiError
56
+ > => Multipart.bodyMultipart(form).pipe(Effect.mapError(transportFailure))