@llm4ts/core 2.54.1 → 3.1.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 (142) hide show
  1. package/README.md +1 -1
  2. package/dist/Capability.d.ts +0 -9
  3. package/dist/Capability.d.ts.map +1 -1
  4. package/dist/Capability.js +0 -38
  5. package/dist/Capability.js.map +1 -1
  6. package/dist/Connector.d.ts +2 -3
  7. package/dist/Connector.d.ts.map +1 -1
  8. package/dist/Connector.js +0 -5
  9. package/dist/Connector.js.map +1 -1
  10. package/dist/ConnectorRegistry.d.ts +0 -3
  11. package/dist/ConnectorRegistry.d.ts.map +1 -1
  12. package/dist/ConnectorRegistry.js +0 -3
  13. package/dist/ConnectorRegistry.js.map +1 -1
  14. package/dist/HttpClient.d.ts +0 -2
  15. package/dist/HttpClient.d.ts.map +1 -1
  16. package/dist/HttpClient.js +0 -2
  17. package/dist/HttpClient.js.map +1 -1
  18. package/dist/LlmService.d.ts +1 -2
  19. package/dist/LlmService.d.ts.map +1 -1
  20. package/dist/LlmService.js.map +1 -1
  21. package/dist/Models.d.ts +0 -21
  22. package/dist/Models.d.ts.map +1 -1
  23. package/dist/Models.js +0 -19
  24. package/dist/Models.js.map +1 -1
  25. package/dist/Streaming.d.ts +4 -11
  26. package/dist/Streaming.d.ts.map +1 -1
  27. package/dist/Streaming.js +2 -29
  28. package/dist/Streaming.js.map +1 -1
  29. package/dist/providers/AnthropicModels.d.ts +0 -43
  30. package/dist/providers/AnthropicModels.d.ts.map +1 -1
  31. package/dist/providers/AnthropicModels.js +0 -39
  32. package/dist/providers/AnthropicModels.js.map +1 -1
  33. package/dist/providers/AnthropicProvider.d.ts +2 -3
  34. package/dist/providers/AnthropicProvider.d.ts.map +1 -1
  35. package/dist/providers/AnthropicProvider.js +2 -49
  36. package/dist/providers/AnthropicProvider.js.map +1 -1
  37. package/dist/providers/GeminiApiProvider.d.ts +2 -3
  38. package/dist/providers/GeminiApiProvider.d.ts.map +1 -1
  39. package/dist/providers/GeminiApiProvider.js +2 -44
  40. package/dist/providers/GeminiApiProvider.js.map +1 -1
  41. package/dist/providers/GeminiModels.d.ts +0 -48
  42. package/dist/providers/GeminiModels.d.ts.map +1 -1
  43. package/dist/providers/GeminiModels.js +0 -40
  44. package/dist/providers/GeminiModels.js.map +1 -1
  45. package/dist/providers/LmStudioProvider.d.ts.map +1 -1
  46. package/dist/providers/LmStudioProvider.js +1 -4
  47. package/dist/providers/LmStudioProvider.js.map +1 -1
  48. package/dist/providers/MlxLmProvider.d.ts.map +1 -1
  49. package/dist/providers/MlxLmProvider.js +1 -2
  50. package/dist/providers/MlxLmProvider.js.map +1 -1
  51. package/dist/providers/MockProvider.d.ts.map +1 -1
  52. package/dist/providers/MockProvider.js +1 -6
  53. package/dist/providers/MockProvider.js.map +1 -1
  54. package/dist/providers/OllamaProvider.d.ts.map +1 -1
  55. package/dist/providers/OllamaProvider.js +1 -4
  56. package/dist/providers/OllamaProvider.js.map +1 -1
  57. package/dist/providers/OpenAIModels.d.ts +0 -58
  58. package/dist/providers/OpenAIModels.d.ts.map +1 -1
  59. package/dist/providers/OpenAIModels.js +0 -51
  60. package/dist/providers/OpenAIModels.js.map +1 -1
  61. package/dist/providers/OpenAIProvider.d.ts +2 -3
  62. package/dist/providers/OpenAIProvider.d.ts.map +1 -1
  63. package/dist/providers/OpenAIProvider.js +2 -46
  64. package/dist/providers/OpenAIProvider.js.map +1 -1
  65. package/package.json +2 -14
  66. package/src/Capability.ts +0 -50
  67. package/src/Connector.ts +2 -20
  68. package/src/ConnectorRegistry.ts +0 -15
  69. package/src/HttpClient.ts +0 -15
  70. package/src/LlmService.ts +1 -13
  71. package/src/Models.ts +0 -22
  72. package/src/Streaming.ts +3 -99
  73. package/src/providers/AnthropicModels.ts +0 -47
  74. package/src/providers/AnthropicProvider.ts +1 -87
  75. package/src/providers/GeminiApiProvider.ts +2 -83
  76. package/src/providers/GeminiModels.ts +0 -46
  77. package/src/providers/LmStudioProvider.ts +1 -7
  78. package/src/providers/MlxLmProvider.ts +1 -3
  79. package/src/providers/MockProvider.ts +0 -9
  80. package/src/providers/OllamaProvider.ts +1 -7
  81. package/src/providers/OpenAIModels.ts +0 -64
  82. package/src/providers/OpenAIProvider.ts +2 -83
  83. package/dist/ContextManagement.d.ts +0 -78
  84. package/dist/ContextManagement.d.ts.map +0 -1
  85. package/dist/ContextManagement.js +0 -222
  86. package/dist/ContextManagement.js.map +0 -1
  87. package/dist/Conversation.d.ts +0 -18
  88. package/dist/Conversation.d.ts.map +0 -1
  89. package/dist/Conversation.js +0 -17
  90. package/dist/Conversation.js.map +0 -1
  91. package/dist/RateLimiter.d.ts +0 -44
  92. package/dist/RateLimiter.d.ts.map +0 -1
  93. package/dist/RateLimiter.js +0 -155
  94. package/dist/RateLimiter.js.map +0 -1
  95. package/dist/observability/MeteredLlmService.d.ts +0 -16
  96. package/dist/observability/MeteredLlmService.d.ts.map +0 -1
  97. package/dist/observability/MeteredLlmService.js +0 -72
  98. package/dist/observability/MeteredLlmService.js.map +0 -1
  99. package/dist/observability/Metrics.d.ts +0 -76
  100. package/dist/observability/Metrics.d.ts.map +0 -1
  101. package/dist/observability/Metrics.js +0 -152
  102. package/dist/observability/Metrics.js.map +0 -1
  103. package/dist/observability/StructuredLogger.d.ts +0 -31
  104. package/dist/observability/StructuredLogger.d.ts.map +0 -1
  105. package/dist/observability/StructuredLogger.js +0 -39
  106. package/dist/observability/StructuredLogger.js.map +0 -1
  107. package/dist/observability/Tracing.d.ts +0 -38
  108. package/dist/observability/Tracing.d.ts.map +0 -1
  109. package/dist/observability/Tracing.js +0 -80
  110. package/dist/observability/Tracing.js.map +0 -1
  111. package/dist/providers/OpenCodeModels.d.ts +0 -61
  112. package/dist/providers/OpenCodeModels.d.ts.map +0 -1
  113. package/dist/providers/OpenCodeModels.js +0 -53
  114. package/dist/providers/OpenCodeModels.js.map +0 -1
  115. package/dist/providers/OpenCodeProvider.d.ts +0 -10
  116. package/dist/providers/OpenCodeProvider.d.ts.map +0 -1
  117. package/dist/providers/OpenCodeProvider.js +0 -154
  118. package/dist/providers/OpenCodeProvider.js.map +0 -1
  119. package/dist/tools/Tool.d.ts +0 -83
  120. package/dist/tools/Tool.d.ts.map +0 -1
  121. package/dist/tools/Tool.js +0 -278
  122. package/dist/tools/Tool.js.map +0 -1
  123. package/dist/tools/ToolCallingExecutor.d.ts +0 -18
  124. package/dist/tools/ToolCallingExecutor.d.ts.map +0 -1
  125. package/dist/tools/ToolCallingExecutor.js +0 -64
  126. package/dist/tools/ToolCallingExecutor.js.map +0 -1
  127. package/dist/tools/ToolRegistry.d.ts +0 -45
  128. package/dist/tools/ToolRegistry.d.ts.map +0 -1
  129. package/dist/tools/ToolRegistry.js +0 -133
  130. package/dist/tools/ToolRegistry.js.map +0 -1
  131. package/src/ContextManagement.ts +0 -367
  132. package/src/Conversation.ts +0 -21
  133. package/src/RateLimiter.ts +0 -238
  134. package/src/observability/MeteredLlmService.ts +0 -168
  135. package/src/observability/Metrics.ts +0 -200
  136. package/src/observability/StructuredLogger.ts +0 -67
  137. package/src/observability/Tracing.ts +0 -146
  138. package/src/providers/OpenCodeModels.ts +0 -60
  139. package/src/providers/OpenCodeProvider.ts +0 -278
  140. package/src/tools/Tool.ts +0 -376
  141. package/src/tools/ToolCallingExecutor.ts +0 -106
  142. package/src/tools/ToolRegistry.ts +0 -224
@@ -1,238 +0,0 @@
1
- import * as Clock from "effect/Clock"
2
- import * as Context from "effect/Context"
3
- import * as Duration from "effect/Duration"
4
- import * as Effect from "effect/Effect"
5
- import * as Layer from "effect/Layer"
6
- import * as Ref from "effect/Ref"
7
- import * as Schema from "effect/Schema"
8
- import type { LlmConfig } from "./Models.ts"
9
-
10
- export class RateLimiterAcquireTimeout extends Schema.TaggedError<RateLimiterAcquireTimeout>()(
11
- "AcquireTimeout",
12
- {
13
- timeout: Schema.Duration
14
- }
15
- ) {}
16
-
17
- export class RateLimiterInvalidConfig extends Schema.TaggedError<RateLimiterInvalidConfig>()(
18
- "InvalidConfig",
19
- {
20
- details: Schema.String
21
- }
22
- ) {}
23
-
24
- export const RateLimiterError = Schema.Union([RateLimiterAcquireTimeout, RateLimiterInvalidConfig])
25
- export type RateLimiterError = typeof RateLimiterError.Type
26
-
27
- export class RateLimiterConfig extends Schema.Class<RateLimiterConfig>("RateLimiterConfig")({
28
- requestsPerMinute: Schema.Int.pipe(Schema.withConstructorDefault(Effect.succeed(60))),
29
- burstSize: Schema.Int.pipe(Schema.withConstructorDefault(Effect.succeed(10))),
30
- acquireTimeout: Schema.Duration.pipe(
31
- Schema.withConstructorDefault(Effect.succeed(Duration.seconds(30)))
32
- )
33
- }) {}
34
-
35
- export const rateLimiterConfigFromLlmConfig = (config: LlmConfig): RateLimiterConfig =>
36
- RateLimiterConfig.make({
37
- requestsPerMinute: config.requestsPerMinute,
38
- burstSize: config.burstSize,
39
- acquireTimeout: config.acquireTimeout
40
- })
41
-
42
- export class RateLimiterMetrics extends Schema.Class<RateLimiterMetrics>("RateLimiterMetrics")({
43
- totalRequests: Schema.Int,
44
- throttledRequests: Schema.Int,
45
- currentTokens: Schema.Int
46
- }) {}
47
-
48
- export interface RateLimiterShape {
49
- readonly acquire: Effect.Effect<void, RateLimiterError>
50
- readonly tryAcquire: Effect.Effect<boolean>
51
- readonly metrics: Effect.Effect<RateLimiterMetrics>
52
- }
53
-
54
- export class RateLimiter extends Context.Service<RateLimiter, RateLimiterShape>()(
55
- "@llm4ts/core/RateLimiter"
56
- ) {}
57
-
58
- interface BucketState {
59
- readonly tokens: number
60
- readonly lastRefillNanos: bigint
61
- }
62
-
63
- interface TakeResult {
64
- readonly acquired: boolean
65
- readonly tokensAfter: number
66
- readonly deficit: number
67
- }
68
-
69
- const nanosPerSecond = 1_000_000_000
70
-
71
- const configDetails = (config: RateLimiterConfig): string =>
72
- `requestsPerMinute=${config.requestsPerMinute}, burstSize=${config.burstSize}, ` +
73
- `acquireTimeout=${Duration.format(config.acquireTimeout)}`
74
-
75
- const isConfigValid = (config: RateLimiterConfig): boolean =>
76
- config.requestsPerMinute > 0 &&
77
- config.burstSize > 0 &&
78
- Duration.toMillis(config.acquireTimeout) > 0
79
-
80
- export const makeRateLimiter = Effect.fn("@llm4ts/core/RateLimiter.make")(function* (
81
- config: RateLimiterConfig
82
- ) {
83
- const initialNanos = yield* Clock.currentTimeNanos
84
- const state = yield* Ref.make<BucketState>({
85
- tokens: config.burstSize,
86
- lastRefillNanos: initialNanos
87
- })
88
- const metrics = yield* Ref.make(
89
- RateLimiterMetrics.make({
90
- totalRequests: 0,
91
- throttledRequests: 0,
92
- currentTokens: config.burstSize
93
- })
94
- )
95
- const capacity = config.burstSize
96
- const ratePerSecond = config.requestsPerMinute / 60
97
-
98
- const takeToken: Effect.Effect<TakeResult> = Effect.gen(function* () {
99
- const now = yield* Clock.currentTimeNanos
100
-
101
- return yield* Ref.modify(state, (current) => {
102
- const elapsedNanos = now - current.lastRefillNanos
103
- const tokensToAdd =
104
- elapsedNanos <= 0n ? 0 : (Number(elapsedNanos) / nanosPerSecond) * ratePerSecond
105
- const refilled =
106
- tokensToAdd <= 0 ? current.tokens : Math.min(capacity, current.tokens + tokensToAdd)
107
- const lastRefillNanos = tokensToAdd <= 0 ? current.lastRefillNanos : now
108
-
109
- if (refilled >= 1) {
110
- const remaining = refilled - 1
111
- return [
112
- {
113
- acquired: true,
114
- tokensAfter: remaining,
115
- deficit: 0
116
- },
117
- {
118
- tokens: remaining,
119
- lastRefillNanos
120
- }
121
- ]
122
- }
123
-
124
- return [
125
- {
126
- acquired: false,
127
- tokensAfter: refilled,
128
- deficit: 1 - refilled
129
- },
130
- {
131
- tokens: refilled,
132
- lastRefillNanos
133
- }
134
- ]
135
- })
136
- })
137
-
138
- const updateCurrentTokens = (tokens: number): Effect.Effect<void> =>
139
- Ref.update(metrics, (current) =>
140
- RateLimiterMetrics.make({
141
- ...current,
142
- currentTokens: Math.trunc(tokens)
143
- })
144
- )
145
-
146
- const waitForToken = (deficit: number): Effect.Effect<void, RateLimiterInvalidConfig> => {
147
- if (ratePerSecond <= 0) {
148
- return Effect.fail(
149
- RateLimiterInvalidConfig.make({
150
- details: configDetails(config)
151
- })
152
- )
153
- }
154
-
155
- const nanoseconds = BigInt(Math.max(0, Math.ceil((deficit / ratePerSecond) * nanosPerSecond)))
156
- return Effect.sleep(Duration.nanos(nanoseconds))
157
- }
158
-
159
- const acquireLoop = (alreadyThrottled: boolean): Effect.Effect<void, RateLimiterError> =>
160
- Effect.gen(function* () {
161
- const result = yield* takeToken
162
- yield* updateCurrentTokens(result.tokensAfter)
163
-
164
- if (result.acquired) {
165
- return
166
- }
167
-
168
- if (!alreadyThrottled) {
169
- yield* Ref.update(metrics, (current) =>
170
- RateLimiterMetrics.make({
171
- ...current,
172
- throttledRequests: current.throttledRequests + 1
173
- })
174
- )
175
- }
176
-
177
- yield* waitForToken(result.deficit)
178
- return yield* Effect.suspend(() => acquireLoop(true))
179
- })
180
-
181
- const acquire: Effect.Effect<void, RateLimiterError> = Effect.suspend(() => {
182
- if (!isConfigValid(config)) {
183
- return Effect.fail(
184
- RateLimiterInvalidConfig.make({
185
- details: configDetails(config)
186
- })
187
- )
188
- }
189
-
190
- return Ref.update(metrics, (current) =>
191
- RateLimiterMetrics.make({
192
- ...current,
193
- totalRequests: current.totalRequests + 1
194
- })
195
- ).pipe(
196
- Effect.andThen(acquireLoop(false)),
197
- Effect.timeoutOrElse({
198
- duration: config.acquireTimeout,
199
- orElse: () =>
200
- Effect.fail(
201
- RateLimiterAcquireTimeout.make({
202
- timeout: config.acquireTimeout
203
- })
204
- )
205
- })
206
- )
207
- })
208
-
209
- const tryAcquire: Effect.Effect<boolean> = Effect.suspend(() => {
210
- if (!isConfigValid(config)) {
211
- return Effect.logWarning(`Rate limiter config invalid: ${configDetails(config)}`).pipe(
212
- Effect.as(false)
213
- )
214
- }
215
-
216
- return Effect.gen(function* () {
217
- const result = yield* takeToken
218
- yield* updateCurrentTokens(result.tokensAfter)
219
- yield* Ref.update(metrics, (current) =>
220
- RateLimiterMetrics.make({
221
- ...current,
222
- totalRequests: current.totalRequests + 1,
223
- throttledRequests: current.throttledRequests + (result.acquired ? 0 : 1)
224
- })
225
- )
226
- return result.acquired
227
- })
228
- })
229
-
230
- return RateLimiter.of({
231
- acquire,
232
- tryAcquire,
233
- metrics: Ref.get(metrics)
234
- })
235
- })
236
-
237
- export const rateLimiterLayer = (config: RateLimiterConfig): Layer.Layer<RateLimiter> =>
238
- Layer.effect(RateLimiter, makeRateLimiter(config))
@@ -1,168 +0,0 @@
1
- /**
2
- * @deprecated Since 2.30.0 (ADR 0026): llm4ts exports OpenTelemetry spans and
3
- * metrics over OTLP from `@llm4ts/runner/Otel`; this in-memory module is not
4
- * used by flow or runner and will be removed in the next major.
5
- */
6
- import * as Clock from "effect/Clock"
7
- import * as Effect from "effect/Effect"
8
- import * as Ref from "effect/Ref"
9
- import * as Schema from "effect/Schema"
10
- import * as Stream from "effect/Stream"
11
- import type { LlmError } from "../Errors.ts"
12
- import type { LlmServiceShape, StructuredResult } from "../LlmService.ts"
13
- import type { LlmChunk, TokenUsage } from "../Models.ts"
14
- import { RequestMetrics } from "./Metrics.ts"
15
- import type { MetricsCollectorShape, RequestLabels } from "./Metrics.ts"
16
-
17
- export class LlmPricing extends Schema.Class<LlmPricing>("LlmPricing")({
18
- inputUsdPer1k: Schema.Number,
19
- outputUsdPer1k: Schema.Number
20
- }) {}
21
-
22
- export interface MeteredLlmOptions {
23
- readonly labels: RequestLabels
24
- readonly pricing?: LlmPricing
25
- }
26
-
27
- const estimatedCost = (
28
- usage: TokenUsage | undefined,
29
- pricing: LlmPricing | undefined
30
- ): number | undefined =>
31
- usage === undefined || pricing === undefined
32
- ? undefined
33
- : (usage.prompt / 1_000) * pricing.inputUsdPer1k +
34
- (usage.completion / 1_000) * pricing.outputUsdPer1k
35
-
36
- const complete = (
37
- collector: MetricsCollectorShape,
38
- options: MeteredLlmOptions,
39
- startedAt: number,
40
- endedAt: number,
41
- success: boolean,
42
- errorType: string | undefined,
43
- usage: TokenUsage | undefined
44
- ): Effect.Effect<void> => {
45
- const cost = estimatedCost(usage, options.pricing)
46
- return collector.recordCompleted(
47
- RequestMetrics.make({
48
- labels: options.labels,
49
- latencyMs: Math.max(0, endedAt - startedAt),
50
- success,
51
- ...(errorType === undefined ? {} : { errorType }),
52
- ...(usage === undefined ? {} : { tokenUsage: usage }),
53
- ...(cost === undefined ? {} : { estimatedCostUsd: cost })
54
- })
55
- )
56
- }
57
-
58
- const meterEffect = <A, R>(
59
- effect: Effect.Effect<A, LlmError, R>,
60
- collector: MetricsCollectorShape,
61
- options: MeteredLlmOptions,
62
- usageOf: (value: A) => TokenUsage | undefined
63
- ): Effect.Effect<A, LlmError, R> =>
64
- Effect.gen(function* () {
65
- const startedAt = yield* Clock.currentTimeMillis
66
- yield* collector.markRequestStarted(options.labels)
67
- const result = yield* Effect.result(effect)
68
- const endedAt = yield* Clock.currentTimeMillis
69
- yield* complete(
70
- collector,
71
- options,
72
- startedAt,
73
- endedAt,
74
- result._tag === "Success",
75
- result._tag === "Failure" ? result.failure._tag : undefined,
76
- result._tag === "Success" ? usageOf(result.success) : undefined
77
- )
78
- return yield* Effect.fromResult(result)
79
- })
80
-
81
- interface StreamMeasurement {
82
- readonly success: boolean
83
- readonly errorType: string | undefined
84
- readonly usage: TokenUsage | undefined
85
- }
86
-
87
- const meterStream = (
88
- stream: Stream.Stream<LlmChunk, LlmError>,
89
- collector: MetricsCollectorShape,
90
- options: MeteredLlmOptions
91
- ): Stream.Stream<LlmChunk, LlmError> =>
92
- Stream.unwrap(
93
- Effect.gen(function* () {
94
- const startedAt = yield* Clock.currentTimeMillis
95
- yield* collector.markRequestStarted(options.labels)
96
- const measurement = yield* Ref.make<StreamMeasurement>({
97
- success: true,
98
- errorType: undefined,
99
- usage: undefined
100
- })
101
- return stream.pipe(
102
- Stream.tap((chunk) =>
103
- chunk.usage === undefined
104
- ? Effect.void
105
- : Ref.update(measurement, (current) => ({
106
- ...current,
107
- usage: chunk.usage
108
- }))
109
- ),
110
- Stream.tapError((error) =>
111
- Ref.update(measurement, (current) => ({
112
- ...current,
113
- success: false,
114
- errorType: error._tag
115
- }))
116
- ),
117
- Stream.ensuring(
118
- Effect.gen(function* () {
119
- const current = yield* Ref.get(measurement)
120
- const endedAt = yield* Clock.currentTimeMillis
121
- yield* complete(
122
- collector,
123
- options,
124
- startedAt,
125
- endedAt,
126
- current.success,
127
- current.errorType,
128
- current.usage
129
- )
130
- })
131
- )
132
- )
133
- })
134
- )
135
-
136
- export const meterLlmService = (
137
- service: LlmServiceShape,
138
- collector: MetricsCollectorShape,
139
- options: MeteredLlmOptions
140
- ): LlmServiceShape => ({
141
- executeStream: (prompt) => meterStream(service.executeStream(prompt), collector, options),
142
- executeStreamWithHistory: (messages) =>
143
- meterStream(service.executeStreamWithHistory(messages), collector, options),
144
- executeWithTools: (prompt, tools) =>
145
- meterEffect(service.executeWithTools(prompt, tools), collector, options, () => undefined),
146
- executeStructured: (prompt, schema, jsonSchema) =>
147
- meterEffect(
148
- service.executeStructured(prompt, schema, jsonSchema),
149
- collector,
150
- options,
151
- () => undefined
152
- ),
153
- executeStructuredWithUsage: (prompt, schema, jsonSchema) =>
154
- meterEffect(
155
- service.executeStructuredWithUsage(prompt, schema, jsonSchema),
156
- collector,
157
- options,
158
- <A>(result: StructuredResult<A>) => result[1]
159
- ),
160
- scoreLabels: (prompt, labels) =>
161
- meterEffect(
162
- service.scoreLabels(prompt, labels),
163
- collector,
164
- options,
165
- (distribution) => distribution.usage
166
- ),
167
- isAvailable: service.isAvailable
168
- })
@@ -1,200 +0,0 @@
1
- /**
2
- * @deprecated Since 2.30.0 (ADR 0026): llm4ts exports OpenTelemetry spans and
3
- * metrics over OTLP from `@llm4ts/runner/Otel`; this in-memory module is not
4
- * used by flow or runner and will be removed in the next major.
5
- */
6
- import * as Context from "effect/Context"
7
- import * as Effect from "effect/Effect"
8
- import * as Layer from "effect/Layer"
9
- import * as Ref from "effect/Ref"
10
- import * as Schema from "effect/Schema"
11
- import { TokenUsage } from "../Models.ts"
12
-
13
- export class RequestLabels extends Schema.Class<RequestLabels>("RequestLabels")({
14
- provider: Schema.String,
15
- model: Schema.String,
16
- agent: Schema.optionalKey(Schema.String),
17
- runId: Schema.optionalKey(Schema.String),
18
- workflowStep: Schema.optionalKey(Schema.String)
19
- }) {}
20
-
21
- export class RequestMetrics extends Schema.Class<RequestMetrics>("RequestMetrics")({
22
- labels: RequestLabels,
23
- tokenUsage: Schema.optionalKey(TokenUsage),
24
- latencyMs: Schema.Number,
25
- success: Schema.Boolean,
26
- errorType: Schema.optionalKey(Schema.String),
27
- estimatedCostUsd: Schema.optionalKey(Schema.Number)
28
- }) {}
29
-
30
- export class ProviderHealth extends Schema.Class<ProviderHealth>("ProviderHealth")({
31
- provider: Schema.String,
32
- requestCount: Schema.Int,
33
- successRate: Schema.Number,
34
- avgLatencyMs: Schema.Number,
35
- p95LatencyMs: Schema.Number,
36
- estimatedCostUsd: Schema.Number
37
- }) {}
38
-
39
- export class ObservabilitySnapshot extends Schema.Class<ObservabilitySnapshot>(
40
- "ObservabilitySnapshot"
41
- )({
42
- totalRequests: Schema.Int,
43
- totalErrors: Schema.Int,
44
- activeRequests: Schema.Int,
45
- promptTokens: Schema.Int,
46
- completionTokens: Schema.Int,
47
- totalTokens: Schema.Int,
48
- estimatedCostUsd: Schema.Number,
49
- p50LatencyMs: Schema.Number,
50
- p95LatencyMs: Schema.Number,
51
- byProvider: Schema.Array(ProviderHealth),
52
- byAgentRequests: Schema.Record(Schema.String, Schema.Int)
53
- }) {}
54
-
55
- export class DashboardMetricsSnapshot extends Schema.Class<DashboardMetricsSnapshot>(
56
- "DashboardMetricsSnapshot"
57
- )({
58
- providerHealth: Schema.Array(ProviderHealth),
59
- totalTokens: Schema.Int,
60
- totalCostUsd: Schema.Number,
61
- totalRequests: Schema.Int,
62
- errorRate: Schema.Number
63
- }) {}
64
-
65
- export interface MetricsCollectorShape {
66
- readonly markRequestStarted: (labels: RequestLabels) => Effect.Effect<void>
67
- readonly recordCompleted: (metrics: RequestMetrics) => Effect.Effect<void>
68
- readonly snapshot: Effect.Effect<ObservabilitySnapshot>
69
- readonly dashboardSnapshot: Effect.Effect<DashboardMetricsSnapshot>
70
- }
71
-
72
- export class MetricsCollector extends Context.Service<MetricsCollector, MetricsCollectorShape>()(
73
- "@llm4ts/core/observability/MetricsCollector"
74
- ) {}
75
-
76
- interface MetricsState {
77
- readonly totalRequests: number
78
- readonly activeRequests: number
79
- readonly completed: ReadonlyArray<RequestMetrics>
80
- readonly byAgentRequests: ReadonlyMap<string, number>
81
- }
82
-
83
- const percentile = (values: ReadonlyArray<number>, quantile: number): number => {
84
- if (values.length === 0) {
85
- return 0
86
- }
87
- const sorted = [...values].sort((left, right) => left - right)
88
- const index = Math.max(0, Math.ceil(quantile * sorted.length) - 1)
89
- return sorted[index] ?? 0
90
- }
91
-
92
- const providerHealth = (
93
- completed: ReadonlyArray<RequestMetrics>
94
- ): ReadonlyArray<ProviderHealth> => {
95
- const providers = new Map<string, Array<RequestMetrics>>()
96
- for (const metric of completed) {
97
- const current = providers.get(metric.labels.provider) ?? []
98
- current.push(metric)
99
- providers.set(metric.labels.provider, current)
100
- }
101
- return [...providers.entries()]
102
- .sort(([left], [right]) => left.localeCompare(right))
103
- .map(([provider, metrics]) => {
104
- const latencies = metrics.map((metric) => metric.latencyMs)
105
- const successes = metrics.filter((metric) => metric.success).length
106
- return ProviderHealth.make({
107
- provider,
108
- requestCount: metrics.length,
109
- successRate: metrics.length === 0 ? 1 : successes / metrics.length,
110
- avgLatencyMs:
111
- metrics.length === 0
112
- ? 0
113
- : latencies.reduce((sum, latency) => sum + latency, 0) / metrics.length,
114
- p95LatencyMs: percentile(latencies, 0.95),
115
- estimatedCostUsd: metrics.reduce((sum, metric) => sum + (metric.estimatedCostUsd ?? 0), 0)
116
- })
117
- })
118
- }
119
-
120
- const toSnapshot = (state: MetricsState): ObservabilitySnapshot => {
121
- const completed = state.completed
122
- const promptTokens = completed.reduce((sum, metric) => sum + (metric.tokenUsage?.prompt ?? 0), 0)
123
- const completionTokens = completed.reduce(
124
- (sum, metric) => sum + (metric.tokenUsage?.completion ?? 0),
125
- 0
126
- )
127
- return ObservabilitySnapshot.make({
128
- totalRequests: state.totalRequests,
129
- totalErrors: completed.filter((metric) => !metric.success).length,
130
- activeRequests: state.activeRequests,
131
- promptTokens,
132
- completionTokens,
133
- totalTokens: promptTokens + completionTokens,
134
- estimatedCostUsd: completed.reduce((sum, metric) => sum + (metric.estimatedCostUsd ?? 0), 0),
135
- p50LatencyMs: percentile(
136
- completed.map((metric) => metric.latencyMs),
137
- 0.5
138
- ),
139
- p95LatencyMs: percentile(
140
- completed.map((metric) => metric.latencyMs),
141
- 0.95
142
- ),
143
- byProvider: providerHealth(completed),
144
- byAgentRequests: Object.fromEntries(state.byAgentRequests)
145
- })
146
- }
147
-
148
- export const makeMetricsCollector = Effect.fn("@llm4ts/core/observability/MetricsCollector.make")(
149
- function* (): Effect.fn.Return<MetricsCollectorShape> {
150
- const state = yield* Ref.make<MetricsState>({
151
- totalRequests: 0,
152
- activeRequests: 0,
153
- completed: [],
154
- byAgentRequests: new Map()
155
- })
156
-
157
- const markRequestStarted = (labels: RequestLabels): Effect.Effect<void> =>
158
- Ref.update(state, (current) => {
159
- const agents = new Map(current.byAgentRequests)
160
- if (labels.agent !== undefined) {
161
- agents.set(labels.agent, (agents.get(labels.agent) ?? 0) + 1)
162
- }
163
- return {
164
- totalRequests: current.totalRequests + 1,
165
- activeRequests: current.activeRequests + 1,
166
- completed: current.completed,
167
- byAgentRequests: agents
168
- }
169
- })
170
-
171
- const recordCompleted = (metrics: RequestMetrics): Effect.Effect<void> =>
172
- Ref.update(state, (current) => ({
173
- ...current,
174
- activeRequests: Math.max(0, current.activeRequests - 1),
175
- completed: [...current.completed, metrics]
176
- }))
177
-
178
- const snapshot = Ref.get(state).pipe(Effect.map(toSnapshot))
179
- const dashboardSnapshot = snapshot.pipe(
180
- Effect.map((current) =>
181
- DashboardMetricsSnapshot.make({
182
- providerHealth: current.byProvider,
183
- totalTokens: current.totalTokens,
184
- totalCostUsd: current.estimatedCostUsd,
185
- totalRequests: current.totalRequests,
186
- errorRate: current.totalRequests === 0 ? 0 : current.totalErrors / current.totalRequests
187
- })
188
- )
189
- )
190
-
191
- return {
192
- markRequestStarted,
193
- recordCompleted,
194
- snapshot,
195
- dashboardSnapshot
196
- }
197
- }
198
- )
199
-
200
- export const MetricsCollectorLive = Layer.effect(MetricsCollector, makeMetricsCollector())
@@ -1,67 +0,0 @@
1
- /**
2
- * @deprecated Since 2.30.0 (ADR 0026): llm4ts exports OpenTelemetry spans and
3
- * metrics over OTLP from `@llm4ts/runner/Otel`; this in-memory module is not
4
- * used by flow or runner and will be removed in the next major.
5
- */
6
- import * as Clock from "effect/Clock"
7
- import * as Context from "effect/Context"
8
- import * as Effect from "effect/Effect"
9
- import * as Layer from "effect/Layer"
10
- import * as Schema from "effect/Schema"
11
- import type { JsonValue } from "../providers/CliSupport.ts"
12
- import { redactText, sanitizeFields, type RedactionOptions } from "./Redaction.ts"
13
- import type { TracingServiceShape } from "./Tracing.ts"
14
-
15
- export const LogLevel = Schema.Literals(["Debug", "Info", "Warning", "Error"])
16
- export type LogLevel = typeof LogLevel.Type
17
-
18
- export class StructuredLogEvent extends Schema.Class<StructuredLogEvent>("StructuredLogEvent")({
19
- level: LogLevel,
20
- message: Schema.String,
21
- correlationId: Schema.String,
22
- timestamp: Schema.Number,
23
- fields: Schema.Record(Schema.String, Schema.Json)
24
- }) {}
25
-
26
- export interface StructuredLoggerShape {
27
- readonly log: (
28
- level: LogLevel,
29
- message: string,
30
- fields?: Readonly<Record<string, JsonValue>>
31
- ) => Effect.Effect<void>
32
- }
33
-
34
- export class StructuredLogger extends Context.Service<StructuredLogger, StructuredLoggerShape>()(
35
- "@llm4ts/core/observability/StructuredLogger"
36
- ) {}
37
-
38
- export interface StructuredLoggerOptions extends RedactionOptions {
39
- readonly sink: (event: StructuredLogEvent) => Effect.Effect<void>
40
- }
41
-
42
- export const makeStructuredLogger = (
43
- tracing: TracingServiceShape,
44
- options: StructuredLoggerOptions
45
- ): StructuredLoggerShape => ({
46
- log: Effect.fn("@llm4ts/core/observability/StructuredLogger.log")(function* (
47
- level: LogLevel,
48
- message: string,
49
- fields: Readonly<Record<string, JsonValue>> = {}
50
- ) {
51
- const correlationId = yield* tracing.correlationId
52
- const timestamp = yield* Clock.currentTimeMillis
53
- yield* options.sink(
54
- StructuredLogEvent.make({
55
- level,
56
- message: redactText(message, options),
57
- correlationId,
58
- timestamp,
59
- fields: sanitizeFields(fields, options)
60
- })
61
- )
62
- })
63
- })
64
-
65
- export const StructuredLoggerNoop = Layer.succeed(StructuredLogger)({
66
- log: (_level, _message, _fields) => Effect.void
67
- })