@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.
- package/dist/Connector.d.ts +4 -0
- package/dist/Connector.d.ts.map +1 -1
- package/dist/Connector.js +8 -3
- package/dist/Connector.js.map +1 -1
- package/dist/ConnectorConfig.d.ts.map +1 -1
- package/dist/ConnectorConfig.js +2 -0
- package/dist/ConnectorConfig.js.map +1 -1
- package/dist/ContextManagement.d.ts.map +1 -1
- package/dist/ContextManagement.js +1 -0
- package/dist/ContextManagement.js.map +1 -1
- package/dist/LabelScoring.d.ts +52 -0
- package/dist/LabelScoring.d.ts.map +1 -0
- package/dist/LabelScoring.js +139 -0
- package/dist/LabelScoring.js.map +1 -0
- package/dist/LlmService.d.ts +30 -2
- package/dist/LlmService.d.ts.map +1 -1
- package/dist/LlmService.js.map +1 -1
- package/dist/Models.d.ts +34 -2
- package/dist/Models.d.ts.map +1 -1
- package/dist/Models.js +37 -1
- package/dist/Models.js.map +1 -1
- package/dist/eval/Judge.d.ts +15 -0
- package/dist/eval/Judge.d.ts.map +1 -1
- package/dist/eval/Judge.js +50 -0
- package/dist/eval/Judge.js.map +1 -1
- package/dist/judgment/FakeJudgment.d.ts +22 -0
- package/dist/judgment/FakeJudgment.d.ts.map +1 -0
- package/dist/judgment/FakeJudgment.js +42 -0
- package/dist/judgment/FakeJudgment.js.map +1 -0
- package/dist/judgment/Judgment.d.ts +57 -0
- package/dist/judgment/Judgment.d.ts.map +1 -0
- package/dist/judgment/Judgment.js +57 -0
- package/dist/judgment/Judgment.js.map +1 -0
- package/dist/judgment/LlmJudgment.d.ts +73 -0
- package/dist/judgment/LlmJudgment.d.ts.map +1 -0
- package/dist/judgment/LlmJudgment.js +239 -0
- package/dist/judgment/LlmJudgment.js.map +1 -0
- package/dist/judgment/Schemas.d.ts +163 -0
- package/dist/judgment/Schemas.d.ts.map +1 -0
- package/dist/judgment/Schemas.js +197 -0
- package/dist/judgment/Schemas.js.map +1 -0
- package/dist/judgment/TypeSafeJudgment.d.ts +46 -0
- package/dist/judgment/TypeSafeJudgment.d.ts.map +1 -0
- package/dist/judgment/TypeSafeJudgment.js +185 -0
- package/dist/judgment/TypeSafeJudgment.js.map +1 -0
- package/dist/observability/MeteredLlmService.d.ts.map +1 -1
- package/dist/observability/MeteredLlmService.js +1 -0
- package/dist/observability/MeteredLlmService.js.map +1 -1
- package/dist/providers/ConnectorFactories.d.ts.map +1 -1
- package/dist/providers/ConnectorFactories.js +2 -0
- package/dist/providers/ConnectorFactories.js.map +1 -1
- package/dist/providers/LmStudioProvider.d.ts.map +1 -1
- package/dist/providers/LmStudioProvider.js +45 -34
- package/dist/providers/LmStudioProvider.js.map +1 -1
- package/dist/providers/MlxLmProvider.d.ts +29 -0
- package/dist/providers/MlxLmProvider.d.ts.map +1 -0
- package/dist/providers/MlxLmProvider.js +249 -0
- package/dist/providers/MlxLmProvider.js.map +1 -0
- package/dist/providers/MockProvider.d.ts.map +1 -1
- package/dist/providers/MockProvider.js +11 -0
- package/dist/providers/MockProvider.js.map +1 -1
- package/dist/providers/OpenAIModels.d.ts +35 -0
- package/dist/providers/OpenAIModels.d.ts.map +1 -1
- package/dist/providers/OpenAIModels.js +36 -3
- package/dist/providers/OpenAIModels.js.map +1 -1
- package/package.json +8 -1
- package/src/Connector.ts +13 -3
- package/src/ConnectorConfig.ts +2 -0
- package/src/ContextManagement.ts +1 -0
- package/src/LabelScoring.ts +211 -0
- package/src/LlmService.ts +37 -1
- package/src/Models.ts +43 -1
- package/src/eval/Judge.ts +74 -0
- package/src/judgment/FakeJudgment.ts +90 -0
- package/src/judgment/Judgment.ts +123 -0
- package/src/judgment/LlmJudgment.ts +399 -0
- package/src/judgment/Schemas.ts +278 -0
- package/src/judgment/TypeSafeJudgment.ts +253 -0
- package/src/observability/MeteredLlmService.ts +7 -0
- package/src/providers/ConnectorFactories.ts +4 -0
- package/src/providers/LmStudioProvider.ts +64 -48
- package/src/providers/MlxLmProvider.ts +357 -0
- package/src/providers/MockProvider.ts +19 -0
- 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 {
|
|
19
|
-
import {
|
|
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
|
|
71
|
-
|
|
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
|
|
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
|
-
|
|
91
|
-
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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(
|
|
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
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
214
|
+
messages: [OpenAIChatMessage.make({ role: "user", content: prompt })],
|
|
190
215
|
temperature: config.temperature ?? 0.7,
|
|
191
216
|
stream: false,
|
|
192
|
-
|
|
193
|
-
|
|
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}/
|
|
229
|
+
`${normalized}/v1/chat/completions`,
|
|
197
230
|
JSON.stringify(request),
|
|
198
231
|
authHeaders(),
|
|
199
232
|
config.timeout
|
|
200
233
|
)
|
|
201
|
-
const response = yield*
|
|
202
|
-
|
|
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,
|
|
237
|
+
const result: StructuredResult<A> = [value, completionUsage(response), undefined]
|
|
222
238
|
return result
|
|
223
239
|
})
|
|
224
240
|
|