@llm4ts/core 2.4.2 → 2.6.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 (84) hide show
  1. package/dist/Connector.d.ts +4 -0
  2. package/dist/Connector.d.ts.map +1 -1
  3. package/dist/Connector.js +8 -3
  4. package/dist/Connector.js.map +1 -1
  5. package/dist/ConnectorConfig.d.ts.map +1 -1
  6. package/dist/ConnectorConfig.js +2 -0
  7. package/dist/ConnectorConfig.js.map +1 -1
  8. package/dist/ContextManagement.d.ts.map +1 -1
  9. package/dist/ContextManagement.js +1 -0
  10. package/dist/ContextManagement.js.map +1 -1
  11. package/dist/LabelScoring.d.ts +52 -0
  12. package/dist/LabelScoring.d.ts.map +1 -0
  13. package/dist/LabelScoring.js +139 -0
  14. package/dist/LabelScoring.js.map +1 -0
  15. package/dist/LlmService.d.ts +30 -2
  16. package/dist/LlmService.d.ts.map +1 -1
  17. package/dist/LlmService.js.map +1 -1
  18. package/dist/Models.d.ts +34 -2
  19. package/dist/Models.d.ts.map +1 -1
  20. package/dist/Models.js +37 -1
  21. package/dist/Models.js.map +1 -1
  22. package/dist/eval/Judge.d.ts +15 -0
  23. package/dist/eval/Judge.d.ts.map +1 -1
  24. package/dist/eval/Judge.js +50 -0
  25. package/dist/eval/Judge.js.map +1 -1
  26. package/dist/judgment/FakeJudgment.d.ts +22 -0
  27. package/dist/judgment/FakeJudgment.d.ts.map +1 -0
  28. package/dist/judgment/FakeJudgment.js +42 -0
  29. package/dist/judgment/FakeJudgment.js.map +1 -0
  30. package/dist/judgment/Judgment.d.ts +57 -0
  31. package/dist/judgment/Judgment.d.ts.map +1 -0
  32. package/dist/judgment/Judgment.js +57 -0
  33. package/dist/judgment/Judgment.js.map +1 -0
  34. package/dist/judgment/LlmJudgment.d.ts +73 -0
  35. package/dist/judgment/LlmJudgment.d.ts.map +1 -0
  36. package/dist/judgment/LlmJudgment.js +239 -0
  37. package/dist/judgment/LlmJudgment.js.map +1 -0
  38. package/dist/judgment/Schemas.d.ts +163 -0
  39. package/dist/judgment/Schemas.d.ts.map +1 -0
  40. package/dist/judgment/Schemas.js +197 -0
  41. package/dist/judgment/Schemas.js.map +1 -0
  42. package/dist/judgment/TypeSafeJudgment.d.ts +46 -0
  43. package/dist/judgment/TypeSafeJudgment.d.ts.map +1 -0
  44. package/dist/judgment/TypeSafeJudgment.js +185 -0
  45. package/dist/judgment/TypeSafeJudgment.js.map +1 -0
  46. package/dist/observability/MeteredLlmService.d.ts.map +1 -1
  47. package/dist/observability/MeteredLlmService.js +1 -0
  48. package/dist/observability/MeteredLlmService.js.map +1 -1
  49. package/dist/providers/ConnectorFactories.d.ts.map +1 -1
  50. package/dist/providers/ConnectorFactories.js +2 -0
  51. package/dist/providers/ConnectorFactories.js.map +1 -1
  52. package/dist/providers/LmStudioProvider.d.ts.map +1 -1
  53. package/dist/providers/LmStudioProvider.js +45 -34
  54. package/dist/providers/LmStudioProvider.js.map +1 -1
  55. package/dist/providers/MlxLmProvider.d.ts +29 -0
  56. package/dist/providers/MlxLmProvider.d.ts.map +1 -0
  57. package/dist/providers/MlxLmProvider.js +249 -0
  58. package/dist/providers/MlxLmProvider.js.map +1 -0
  59. package/dist/providers/MockProvider.d.ts.map +1 -1
  60. package/dist/providers/MockProvider.js +11 -0
  61. package/dist/providers/MockProvider.js.map +1 -1
  62. package/dist/providers/OpenAIModels.d.ts +35 -0
  63. package/dist/providers/OpenAIModels.d.ts.map +1 -1
  64. package/dist/providers/OpenAIModels.js +36 -3
  65. package/dist/providers/OpenAIModels.js.map +1 -1
  66. package/package.json +8 -1
  67. package/src/Connector.ts +13 -3
  68. package/src/ConnectorConfig.ts +2 -0
  69. package/src/ContextManagement.ts +1 -0
  70. package/src/LabelScoring.ts +211 -0
  71. package/src/LlmService.ts +37 -1
  72. package/src/Models.ts +43 -1
  73. package/src/eval/Judge.ts +74 -0
  74. package/src/judgment/FakeJudgment.ts +90 -0
  75. package/src/judgment/Judgment.ts +123 -0
  76. package/src/judgment/LlmJudgment.ts +399 -0
  77. package/src/judgment/Schemas.ts +278 -0
  78. package/src/judgment/TypeSafeJudgment.ts +253 -0
  79. package/src/observability/MeteredLlmService.ts +7 -0
  80. package/src/providers/ConnectorFactories.ts +4 -0
  81. package/src/providers/LmStudioProvider.ts +64 -48
  82. package/src/providers/MlxLmProvider.ts +357 -0
  83. package/src/providers/MockProvider.ts +19 -0
  84. package/src/providers/OpenAIModels.ts +40 -3
@@ -0,0 +1,253 @@
1
+ import * as Duration from "effect/Duration"
2
+ import * as Effect from "effect/Effect"
3
+ import * as Layer from "effect/Layer"
4
+ import * as Redacted from "effect/Redacted"
5
+ import * as Schema from "effect/Schema"
6
+ import type { HttpClientShape } from "../HttpClient.ts"
7
+ import { TokenUsage } from "../Models.ts"
8
+ import {
9
+ Judgment,
10
+ JudgmentBackendError,
11
+ type JudgmentInput,
12
+ type JudgmentShape
13
+ } from "./Judgment.ts"
14
+ import {
15
+ ChoiceAnswer,
16
+ confidenceOf,
17
+ Description,
18
+ JudgmentResult,
19
+ origins,
20
+ QuestionFailure,
21
+ ScoreAnswer,
22
+ State,
23
+ TruthAnswer,
24
+ TruthCriteria,
25
+ type Answer,
26
+ type Question
27
+ } from "./Schemas.ts"
28
+
29
+ /**
30
+ * `TypeSafeJudgment`: the Judgment service over TypeSafe's hosted Jev
31
+ * (`POST /v1/systemone`). One request carries every question; the model
32
+ * answers them independently and its probabilities are calibrated. The only
33
+ * translation is `truth` ↔ `noul`. The key travels in a header and nowhere
34
+ * else: never in a log, an error, or a persisted result.
35
+ */
36
+
37
+ /** Published input price on 2026-09-19: $42 per billion tokens; output is free. */
38
+ export const typeSafeInputUsdPer1k = 0.000042
39
+
40
+ export interface TypeSafeJudgmentConfig {
41
+ readonly apiKey: Redacted.Redacted<string>
42
+ readonly baseUrl?: string
43
+ readonly model?: string
44
+ readonly timeout?: Duration.Duration
45
+ readonly onUsage?: (usage: TokenUsage, model: string | undefined) => Effect.Effect<void>
46
+ }
47
+
48
+ export const defaultTypeSafeBaseUrl = "https://api.typesafe.ai"
49
+ export const defaultTypeSafeModel = "jev-latest"
50
+
51
+ // Wire schemas, kept private: the public shape is `Schemas.ts`.
52
+ class WireNoulCriteria extends Schema.Class<WireNoulCriteria>("WireNoulCriteria")({
53
+ true: Description,
54
+ false: Description
55
+ }) {}
56
+
57
+ class WireQuestion extends Schema.Class<WireQuestion>("WireQuestion")({
58
+ type: Schema.Literals(["choice", "score", "noul"]),
59
+ instructions: Schema.String,
60
+ criteria: Schema.optionalKey(
61
+ Schema.Union([
62
+ Schema.Record(Schema.String, Description),
63
+ Schema.Array(Description),
64
+ WireNoulCriteria
65
+ ])
66
+ )
67
+ }) {}
68
+
69
+ class WireRequest extends Schema.Class<WireRequest>("WireRequest")({
70
+ model: Schema.String,
71
+ state: State,
72
+ questions: Schema.Record(Schema.String, WireQuestion)
73
+ }) {}
74
+
75
+ class WireChoice extends Schema.Class<WireChoice>("WireChoice")({
76
+ type: Schema.Literal("choice"),
77
+ choice: Schema.String,
78
+ probabilities: Schema.Record(Schema.String, Schema.Number),
79
+ confidence: Schema.Number
80
+ }) {}
81
+
82
+ class WireScore extends Schema.Class<WireScore>("WireScore")({
83
+ type: Schema.Literal("score"),
84
+ score: Schema.Number,
85
+ legend: Schema.Record(Schema.String, Description),
86
+ probabilities: Schema.Record(Schema.String, Schema.Number),
87
+ confidence: Schema.Number
88
+ }) {}
89
+
90
+ class WireNoul extends Schema.Class<WireNoul>("WireNoul")({
91
+ type: Schema.Literal("noul"),
92
+ noul: Schema.Number
93
+ }) {}
94
+
95
+ class WireError extends Schema.Class<WireError>("WireError")({
96
+ type: Schema.Literal("error"),
97
+ message: Schema.optionalKey(Schema.String)
98
+ }) {}
99
+
100
+ class WireUsage extends Schema.Class<WireUsage>("WireUsage")({
101
+ input_tokens: Schema.optionalKey(Schema.Int),
102
+ output_tokens: Schema.optionalKey(Schema.Int)
103
+ }) {}
104
+
105
+ class WireResponse extends Schema.Class<WireResponse>("WireResponse")({
106
+ model: Schema.optionalKey(Schema.String),
107
+ answers: Schema.Record(Schema.String, Schema.Union([WireChoice, WireScore, WireNoul, WireError])),
108
+ usage: Schema.optionalKey(WireUsage)
109
+ }) {}
110
+
111
+ export const toWireQuestion = (question: Question): WireQuestion =>
112
+ question.type === "truth"
113
+ ? WireQuestion.make({
114
+ type: "noul",
115
+ instructions: question.instructions,
116
+ ...(question.criteria === undefined
117
+ ? {}
118
+ : {
119
+ criteria: WireNoulCriteria.make({
120
+ true: question.criteria.true,
121
+ false: question.criteria.false
122
+ })
123
+ })
124
+ })
125
+ : WireQuestion.make({
126
+ type: question.type,
127
+ instructions: question.instructions,
128
+ criteria: question.criteria
129
+ })
130
+
131
+ // `confidence` is recomputed as the maximum probability so the field means
132
+ // the same thing whichever backend answered; TypeSafe's own statistic is
133
+ // kept beside it as `reportedConfidence`.
134
+ const fromWireAnswer = (
135
+ answer: WireChoice | WireScore | WireNoul,
136
+ model: string | undefined
137
+ ): Answer =>
138
+ answer instanceof WireChoice
139
+ ? ChoiceAnswer.make({
140
+ type: "choice",
141
+ choice: answer.choice,
142
+ probabilities: answer.probabilities,
143
+ confidence: confidenceOf(answer.probabilities),
144
+ reportedConfidence: answer.confidence,
145
+ origin: origins.hosted(model)
146
+ })
147
+ : answer instanceof WireScore
148
+ ? ScoreAnswer.make({
149
+ type: "score",
150
+ score: answer.score,
151
+ legend: answer.legend,
152
+ probabilities: answer.probabilities,
153
+ confidence: confidenceOf(answer.probabilities),
154
+ reportedConfidence: answer.confidence,
155
+ origin: origins.hosted(model)
156
+ })
157
+ : TruthAnswer.make({ type: "truth", truth: answer.noul, origin: origins.hosted(model) })
158
+
159
+ const toUsage = (usage: WireUsage | undefined): TokenUsage | undefined => {
160
+ if (usage === undefined) {
161
+ return undefined
162
+ }
163
+ const prompt = usage.input_tokens ?? 0
164
+ const completion = usage.output_tokens ?? 0
165
+ return TokenUsage.make({
166
+ prompt,
167
+ completion,
168
+ total: prompt + completion,
169
+ costUsd: (prompt / 1_000) * typeSafeInputUsdPer1k
170
+ })
171
+ }
172
+
173
+ export const makeTypeSafeJudgment = (
174
+ config: TypeSafeJudgmentConfig,
175
+ httpClient: HttpClientShape
176
+ ): JudgmentShape => {
177
+ const baseUrl = (config.baseUrl ?? defaultTypeSafeBaseUrl).replace(/\/+$/, "")
178
+ const model = config.model ?? defaultTypeSafeModel
179
+ const timeout = config.timeout ?? Duration.seconds(60)
180
+
181
+ const judge = Effect.fn("@llm4ts/core/judgment/TypeSafeJudgment.judge")(function* (
182
+ input: JudgmentInput
183
+ ): Effect.fn.Return<JudgmentResult, JudgmentBackendError> {
184
+ const request = WireRequest.make({
185
+ model,
186
+ state: input.state,
187
+ questions: Object.fromEntries(
188
+ Object.entries(input.questions).map(([key, question]) => [key, toWireQuestion(question)])
189
+ )
190
+ })
191
+ const raw = yield* httpClient
192
+ .postJson(
193
+ `${baseUrl}/v1/systemone`,
194
+ JSON.stringify(request),
195
+ { Authorization: `Bearer ${Redacted.value(config.apiKey)}` },
196
+ timeout
197
+ )
198
+ .pipe(
199
+ Effect.mapError((error) =>
200
+ JudgmentBackendError.make({
201
+ backend: "typesafe",
202
+ message: `TypeSafe request failed: ${error._tag}`,
203
+ cause: error
204
+ })
205
+ )
206
+ )
207
+ const response = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(WireResponse))(
208
+ raw
209
+ ).pipe(
210
+ Effect.mapError((error) =>
211
+ JudgmentBackendError.make({
212
+ backend: "typesafe",
213
+ message: `TypeSafe response did not decode: ${String(error)}`
214
+ })
215
+ )
216
+ )
217
+ const answers: Record<string, Answer> = {}
218
+ const failures: Array<QuestionFailure> = []
219
+ for (const [key, answer] of Object.entries(response.answers)) {
220
+ if (answer instanceof WireError) {
221
+ failures.push(QuestionFailure.make({ key, reason: answer.message ?? "backend error" }))
222
+ } else {
223
+ answers[key] = fromWireAnswer(answer, response.model ?? model)
224
+ }
225
+ }
226
+ for (const key of Object.keys(input.questions)) {
227
+ if (answers[key] === undefined && !failures.some((failure) => failure.key === key)) {
228
+ failures.push(QuestionFailure.make({ key, reason: "no answer returned" }))
229
+ }
230
+ }
231
+ const usage = toUsage(response.usage)
232
+ if (usage !== undefined && config.onUsage !== undefined) {
233
+ yield* config.onUsage(usage, response.model)
234
+ }
235
+ return JudgmentResult.make({
236
+ answers,
237
+ failures,
238
+ backend: "typesafe",
239
+ ...(usage === undefined ? {} : { usage }),
240
+ ...(response.model === undefined ? {} : { model: response.model })
241
+ })
242
+ })
243
+
244
+ return { backend: "typesafe", identity: `typesafe:${model}`, judge }
245
+ }
246
+
247
+ export const TypeSafeJudgmentLive = (
248
+ config: TypeSafeJudgmentConfig,
249
+ httpClient: HttpClientShape
250
+ ): Layer.Layer<Judgment> => Layer.succeed(Judgment, makeTypeSafeJudgment(config, httpClient))
251
+
252
+ /** `TruthCriteria` is re-exported so callers can build the wire form themselves. */
253
+ export { TruthCriteria }
@@ -152,5 +152,12 @@ export const meterLlmService = (
152
152
  options,
153
153
  <A>(result: StructuredResult<A>) => result[1]
154
154
  ),
155
+ scoreLabels: (prompt, labels) =>
156
+ meterEffect(
157
+ service.scoreLabels(prompt, labels),
158
+ collector,
159
+ options,
160
+ (distribution) => distribution.usage
161
+ ),
155
162
  isAvailable: service.isAvailable
156
163
  })
@@ -33,6 +33,7 @@ import {
33
33
  } from "./GeminiCliProvider.ts"
34
34
  import { makeGrokCliConnector } from "./GrokCliConnector.ts"
35
35
  import { makeLmStudioProvider } from "./LmStudioProvider.ts"
36
+ import { makeMlxLmProvider } from "./MlxLmProvider.ts"
36
37
  import { makeMockProvider } from "./MockProvider.ts"
37
38
  import { makeOllamaProvider } from "./OllamaProvider.ts"
38
39
  import { makeOpenAIProvider } from "./OpenAIProvider.ts"
@@ -94,6 +95,9 @@ export const createConnectorRegistry = (
94
95
  apiFactory(ConnectorIds.Ollama, (config) =>
95
96
  makeOllamaProvider(toLlmConfig(config), dependencies.http)
96
97
  ),
98
+ apiFactory(ConnectorIds.MlxLm, (config) =>
99
+ makeMlxLmProvider(toLlmConfig(config), dependencies.http)
100
+ ),
97
101
  cliFactory(ConnectorIds.ClaudeCli, (config) =>
98
102
  makeClaudeCliConnector(config, dependencies.process)
99
103
  ),
@@ -9,14 +9,23 @@ import type { StructuredResult } from "../LlmService.ts"
9
9
  import {
10
10
  ConnectorIds,
11
11
  LlmChunk,
12
+ TokenUsage,
12
13
  type JsonSchema,
13
14
  type LlmConfig,
14
15
  type Message,
15
16
  type MessageRole
16
17
  } from "../Models.ts"
17
18
  import { parseFromText } from "../StructuredOutput.ts"
18
- import { LmStudioChatRequest, LmStudioChatResponse, LmStudioMessage } from "./LmStudioModels.ts"
19
- import { OpenAIChatChunk, OpenAIChatCompletionRequest, OpenAIChatMessage } from "./OpenAIModels.ts"
19
+ import { LmStudioMessage } from "./LmStudioModels.ts"
20
+ import {
21
+ OpenAIChatChunk,
22
+ OpenAIChatCompletionRequest,
23
+ OpenAIChatCompletionResponse,
24
+ OpenAIChatMessage,
25
+ OpenAIChatTemplateKwargs,
26
+ OpenAIJsonSchemaSpec,
27
+ OpenAIResponseFormat
28
+ } from "./OpenAIModels.ts"
20
29
 
21
30
  const emptyHeaders: Readonly<Record<string, string>> = Object.freeze({})
22
31
 
@@ -67,11 +76,13 @@ export const renderLmStudioNativeInput = (
67
76
  }
68
77
  }
69
78
 
70
- const decodeNativeResponse = (raw: string): Effect.Effect<LmStudioChatResponse, ParseError> =>
71
- Schema.decodeUnknownEffect(Schema.fromJsonString(LmStudioChatResponse))(raw).pipe(
79
+ const decodeCompletionResponse = (
80
+ raw: string
81
+ ): Effect.Effect<OpenAIChatCompletionResponse, ParseError> =>
82
+ Schema.decodeUnknownEffect(Schema.fromJsonString(OpenAIChatCompletionResponse))(raw).pipe(
72
83
  Effect.mapError((error) =>
73
84
  ParseError.make({
74
- message: `Failed to decode LmStudio native API response: ${String(error)}`,
85
+ message: `Failed to decode LmStudio chat completion: ${String(error)}`,
75
86
  raw
76
87
  })
77
88
  )
@@ -87,30 +98,40 @@ const decodeStreamChunk = (raw: string): Effect.Effect<OpenAIChatChunk, ParseErr
87
98
  )
88
99
  )
89
100
 
90
- const nativeContent = (
91
- response: LmStudioChatResponse,
101
+ /**
102
+ * The visible reply, or the reasoning field when LM Studio's reasoning
103
+ * parser filed a constrained JSON reply under `reasoning_content` and left
104
+ * `content` empty (observed with Qwen 3.6 on 2026-09-19).
105
+ */
106
+ const completionContent = (
107
+ response: OpenAIChatCompletionResponse,
92
108
  raw: string
93
109
  ): Effect.Effect<string, ParseError> => {
94
- const content = response.output
95
- .find(
96
- (item) =>
97
- item.type === "message" &&
98
- item.content !== undefined &&
99
- item.content !== null &&
100
- item.content.trim().length > 0
101
- )
102
- ?.content?.trim()
103
-
104
- return content === undefined || content === null
110
+ const message = response.choices[0]?.message
111
+ const content = message?.content?.trim() ?? ""
112
+ const reasoning = message?.reasoning_content?.trim() ?? ""
113
+ const text = content.length > 0 ? content : reasoning
114
+ return text.length === 0
105
115
  ? Effect.fail(
106
116
  ParseError.make({
107
117
  message: "LmStudio response missing choices[0].message.content",
108
118
  raw
109
119
  })
110
120
  )
111
- : Effect.succeed(content)
121
+ : Effect.succeed(text)
112
122
  }
113
123
 
124
+ const completionUsage = (response: OpenAIChatCompletionResponse): TokenUsage | undefined =>
125
+ response.usage === undefined
126
+ ? undefined
127
+ : TokenUsage.make({
128
+ prompt: response.usage.prompt_tokens ?? 0,
129
+ completion: response.usage.completion_tokens ?? 0,
130
+ total:
131
+ response.usage.total_tokens ??
132
+ (response.usage.prompt_tokens ?? 0) + (response.usage.completion_tokens ?? 0)
133
+ })
134
+
114
135
  export const makeLmStudioProvider = (
115
136
  config: LlmConfig,
116
137
  httpClient: HttpClientShape
@@ -178,47 +199,42 @@ export const makeLmStudioProvider = (
178
199
  })
179
200
  )
180
201
 
181
- const nativeRequest = (
182
- messages: ReadonlyArray<LmStudioMessage>
183
- ): Effect.Effect<readonly [response: LmStudioChatResponse, raw: string], LlmError> =>
202
+ const executeStructuredWithUsage = <A, E, RD, RE>(
203
+ prompt: string,
204
+ schema: Schema.ConstraintCodec<A, E, RD, RE>,
205
+ jsonSchema: JsonSchema
206
+ ): Effect.Effect<StructuredResult<A>, LlmError, RD> =>
184
207
  Effect.gen(function* () {
185
208
  const normalized = yield* baseUrl
186
- const rendered = renderLmStudioNativeInput(messages)
187
- const request = LmStudioChatRequest.make({
209
+ // Grammar-constrained sampling on the OpenAI-compatible endpoint: the
210
+ // reply is guaranteed to match `jsonSchema`, so no "JSON only" nudge is
211
+ // needed and `parseFromText` only has to decode it.
212
+ const request = OpenAIChatCompletionRequest.make({
188
213
  model: config.model,
189
- input: rendered.input,
214
+ messages: [OpenAIChatMessage.make({ role: "user", content: prompt })],
190
215
  temperature: config.temperature ?? 0.7,
191
216
  stream: false,
192
- ...(rendered.systemPrompt === undefined ? {} : { system_prompt: rendered.systemPrompt }),
193
- ...(config.maxTokens === undefined ? {} : { max_output_tokens: config.maxTokens })
217
+ response_format: OpenAIResponseFormat.make({
218
+ type: "json_schema",
219
+ json_schema: OpenAIJsonSchemaSpec.make({
220
+ name: "response",
221
+ schema: jsonSchema,
222
+ strict: true
223
+ })
224
+ }),
225
+ chat_template_kwargs: OpenAIChatTemplateKwargs.make({ enable_thinking: false }),
226
+ ...(config.maxTokens === undefined ? {} : { max_tokens: config.maxTokens })
194
227
  })
195
228
  const raw = yield* httpClient.postJson(
196
- `${normalized}/api/v1/chat`,
229
+ `${normalized}/v1/chat/completions`,
197
230
  JSON.stringify(request),
198
231
  authHeaders(),
199
232
  config.timeout
200
233
  )
201
- const response = yield* decodeNativeResponse(raw)
202
- return [response, raw]
203
- })
204
-
205
- const executeStructuredWithUsage = <A, E, RD, RE>(
206
- prompt: string,
207
- schema: Schema.ConstraintCodec<A, E, RD, RE>,
208
- jsonSchema: JsonSchema
209
- ): Effect.Effect<StructuredResult<A>, LlmError, RD> =>
210
- Effect.gen(function* () {
211
- const [response, raw] = yield* nativeRequest([
212
- LmStudioMessage.make({
213
- role: "user",
214
- content:
215
- `${prompt}\n\n` +
216
- "Please respond with valid JSON only, no additional text or markdown formatting."
217
- })
218
- ])
219
- const content = yield* nativeContent(response, raw)
234
+ const response = yield* decodeCompletionResponse(raw)
235
+ const content = yield* completionContent(response, raw)
220
236
  const value = yield* parseFromText(content, schema, jsonSchema)
221
- const result: StructuredResult<A> = [value, undefined, undefined]
237
+ const result: StructuredResult<A> = [value, completionUsage(response), undefined]
222
238
  return result
223
239
  })
224
240