@effect-uai/mistral 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/Mistral.d.mts +1 -1
  2. package/dist/Mistral.d.mts.map +1 -1
  3. package/dist/Mistral.mjs +2 -2
  4. package/dist/Mistral.mjs.map +1 -1
  5. package/dist/MistralRealtimeTranscriber.d.mts +1 -1
  6. package/dist/MistralRealtimeTranscriber.mjs +1 -1
  7. package/dist/MistralRealtimeTranscriber.mjs.map +1 -1
  8. package/dist/MistralSynthesizer.d.mts +1 -1
  9. package/dist/MistralSynthesizer.mjs +1 -1
  10. package/dist/MistralSynthesizer.mjs.map +1 -1
  11. package/dist/MistralTranscriber.d.mts +1 -1
  12. package/dist/MistralTranscriber.mjs +1 -1
  13. package/dist/MistralTranscriber.mjs.map +1 -1
  14. package/dist/codec.d.mts +5 -80
  15. package/dist/codec.d.mts.map +1 -1
  16. package/dist/codec.mjs +10 -160
  17. package/dist/codec.mjs.map +1 -1
  18. package/dist/codec.test.mjs +1 -13630
  19. package/dist/codec.test.mjs.map +1 -1
  20. package/dist/index.d.mts +1 -1
  21. package/dist/{magic-string.es-PAZgLX4h.mjs → magic-string.es-CaWgF-3p.mjs} +1 -1
  22. package/dist/{magic-string.es-PAZgLX4h.mjs.map → magic-string.es-CaWgF-3p.mjs.map} +1 -1
  23. package/dist/{models-DnDmylpj.d.mts → models-B1wZuvz5.d.mts} +1 -1
  24. package/dist/models-B1wZuvz5.d.mts.map +1 -0
  25. package/dist/models.d.mts +1 -1
  26. package/dist/onHalt.test.d.mts +1 -0
  27. package/dist/onHalt.test.mjs +61 -0
  28. package/dist/onHalt.test.mjs.map +1 -0
  29. package/dist/{realtimeStt-ClXWXbXX.d.mts → realtimeStt-BFcctqFC.d.mts} +1 -1
  30. package/dist/{realtimeStt-ClXWXbXX.d.mts.map → realtimeStt-BFcctqFC.d.mts.map} +1 -1
  31. package/dist/realtimeStt.d.mts +1 -1
  32. package/dist/test.DNmyFkvJ-CPKukVWS.mjs +13633 -0
  33. package/dist/test.DNmyFkvJ-CPKukVWS.mjs.map +1 -0
  34. package/package.json +7 -4
  35. package/src/Mistral.ts +7 -2
  36. package/src/MistralRealtimeTranscriber.ts +1 -1
  37. package/src/MistralSynthesizer.ts +1 -1
  38. package/src/MistralTranscriber.ts +1 -1
  39. package/src/codec.ts +40 -232
  40. package/src/onHalt.test.ts +75 -0
  41. package/dist/models-DnDmylpj.d.mts.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-uai/mistral",
3
- "version": "0.10.0",
3
+ "version": "0.12.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",
@@ -58,16 +58,19 @@
58
58
  "publishConfig": {
59
59
  "access": "public"
60
60
  },
61
+ "dependencies": {
62
+ "@effect-uai/chat-completions": "0.12.0"
63
+ },
61
64
  "devDependencies": {
62
65
  "@types/ws": "^8.5.13",
63
- "effect": "4.0.0-beta.57",
66
+ "effect": "4.0.0-beta.94",
64
67
  "typescript": "^6.0.3",
65
68
  "ws": "^8.18.0",
66
- "@effect-uai/core": "0.10.0"
69
+ "@effect-uai/core": "0.12.0"
67
70
  },
68
71
  "peerDependencies": {
69
72
  "@effect-uai/core": ">=0.2.0 <1",
70
- "effect": "4.0.0-beta.57",
73
+ "effect": ">=4.0.0-beta.94 <5.0.0",
71
74
  "ws": "^8.0.0"
72
75
  },
73
76
  "peerDependenciesMeta": {
package/src/Mistral.ts CHANGED
@@ -145,7 +145,12 @@ const buildStream = (cfg: Config) => {
145
145
  Option.isSome(chunk) ? Result.succeed(chunk.value) : Result.failVoid,
146
146
  ),
147
147
  Stream.mapAccum((): Accumulator => emptyAccumulator, applyChunk, {
148
- onHalt: (acc) => [TurnEvent.TurnComplete({ turn: accumulatorToTurn(acc) })],
148
+ // `onHalt` also fires on upstream failure and truncated streams,
149
+ // so only emit `TurnComplete` once a `finish_reason` was observed
150
+ onHalt: (acc) =>
151
+ Option.isSome(acc.finishReason)
152
+ ? [TurnEvent.TurnComplete({ turn: accumulatorToTurn(acc) })]
153
+ : [],
149
154
  }),
150
155
  )
151
156
  }),
@@ -158,7 +163,7 @@ const buildStream = (cfg: Config) => {
158
163
 
159
164
  /** Build a `MistralService` value. For Layer-based setup, prefer `layer`. */
160
165
  export const make = (cfg: Config): Effect.Effect<MistralService, never, HttpClient.HttpClient> =>
161
- Effect.map(HttpClient.HttpClient.asEffect(), (client) => {
166
+ Effect.map(HttpClient.HttpClient, (client) => {
162
167
  const streamTurn: MistralService["streamTurn"] = (request) =>
163
168
  buildStream(cfg)(request).pipe(Stream.provideService(HttpClient.HttpClient, client))
164
169
  return {
@@ -34,7 +34,7 @@ export {
34
34
  export const make = (
35
35
  cfg: Config,
36
36
  ): Effect.Effect<MistralTranscriberService, never, HttpClient.HttpClient> =>
37
- Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({
37
+ Effect.map(HttpClient.HttpClient, (client) => ({
38
38
  transcribe: (request) =>
39
39
  transcribeImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),
40
40
  streamTranscriptionFrom: streamTranscription(cfg),
@@ -226,7 +226,7 @@ const streamSynthesizeDialogueUnsupported: SpeechSynthesizerService["streamSynth
226
226
  export const make = (
227
227
  cfg: Config,
228
228
  ): Effect.Effect<MistralSynthesizerService, never, HttpClient.HttpClient> =>
229
- Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({
229
+ Effect.map(HttpClient.HttpClient, (client) => ({
230
230
  synthesize: (request) =>
231
231
  synthesizeImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),
232
232
  streamSynthesis: (request) =>
@@ -194,7 +194,7 @@ const streamUnsupported = <E, R>(
194
194
  export const make = (
195
195
  cfg: Config,
196
196
  ): Effect.Effect<MistralTranscriberService, never, HttpClient.HttpClient> =>
197
- Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({
197
+ Effect.map(HttpClient.HttpClient, (client) => ({
198
198
  transcribe: (request) =>
199
199
  transcribeImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),
200
200
  streamTranscriptionFrom: streamUnsupported,
package/src/codec.ts CHANGED
@@ -1,25 +1,29 @@
1
- import { Array as Arr, Encoding, Match, Option, Schema } from "effect"
2
- import type {
3
- ContentBlock,
4
- HistoryItem,
5
- InputImage,
6
- StopReason,
7
- Usage,
8
- } from "@effect-uai/core/Items"
9
- import type { StructuredFormat } from "@effect-uai/core/StructuredFormat"
10
- import type { ToolDescriptor } from "@effect-uai/core/Tool"
11
- import type { Turn } from "@effect-uai/core/Turn"
12
- import { TurnEvent } from "@effect-uai/core/Turn"
1
+ import { Array as Arr, Effect, Encoding, Match, Option } from "effect"
2
+ import type { ContentBlock, HistoryItem, InputImage } from "@effect-uai/core/Items"
3
+
4
+ // The streaming decoder, tool encoding, and terminal-turn assembly are the
5
+ // generic OpenAI chat-completions codec: Mistral speaks that dialect on the
6
+ // wire, so it reuses them verbatim. Only request encoding diverges (below).
7
+ export {
8
+ type Accumulator,
9
+ type WireChunk,
10
+ accumulatorToTurn,
11
+ applyChunk,
12
+ emptyAccumulator,
13
+ responseFormatWire,
14
+ toolsWire,
15
+ } from "@effect-uai/chat-completions/codec"
16
+ import {
17
+ type WireChunk,
18
+ decodeChunk as decodeChunkShared,
19
+ } from "@effect-uai/chat-completions/codec"
13
20
 
14
21
  // ---------------------------------------------------------------------------
15
22
  // History → Mistral chat `messages`
16
23
  //
17
- // Mistral speaks the OpenAI chat-completions dialect: a flat list of
18
- // `{ role, content, ... }` objects where an assistant turn's tool calls live
19
- // on the assistant message (`tool_calls`) and each tool result is its own
20
- // `{ role: "tool", tool_call_id, content }` message. Our history is a flatter
21
- // item list (assistant message, then separate `function_call` items), so we
22
- // fold consecutive `function_call`s onto the preceding assistant message.
24
+ // Same shape as the generic codec, with one wire quirk: Mistral takes a bare
25
+ // `image_url` string, not OpenAI's `image_url: { url }` object. That is the
26
+ // reason this half stays local rather than importing `itemsToMessages`.
23
27
  // ---------------------------------------------------------------------------
24
28
 
25
29
  type WireMessage = {
@@ -42,6 +46,7 @@ const blockToPart = Match.type<ContentBlock>().pipe(
42
46
  input_text: (b) => ({ type: "text", text: b.text }),
43
47
  output_text: (b) => ({ type: "text", text: b.text }),
44
48
  refusal: (b) => ({ type: "text", text: b.text }),
49
+ // Bare string, not `{ url }`: the Mistral divergence from OpenAI.
45
50
  input_image: (b) => ({ type: "image_url", image_url: imageSourceToUrl(b.source) }),
46
51
  }),
47
52
  )
@@ -102,26 +107,9 @@ export const itemsToMessages = (items: ReadonlyArray<HistoryItem>): ReadonlyArra
102
107
  Arr.reduce(items, [] as ReadonlyArray<WireMessage>, foldItem)
103
108
 
104
109
  // ---------------------------------------------------------------------------
105
- // tools / tool_choice / response_format
110
+ // tool_choice
106
111
  // ---------------------------------------------------------------------------
107
112
 
108
- export const toolsWire = (
109
- descriptors: ReadonlyArray<ToolDescriptor>,
110
- ): Option.Option<ReadonlyArray<Record<string, unknown>>> =>
111
- descriptors.length > 0
112
- ? Option.some(
113
- descriptors.map((t) => ({
114
- type: "function",
115
- function: {
116
- name: t.name,
117
- description: t.description,
118
- parameters: t.inputSchema,
119
- ...(t.strict !== undefined && { strict: t.strict }),
120
- },
121
- })),
122
- )
123
- : Option.none()
124
-
125
113
  type ToolChoice =
126
114
  | "auto"
127
115
  | "required"
@@ -137,202 +125,22 @@ export const toolChoiceWire = (choice: ToolChoice): string | Record<string, unkn
137
125
  Match.orElse((c) => ({ type: "function", function: { name: c.name } })),
138
126
  )
139
127
 
140
- export const responseFormatWire = (
141
- structured: StructuredFormat<unknown>,
142
- ): Record<string, unknown> => ({
143
- type: "json_schema",
144
- json_schema: {
145
- name: structured.name,
146
- schema: structured.schema["~standard"].jsonSchema.input({ target: "draft-2020-12" }),
147
- ...(structured.strict !== undefined && { strict: structured.strict }),
148
- },
149
- })
150
-
151
128
  // ---------------------------------------------------------------------------
152
- // Streaming decode: chat.completion.chunk → TurnEvent
129
+ // Streaming decode
153
130
  // ---------------------------------------------------------------------------
154
131
 
155
- const WireFunction = Schema.Struct({
156
- name: Schema.optional(Schema.NullOr(Schema.String)),
157
- arguments: Schema.optional(Schema.NullOr(Schema.String)),
158
- })
159
-
160
- const WireToolCall = Schema.Struct({
161
- index: Schema.optional(Schema.Number),
162
- id: Schema.optional(Schema.NullOr(Schema.String)),
163
- function: Schema.optional(WireFunction),
164
- })
165
-
166
- const WireDelta = Schema.Struct({
167
- role: Schema.optional(Schema.NullOr(Schema.String)),
168
- content: Schema.optional(Schema.NullOr(Schema.String)),
169
- tool_calls: Schema.optional(Schema.NullOr(Schema.Array(WireToolCall))),
170
- })
171
-
172
- const WireChoice = Schema.Struct({
173
- index: Schema.optional(Schema.Number),
174
- delta: Schema.optional(WireDelta),
175
- finish_reason: Schema.optional(Schema.NullOr(Schema.String)),
176
- })
177
-
178
- const WireUsage = Schema.Struct({
179
- prompt_tokens: Schema.optional(Schema.Number),
180
- completion_tokens: Schema.optional(Schema.Number),
181
- total_tokens: Schema.optional(Schema.Number),
182
- })
183
-
184
- export const WireChunk = Schema.Struct({
185
- choices: Schema.optional(Schema.Array(WireChoice)),
186
- usage: Schema.optional(Schema.NullOr(WireUsage)),
187
- })
188
- export type WireChunk = typeof WireChunk.Type
189
-
190
- export const decodeChunk = Schema.decodeUnknownEffect(WireChunk)
191
-
192
- // ---------------------------------------------------------------------------
193
- // Accumulator
194
- // ---------------------------------------------------------------------------
195
-
196
- type ToolAcc = {
197
- readonly call_id: string
198
- readonly name: string
199
- readonly arguments: string
200
- }
201
-
202
- export type Accumulator = {
203
- readonly text: string
204
- /** Tool calls keyed by their wire `index` (defaults to position when absent). */
205
- readonly tools: ReadonlyMap<number, ToolAcc>
206
- readonly order: ReadonlyArray<number>
207
- readonly usage: Usage
208
- readonly finishReason: Option.Option<string>
209
- }
210
-
211
- export const emptyAccumulator: Accumulator = {
212
- text: "",
213
- tools: new Map(),
214
- order: [],
215
- usage: {},
216
- finishReason: Option.none(),
217
- }
218
-
219
- /** State threaded through the fold plus the events the step produced. */
220
- type Step = readonly [Accumulator, ReadonlyArray<TurnEvent>]
221
-
222
- const usageFrom = (wire: typeof WireUsage.Type): Usage => ({
223
- ...(wire.prompt_tokens !== undefined && { input_tokens: wire.prompt_tokens }),
224
- ...(wire.completion_tokens !== undefined && { output_tokens: wire.completion_tokens }),
225
- ...(wire.total_tokens !== undefined && { total_tokens: wire.total_tokens }),
226
- })
227
-
228
- const reasonToStop = Match.type<string>().pipe(
229
- Match.when("stop", () => "stop" as const),
230
- Match.when("length", () => "max_tokens" as const),
231
- Match.when("model_length", () => "max_tokens" as const),
232
- Match.when("tool_calls", () => "tool_calls" as const),
233
- Match.when("content_filter", () => "content_filter" as const),
234
- Match.orElse(() => "stop" as const),
235
- )
236
-
237
- const stopReasonOf = (acc: Accumulator): StopReason =>
238
- Option.match(acc.finishReason, {
239
- onNone: () => (acc.tools.size > 0 ? "tool_calls" : "stop"),
240
- onSome: reasonToStop,
241
- })
242
-
243
- const withTool = (acc: Accumulator, index: number, tool: ToolAcc, isNew: boolean): Accumulator => ({
244
- ...acc,
245
- tools: new Map(acc.tools).set(index, tool),
246
- order: isNew ? [...acc.order, index] : acc.order,
247
- })
248
-
249
- const applyContent = (acc: Accumulator, delta: typeof WireDelta.Type | undefined): Step => {
250
- const content = delta?.content
251
- return content !== undefined && content !== null && content.length > 0
252
- ? [{ ...acc, text: acc.text + content }, [TurnEvent.TextDelta({ text: content })]]
253
- : [acc, []]
254
- }
255
-
256
- const applyToolCall = (acc: Accumulator, tc: typeof WireToolCall.Type, position: number): Step => {
257
- const index = tc.index ?? position
258
- const argsDelta = tc.function?.arguments ?? ""
259
- const existing = acc.tools.get(index)
260
- if (existing === undefined) {
261
- const call_id = tc.id ?? `call_${index}`
262
- const name = tc.function?.name ?? ""
263
- return [
264
- withTool(acc, index, { call_id, name, arguments: argsDelta }, true),
265
- [
266
- TurnEvent.ToolCallStart({ call_id, name }),
267
- ...(argsDelta.length > 0
268
- ? [TurnEvent.ToolCallArgsDelta({ call_id, delta: argsDelta })]
269
- : []),
270
- ],
271
- ]
272
- }
273
- return argsDelta.length > 0
274
- ? [
275
- withTool(acc, index, { ...existing, arguments: existing.arguments + argsDelta }, false),
276
- [TurnEvent.ToolCallArgsDelta({ call_id: existing.call_id, delta: argsDelta })],
277
- ]
278
- : [acc, []]
279
- }
280
-
281
- const chain = (step: Step, next: (acc: Accumulator) => Step): Step => {
282
- const [acc, events] = step
283
- const [acc2, more] = next(acc)
284
- return [acc2, [...events, ...more]]
285
- }
286
-
287
- const applyChoice = (step: Step, choice: typeof WireChoice.Type): Step => {
288
- const withContent = chain(step, (acc) => applyContent(acc, choice.delta))
289
- const withTools = Arr.reduce(choice.delta?.tool_calls ?? [], withContent, (s: Step, tc, i) =>
290
- chain(s, (acc) => applyToolCall(acc, tc, i)),
291
- )
292
- const reason = choice.finish_reason
293
- return reason !== undefined && reason !== null
294
- ? [{ ...withTools[0], finishReason: Option.some(reason) }, withTools[1]]
295
- : withTools
296
- }
297
-
298
- /**
299
- * Fold one decoded chunk into the accumulator, returning the next state and
300
- * the `TurnEvent`s the chunk produced. Mirrors Anthropic's `deltasFromEvent`,
301
- * but over chat-completions deltas.
302
- */
303
- export const applyChunk = (acc: Accumulator, chunk: WireChunk): Step => {
304
- const afterChoices = Arr.reduce(chunk.choices ?? [], [acc, []] as Step, applyChoice)
305
- const usage = chunk.usage
306
- return usage !== undefined && usage !== null
307
- ? chain(afterChoices, (a) => {
308
- const u = usageFrom(usage)
309
- return [{ ...a, usage: u }, [TurnEvent.UsageUpdate({ usage: u })]]
310
- })
311
- : afterChoices
312
- }
313
-
314
- /** Assemble the terminal `Turn` from the accumulated state. */
315
- export const accumulatorToTurn = (acc: Accumulator): Turn => {
316
- const message: ReadonlyArray<HistoryItem> =
317
- acc.text.length > 0
318
- ? [{ type: "message", role: "assistant", content: [{ type: "output_text", text: acc.text }] }]
319
- : []
320
- const toolCalls: ReadonlyArray<HistoryItem> = acc.order.flatMap((index) => {
321
- const t = acc.tools.get(index)
322
- return t === undefined
323
- ? []
324
- : [
325
- {
326
- type: "function_call" as const,
327
- call_id: t.call_id,
328
- name: t.name,
329
- arguments: t.arguments,
330
- },
331
- ]
332
- })
333
- return {
334
- items: [...message, ...toolCalls],
335
- usage: acc.usage,
336
- stop_reason: stopReasonOf(acc),
337
- }
338
- }
132
+ // Mistral emits `model_length` when it truncates on the context window; the
133
+ // shared decoder only knows OpenAI's `length`. Rewrite it before folding so the
134
+ // shared accumulator maps it to `max_tokens` rather than the `stop` fallback.
135
+ const normalizeChunk = (chunk: WireChunk): WireChunk =>
136
+ chunk.choices === undefined
137
+ ? chunk
138
+ : {
139
+ ...chunk,
140
+ choices: chunk.choices.map((c) =>
141
+ c.finish_reason === "model_length" ? { ...c, finish_reason: "length" } : c,
142
+ ),
143
+ }
144
+
145
+ export const decodeChunk = (u: unknown): Effect.Effect<WireChunk, unknown> =>
146
+ decodeChunkShared(u).pipe(Effect.map(normalizeChunk))
@@ -0,0 +1,75 @@
1
+ import { Effect, Exit, Redacted, Stream } from "effect"
2
+ import { HttpClient, HttpClientResponse } from "effect/unstable/http"
3
+ import { describe, expect, it } from "vitest"
4
+ import { TurnEvent } from "@effect-uai/core/Turn"
5
+ import { make as makeMistral } from "./Mistral.js"
6
+
7
+ // An HttpClient backed by a scripted SSE body
8
+ const clientWithBody = (makeBody: () => ReadableStream<Uint8Array>): HttpClient.HttpClient =>
9
+ HttpClient.make((request) =>
10
+ Effect.succeed(
11
+ HttpClientResponse.fromWeb(
12
+ request,
13
+ new Response(makeBody(), {
14
+ status: 200,
15
+ headers: { "content-type": "text/event-stream" },
16
+ }),
17
+ ),
18
+ ),
19
+ )
20
+
21
+ const enc = new TextEncoder()
22
+ const sse = (obj: unknown) => enc.encode(`data: ${JSON.stringify(obj)}\n\n`)
23
+
24
+ const runStream = (client: HttpClient.HttpClient) =>
25
+ Effect.gen(function* () {
26
+ const svc = yield* makeMistral({ apiKey: Redacted.make("k") })
27
+ const events: Array<TurnEvent> = []
28
+ const exit = yield* svc.streamTurn({ model: "mistral-small-latest", history: [] }).pipe(
29
+ Stream.runForEach((e) => Effect.sync(() => events.push(e))),
30
+ Effect.exit,
31
+ )
32
+ return { tags: events.map((e) => e._tag), events, exit }
33
+ }).pipe(Effect.provideService(HttpClient.HttpClient, client))
34
+
35
+ describe("Mistral streamTurn: truncated turn presented as complete", () => {
36
+ it("truncated EOF (no finish_reason) must not synthesize a complete turn", async () => {
37
+ const body = () =>
38
+ new ReadableStream<Uint8Array>({
39
+ start(controller) {
40
+ controller.enqueue(sse({ choices: [{ delta: { content: "Hel" } }] }))
41
+ controller.close() // no finish_reason, no [DONE]
42
+ },
43
+ })
44
+ const { tags } = await Effect.runPromise(runStream(clientWithBody(body)))
45
+ expect(tags).toContain("TextDelta")
46
+ expect(tags).not.toContain("TurnComplete")
47
+ })
48
+
49
+ it("mid-stream error must surface as a failure, not a fake TurnComplete", async () => {
50
+ const body = () =>
51
+ new ReadableStream<Uint8Array>({
52
+ start(controller) {
53
+ controller.enqueue(sse({ choices: [{ delta: { content: "Hel" } }] }))
54
+ controller.error(new Error("connection reset"))
55
+ },
56
+ })
57
+ const { tags, exit } = await Effect.runPromise(runStream(clientWithBody(body)))
58
+ expect(Exit.isFailure(exit)).toBe(true)
59
+ expect(tags).not.toContain("TurnComplete")
60
+ })
61
+
62
+ it("a well-formed stream (finish_reason present) still completes", async () => {
63
+ const body = () =>
64
+ new ReadableStream<Uint8Array>({
65
+ start(controller) {
66
+ controller.enqueue(sse({ choices: [{ delta: { content: "Hel" } }] }))
67
+ controller.enqueue(sse({ choices: [{ delta: {}, finish_reason: "stop" }] }))
68
+ controller.enqueue(enc.encode("data: [DONE]\n\n"))
69
+ controller.close()
70
+ },
71
+ })
72
+ const { tags } = await Effect.runPromise(runStream(clientWithBody(body)))
73
+ expect(tags).toContain("TurnComplete")
74
+ })
75
+ })
@@ -1 +0,0 @@
1
- {"version":3,"file":"models-DnDmylpj.d.mts","names":[],"sources":["../src/models.ts"],"mappings":";;AAWA;;;;;AAmBA;;;;;KAnBY,YAAA;;KAmBA,sBAAA;;KAOA,oBAAA;AAMZ;AAAA,KAAY,eAAA"}