@llm4ts/core 0.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 (284) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -0
  3. package/dist/AgentSession.d.ts +66 -0
  4. package/dist/AgentSession.d.ts.map +1 -0
  5. package/dist/AgentSession.js +53 -0
  6. package/dist/AgentSession.js.map +1 -0
  7. package/dist/Capability.d.ts +101 -0
  8. package/dist/Capability.d.ts.map +1 -0
  9. package/dist/Capability.js +210 -0
  10. package/dist/Capability.js.map +1 -0
  11. package/dist/Connector.d.ts +69 -0
  12. package/dist/Connector.d.ts.map +1 -0
  13. package/dist/Connector.js +86 -0
  14. package/dist/Connector.js.map +1 -0
  15. package/dist/ConnectorConfig.d.ts +70 -0
  16. package/dist/ConnectorConfig.d.ts.map +1 -0
  17. package/dist/ConnectorConfig.js +110 -0
  18. package/dist/ConnectorConfig.js.map +1 -0
  19. package/dist/ConnectorRegistry.d.ts +28 -0
  20. package/dist/ConnectorRegistry.d.ts.map +1 -0
  21. package/dist/ConnectorRegistry.js +87 -0
  22. package/dist/ConnectorRegistry.js.map +1 -0
  23. package/dist/ContextManagement.d.ts +78 -0
  24. package/dist/ContextManagement.d.ts.map +1 -0
  25. package/dist/ContextManagement.js +221 -0
  26. package/dist/ContextManagement.js.map +1 -0
  27. package/dist/Conversation.d.ts +18 -0
  28. package/dist/Conversation.d.ts.map +1 -0
  29. package/dist/Conversation.js +17 -0
  30. package/dist/Conversation.js.map +1 -0
  31. package/dist/Errors.d.ts +64 -0
  32. package/dist/Errors.d.ts.map +1 -0
  33. package/dist/Errors.js +76 -0
  34. package/dist/Errors.js.map +1 -0
  35. package/dist/HttpClient.d.ts +43 -0
  36. package/dist/HttpClient.d.ts.map +1 -0
  37. package/dist/HttpClient.js +99 -0
  38. package/dist/HttpClient.js.map +1 -0
  39. package/dist/LlmService.d.ts +24 -0
  40. package/dist/LlmService.d.ts.map +1 -0
  41. package/dist/LlmService.js +4 -0
  42. package/dist/LlmService.js.map +1 -0
  43. package/dist/Models.d.ts +138 -0
  44. package/dist/Models.d.ts.map +1 -0
  45. package/dist/Models.js +209 -0
  46. package/dist/Models.js.map +1 -0
  47. package/dist/ProcessExecutor.d.ts +57 -0
  48. package/dist/ProcessExecutor.d.ts.map +1 -0
  49. package/dist/ProcessExecutor.js +71 -0
  50. package/dist/ProcessExecutor.js.map +1 -0
  51. package/dist/RateLimiter.d.ts +44 -0
  52. package/dist/RateLimiter.d.ts.map +1 -0
  53. package/dist/RateLimiter.js +155 -0
  54. package/dist/RateLimiter.js.map +1 -0
  55. package/dist/Streaming.d.ts +17 -0
  56. package/dist/Streaming.d.ts.map +1 -0
  57. package/dist/Streaming.js +65 -0
  58. package/dist/Streaming.js.map +1 -0
  59. package/dist/StructuredOutput.d.ts +10 -0
  60. package/dist/StructuredOutput.d.ts.map +1 -0
  61. package/dist/StructuredOutput.js +97 -0
  62. package/dist/StructuredOutput.js.map +1 -0
  63. package/dist/TemporaryFiles.d.ts +20 -0
  64. package/dist/TemporaryFiles.d.ts.map +1 -0
  65. package/dist/TemporaryFiles.js +18 -0
  66. package/dist/TemporaryFiles.js.map +1 -0
  67. package/dist/UsageLimits.d.ts +5 -0
  68. package/dist/UsageLimits.d.ts.map +1 -0
  69. package/dist/UsageLimits.js +187 -0
  70. package/dist/UsageLimits.js.map +1 -0
  71. package/dist/eval/Checks.d.ts +6 -0
  72. package/dist/eval/Checks.d.ts.map +1 -0
  73. package/dist/eval/Checks.js +39 -0
  74. package/dist/eval/Checks.js.map +1 -0
  75. package/dist/eval/Eval.d.ts +57 -0
  76. package/dist/eval/Eval.d.ts.map +1 -0
  77. package/dist/eval/Eval.js +91 -0
  78. package/dist/eval/Eval.js.map +1 -0
  79. package/dist/eval/EvalSuite.d.ts +21 -0
  80. package/dist/eval/EvalSuite.d.ts.map +1 -0
  81. package/dist/eval/EvalSuite.js +30 -0
  82. package/dist/eval/EvalSuite.js.map +1 -0
  83. package/dist/eval/Evaluator.d.ts +10 -0
  84. package/dist/eval/Evaluator.d.ts.map +1 -0
  85. package/dist/eval/Evaluator.js +14 -0
  86. package/dist/eval/Evaluator.js.map +1 -0
  87. package/dist/eval/Judge.d.ts +15 -0
  88. package/dist/eval/Judge.d.ts.map +1 -0
  89. package/dist/eval/Judge.js +73 -0
  90. package/dist/eval/Judge.js.map +1 -0
  91. package/dist/observability/MeteredLlmService.d.ts +16 -0
  92. package/dist/observability/MeteredLlmService.d.ts.map +1 -0
  93. package/dist/observability/MeteredLlmService.js +66 -0
  94. package/dist/observability/MeteredLlmService.js.map +1 -0
  95. package/dist/observability/Metrics.d.ts +71 -0
  96. package/dist/observability/Metrics.d.ts.map +1 -0
  97. package/dist/observability/Metrics.js +147 -0
  98. package/dist/observability/Metrics.js.map +1 -0
  99. package/dist/observability/Redaction.d.ts +8 -0
  100. package/dist/observability/Redaction.d.ts.map +1 -0
  101. package/dist/observability/Redaction.js +38 -0
  102. package/dist/observability/Redaction.js.map +1 -0
  103. package/dist/observability/StreamRecorder.d.ts +9 -0
  104. package/dist/observability/StreamRecorder.d.ts.map +1 -0
  105. package/dist/observability/StreamRecorder.js +10 -0
  106. package/dist/observability/StreamRecorder.js.map +1 -0
  107. package/dist/observability/StructuredLogger.d.ts +31 -0
  108. package/dist/observability/StructuredLogger.d.ts.map +1 -0
  109. package/dist/observability/StructuredLogger.js +34 -0
  110. package/dist/observability/StructuredLogger.js.map +1 -0
  111. package/dist/observability/Tracing.d.ts +38 -0
  112. package/dist/observability/Tracing.d.ts.map +1 -0
  113. package/dist/observability/Tracing.js +75 -0
  114. package/dist/observability/Tracing.js.map +1 -0
  115. package/dist/providers/AnthropicModels.d.ts +95 -0
  116. package/dist/providers/AnthropicModels.d.ts.map +1 -0
  117. package/dist/providers/AnthropicModels.js +83 -0
  118. package/dist/providers/AnthropicModels.js.map +1 -0
  119. package/dist/providers/AnthropicProvider.d.ts +13 -0
  120. package/dist/providers/AnthropicProvider.d.ts.map +1 -0
  121. package/dist/providers/AnthropicProvider.js +177 -0
  122. package/dist/providers/AnthropicProvider.js.map +1 -0
  123. package/dist/providers/AntigravityConnector.d.ts +7 -0
  124. package/dist/providers/AntigravityConnector.d.ts.map +1 -0
  125. package/dist/providers/AntigravityConnector.js +52 -0
  126. package/dist/providers/AntigravityConnector.js.map +1 -0
  127. package/dist/providers/ClaudeAgentSession.d.ts +11 -0
  128. package/dist/providers/ClaudeAgentSession.d.ts.map +1 -0
  129. package/dist/providers/ClaudeAgentSession.js +158 -0
  130. package/dist/providers/ClaudeAgentSession.js.map +1 -0
  131. package/dist/providers/ClaudeCliConnector.d.ts +9 -0
  132. package/dist/providers/ClaudeCliConnector.d.ts.map +1 -0
  133. package/dist/providers/ClaudeCliConnector.js +123 -0
  134. package/dist/providers/ClaudeCliConnector.js.map +1 -0
  135. package/dist/providers/CliSupport.d.ts +21 -0
  136. package/dist/providers/CliSupport.d.ts.map +1 -0
  137. package/dist/providers/CliSupport.js +62 -0
  138. package/dist/providers/CliSupport.js.map +1 -0
  139. package/dist/providers/CodexConnector.d.ts +12 -0
  140. package/dist/providers/CodexConnector.d.ts.map +1 -0
  141. package/dist/providers/CodexConnector.js +175 -0
  142. package/dist/providers/CodexConnector.js.map +1 -0
  143. package/dist/providers/ConnectorFactories.d.ts +15 -0
  144. package/dist/providers/ConnectorFactories.d.ts.map +1 -0
  145. package/dist/providers/ConnectorFactories.js +74 -0
  146. package/dist/providers/ConnectorFactories.js.map +1 -0
  147. package/dist/providers/CopilotConnector.d.ts +6 -0
  148. package/dist/providers/CopilotConnector.d.ts.map +1 -0
  149. package/dist/providers/CopilotConnector.js +35 -0
  150. package/dist/providers/CopilotConnector.js.map +1 -0
  151. package/dist/providers/CursorConnector.d.ts +8 -0
  152. package/dist/providers/CursorConnector.d.ts.map +1 -0
  153. package/dist/providers/CursorConnector.js +82 -0
  154. package/dist/providers/CursorConnector.js.map +1 -0
  155. package/dist/providers/GeminiApiProvider.d.ts +9 -0
  156. package/dist/providers/GeminiApiProvider.d.ts.map +1 -0
  157. package/dist/providers/GeminiApiProvider.js +181 -0
  158. package/dist/providers/GeminiApiProvider.js.map +1 -0
  159. package/dist/providers/GeminiCliProvider.d.ts +106 -0
  160. package/dist/providers/GeminiCliProvider.d.ts.map +1 -0
  161. package/dist/providers/GeminiCliProvider.js +596 -0
  162. package/dist/providers/GeminiCliProvider.js.map +1 -0
  163. package/dist/providers/GeminiModels.d.ts +92 -0
  164. package/dist/providers/GeminiModels.d.ts.map +1 -0
  165. package/dist/providers/GeminiModels.js +80 -0
  166. package/dist/providers/GeminiModels.js.map +1 -0
  167. package/dist/providers/GrokCliConnector.d.ts +8 -0
  168. package/dist/providers/GrokCliConnector.d.ts.map +1 -0
  169. package/dist/providers/GrokCliConnector.js +82 -0
  170. package/dist/providers/GrokCliConnector.js.map +1 -0
  171. package/dist/providers/LmStudioModels.d.ts +114 -0
  172. package/dist/providers/LmStudioModels.d.ts.map +1 -0
  173. package/dist/providers/LmStudioModels.js +102 -0
  174. package/dist/providers/LmStudioModels.js.map +1 -0
  175. package/dist/providers/LmStudioProvider.d.ts +13 -0
  176. package/dist/providers/LmStudioProvider.d.ts.map +1 -0
  177. package/dist/providers/LmStudioProvider.js +158 -0
  178. package/dist/providers/LmStudioProvider.js.map +1 -0
  179. package/dist/providers/MockProvider.d.ts +6 -0
  180. package/dist/providers/MockProvider.d.ts.map +1 -0
  181. package/dist/providers/MockProvider.js +271 -0
  182. package/dist/providers/MockProvider.js.map +1 -0
  183. package/dist/providers/OllamaModels.d.ts +68 -0
  184. package/dist/providers/OllamaModels.d.ts.map +1 -0
  185. package/dist/providers/OllamaModels.js +59 -0
  186. package/dist/providers/OllamaModels.js.map +1 -0
  187. package/dist/providers/OllamaProvider.d.ts +7 -0
  188. package/dist/providers/OllamaProvider.d.ts.map +1 -0
  189. package/dist/providers/OllamaProvider.js +134 -0
  190. package/dist/providers/OllamaProvider.js.map +1 -0
  191. package/dist/providers/OpenAIModels.d.ts +139 -0
  192. package/dist/providers/OpenAIModels.d.ts.map +1 -0
  193. package/dist/providers/OpenAIModels.js +123 -0
  194. package/dist/providers/OpenAIModels.js.map +1 -0
  195. package/dist/providers/OpenAIProvider.d.ts +9 -0
  196. package/dist/providers/OpenAIProvider.d.ts.map +1 -0
  197. package/dist/providers/OpenAIProvider.js +185 -0
  198. package/dist/providers/OpenAIProvider.js.map +1 -0
  199. package/dist/providers/OpenCodeCliConnector.d.ts +8 -0
  200. package/dist/providers/OpenCodeCliConnector.d.ts.map +1 -0
  201. package/dist/providers/OpenCodeCliConnector.js +93 -0
  202. package/dist/providers/OpenCodeCliConnector.js.map +1 -0
  203. package/dist/providers/OpenCodeModels.d.ts +61 -0
  204. package/dist/providers/OpenCodeModels.d.ts.map +1 -0
  205. package/dist/providers/OpenCodeModels.js +53 -0
  206. package/dist/providers/OpenCodeModels.js.map +1 -0
  207. package/dist/providers/OpenCodeProvider.d.ts +10 -0
  208. package/dist/providers/OpenCodeProvider.d.ts.map +1 -0
  209. package/dist/providers/OpenCodeProvider.js +154 -0
  210. package/dist/providers/OpenCodeProvider.js.map +1 -0
  211. package/dist/providers/PiConnector.d.ts +8 -0
  212. package/dist/providers/PiConnector.d.ts.map +1 -0
  213. package/dist/providers/PiConnector.js +99 -0
  214. package/dist/providers/PiConnector.js.map +1 -0
  215. package/dist/tools/Tool.d.ts +83 -0
  216. package/dist/tools/Tool.d.ts.map +1 -0
  217. package/dist/tools/Tool.js +278 -0
  218. package/dist/tools/Tool.js.map +1 -0
  219. package/dist/tools/ToolCallingExecutor.d.ts +18 -0
  220. package/dist/tools/ToolCallingExecutor.d.ts.map +1 -0
  221. package/dist/tools/ToolCallingExecutor.js +64 -0
  222. package/dist/tools/ToolCallingExecutor.js.map +1 -0
  223. package/dist/tools/ToolRegistry.d.ts +45 -0
  224. package/dist/tools/ToolRegistry.d.ts.map +1 -0
  225. package/dist/tools/ToolRegistry.js +133 -0
  226. package/dist/tools/ToolRegistry.js.map +1 -0
  227. package/package.json +105 -0
  228. package/src/.gitkeep +1 -0
  229. package/src/AgentSession.ts +69 -0
  230. package/src/Capability.ts +240 -0
  231. package/src/Connector.ts +220 -0
  232. package/src/ConnectorConfig.ts +146 -0
  233. package/src/ConnectorRegistry.ts +184 -0
  234. package/src/ContextManagement.ts +372 -0
  235. package/src/Conversation.ts +21 -0
  236. package/src/Errors.ts +87 -0
  237. package/src/HttpClient.ts +202 -0
  238. package/src/LlmService.ts +45 -0
  239. package/src/Models.ts +250 -0
  240. package/src/ProcessExecutor.ts +157 -0
  241. package/src/RateLimiter.ts +238 -0
  242. package/src/Streaming.ts +164 -0
  243. package/src/StructuredOutput.ts +138 -0
  244. package/src/TemporaryFiles.ts +51 -0
  245. package/src/UsageLimits.ts +242 -0
  246. package/src/eval/Checks.ts +68 -0
  247. package/src/eval/Eval.ts +112 -0
  248. package/src/eval/EvalSuite.ts +63 -0
  249. package/src/eval/Evaluator.ts +31 -0
  250. package/src/eval/Judge.ts +92 -0
  251. package/src/observability/MeteredLlmService.ts +156 -0
  252. package/src/observability/Metrics.ts +195 -0
  253. package/src/observability/Redaction.ts +55 -0
  254. package/src/observability/StreamRecorder.ts +32 -0
  255. package/src/observability/StructuredLogger.ts +62 -0
  256. package/src/observability/Tracing.ts +141 -0
  257. package/src/providers/AnthropicModels.ts +96 -0
  258. package/src/providers/AnthropicProvider.ts +320 -0
  259. package/src/providers/AntigravityConnector.ts +77 -0
  260. package/src/providers/ClaudeAgentSession.ts +226 -0
  261. package/src/providers/ClaudeCliConnector.ts +201 -0
  262. package/src/providers/CliSupport.ts +100 -0
  263. package/src/providers/CodexConnector.ts +251 -0
  264. package/src/providers/ConnectorFactories.ts +142 -0
  265. package/src/providers/CopilotConnector.ts +47 -0
  266. package/src/providers/CursorConnector.ts +120 -0
  267. package/src/providers/GeminiApiProvider.ts +337 -0
  268. package/src/providers/GeminiCliProvider.ts +793 -0
  269. package/src/providers/GeminiModels.ts +97 -0
  270. package/src/providers/GrokCliConnector.ts +117 -0
  271. package/src/providers/LmStudioModels.ts +117 -0
  272. package/src/providers/LmStudioProvider.ts +260 -0
  273. package/src/providers/MockProvider.ts +322 -0
  274. package/src/providers/OllamaModels.ts +64 -0
  275. package/src/providers/OllamaProvider.ts +236 -0
  276. package/src/providers/OpenAIModels.ts +152 -0
  277. package/src/providers/OpenAIProvider.ts +331 -0
  278. package/src/providers/OpenCodeCliConnector.ts +135 -0
  279. package/src/providers/OpenCodeModels.ts +60 -0
  280. package/src/providers/OpenCodeProvider.ts +278 -0
  281. package/src/providers/PiConnector.ts +142 -0
  282. package/src/tools/Tool.ts +379 -0
  283. package/src/tools/ToolCallingExecutor.ts +106 -0
  284. package/src/tools/ToolRegistry.ts +224 -0
@@ -0,0 +1,51 @@
1
+ import * as Context from "effect/Context"
2
+ import * as Effect from "effect/Effect"
3
+ import * as Ref from "effect/Ref"
4
+ import type * as Scope from "effect/Scope"
5
+ import type { LlmError } from "./Errors.ts"
6
+
7
+ export interface TemporaryFilesShape {
8
+ readonly write: (
9
+ prefix: string,
10
+ suffix: string,
11
+ contents: string
12
+ ) => Effect.Effect<string, LlmError, Scope.Scope>
13
+ }
14
+
15
+ export class TemporaryFiles extends Context.Service<TemporaryFiles, TemporaryFilesShape>()(
16
+ "@llm4ts/core/TemporaryFiles"
17
+ ) {}
18
+
19
+ export interface FakeTemporaryFiles {
20
+ readonly files: Effect.Effect<
21
+ ReadonlyArray<{
22
+ readonly path: string
23
+ readonly contents: string
24
+ }>
25
+ >
26
+ readonly temporaryFiles: TemporaryFilesShape
27
+ }
28
+
29
+ export const makeFakeTemporaryFiles = (
30
+ path = "/tmp/llm4ts-schema.json"
31
+ ): Effect.Effect<FakeTemporaryFiles> =>
32
+ Ref.make<
33
+ ReadonlyArray<{
34
+ readonly path: string
35
+ readonly contents: string
36
+ }>
37
+ >([]).pipe(
38
+ Effect.map((files) => ({
39
+ files: Ref.get(files),
40
+ temporaryFiles: {
41
+ write: (_prefix, _suffix, contents) =>
42
+ Ref.update(files, (current) => [
43
+ ...current,
44
+ {
45
+ path,
46
+ contents
47
+ }
48
+ ]).pipe(Effect.as(path))
49
+ }
50
+ }))
51
+ )
@@ -0,0 +1,242 @@
1
+ import * as DateTime from "effect/DateTime"
2
+ import * as Duration from "effect/Duration"
3
+ import { RateLimitError, UsageLimitError, type LlmError } from "./Errors.ts"
4
+
5
+ const codexAt = /try again at\s+(\d{1,2}:\d{2}\s*[AP]M)/i
6
+ const claudeAt = /usage limit.*?(?:resets?|try again) at\s+(\d{1,2}(?::\d{2})?\s*[ap]m)/i
7
+ const geminiReset = /reset after\s+((?:\d+(?:ms|[dhms]))+)/i
8
+ const resetComponent = /(\d+)(ms|[dhms])/gi
9
+ const shortResetLimit = Duration.seconds(120)
10
+
11
+ const geminiQuotaSignals: ReadonlyArray<string> = [
12
+ "exhausted your capacity",
13
+ "quota",
14
+ "resource_exhausted",
15
+ "rate_limit",
16
+ "rate limit",
17
+ "429"
18
+ ]
19
+
20
+ const genericQuotaSignals: ReadonlyArray<string> = [
21
+ ...geminiQuotaSignals,
22
+ "usage limit",
23
+ "out of credits",
24
+ "too many requests"
25
+ ]
26
+
27
+ const includesSignal = (text: string, signals: ReadonlyArray<string>): boolean => {
28
+ const normalized = text.toLowerCase()
29
+ return signals.some((signal) => normalized.includes(signal))
30
+ }
31
+
32
+ export const isGeminiQuota = (text: string): boolean => includesSignal(text, geminiQuotaSignals)
33
+
34
+ const isGenericQuota = (text: string): boolean => includesSignal(text, genericQuotaSignals)
35
+
36
+ const resetDuration = (specification: string): Duration.Duration => {
37
+ let milliseconds = 0
38
+
39
+ for (const match of specification.matchAll(resetComponent)) {
40
+ const amount = Number(match[1] ?? "0")
41
+ const unit = (match[2] ?? "ms").toLowerCase()
42
+
43
+ switch (unit) {
44
+ case "d":
45
+ milliseconds += amount * 86_400_000
46
+ break
47
+ case "h":
48
+ milliseconds += amount * 3_600_000
49
+ break
50
+ case "m":
51
+ milliseconds += amount * 60_000
52
+ break
53
+ case "s":
54
+ milliseconds += amount * 1_000
55
+ break
56
+ default:
57
+ milliseconds += amount
58
+ }
59
+ }
60
+
61
+ return Duration.millis(milliseconds)
62
+ }
63
+
64
+ interface WallClockTime {
65
+ readonly hour: number
66
+ readonly minute: number
67
+ }
68
+
69
+ const parseWallClockTime = (raw: string): WallClockTime | undefined => {
70
+ const match = raw.trim().match(/^(\d{1,2})(?::(\d{2}))?\s*([ap])m$/i)
71
+ if (match === null) {
72
+ return undefined
73
+ }
74
+
75
+ const clockHour = Number(match[1])
76
+ const minute = Number(match[2] ?? "0")
77
+ const period = (match[3] ?? "").toLowerCase()
78
+ if (clockHour < 1 || clockHour > 12 || minute < 0 || minute > 59) {
79
+ return undefined
80
+ }
81
+
82
+ return {
83
+ hour: (clockHour % 12) + (period === "p" ? 12 : 0),
84
+ minute
85
+ }
86
+ }
87
+
88
+ interface CalendarDate {
89
+ readonly year: number
90
+ readonly month: number
91
+ readonly day: number
92
+ }
93
+
94
+ const dateTimeFormatter = (timeZone: string): Intl.DateTimeFormat =>
95
+ new Intl.DateTimeFormat("en-US-u-nu-latn", {
96
+ timeZone,
97
+ year: "numeric",
98
+ month: "2-digit",
99
+ day: "2-digit",
100
+ hour: "2-digit",
101
+ minute: "2-digit",
102
+ second: "2-digit",
103
+ hourCycle: "h23"
104
+ })
105
+
106
+ const numericPart = (
107
+ parts: ReadonlyArray<Intl.DateTimeFormatPart>,
108
+ type: Intl.DateTimeFormatPartTypes
109
+ ): number => Number(parts.find((part) => part.type === type)?.value ?? "0")
110
+
111
+ const localParts = (epochMillis: number, timeZone: string) => {
112
+ const parts = dateTimeFormatter(timeZone).formatToParts(new Date(epochMillis))
113
+ return {
114
+ year: numericPart(parts, "year"),
115
+ month: numericPart(parts, "month"),
116
+ day: numericPart(parts, "day"),
117
+ hour: numericPart(parts, "hour"),
118
+ minute: numericPart(parts, "minute"),
119
+ second: numericPart(parts, "second")
120
+ }
121
+ }
122
+
123
+ const timeZoneOffsetMillis = (epochMillis: number, timeZone: string): number => {
124
+ const parts = localParts(epochMillis, timeZone)
125
+ const representedAsUtc = Date.UTC(
126
+ parts.year,
127
+ parts.month - 1,
128
+ parts.day,
129
+ parts.hour,
130
+ parts.minute,
131
+ parts.second
132
+ )
133
+ return representedAsUtc - Math.trunc(epochMillis / 1_000) * 1_000
134
+ }
135
+
136
+ const epochForLocalTime = (date: CalendarDate, time: WallClockTime, timeZone: string): number => {
137
+ const utcGuess = Date.UTC(date.year, date.month - 1, date.day, time.hour, time.minute)
138
+ const firstCandidate = utcGuess - timeZoneOffsetMillis(utcGuess, timeZone)
139
+ return utcGuess - timeZoneOffsetMillis(firstCandidate, timeZone)
140
+ }
141
+
142
+ const nextCalendarDate = (date: CalendarDate): CalendarDate => {
143
+ const next = new Date(Date.UTC(date.year, date.month - 1, date.day + 1))
144
+ return {
145
+ year: next.getUTCFullYear(),
146
+ month: next.getUTCMonth() + 1,
147
+ day: next.getUTCDate()
148
+ }
149
+ }
150
+
151
+ const wallClockReset = (
152
+ text: string,
153
+ pattern: RegExp,
154
+ now: DateTime.Utc,
155
+ timeZone: string
156
+ ): DateTime.Utc | undefined => {
157
+ const rawTime = text.match(pattern)?.[1]
158
+ const time = rawTime === undefined ? undefined : parseWallClockTime(rawTime)
159
+ if (time === undefined) {
160
+ return undefined
161
+ }
162
+
163
+ try {
164
+ const nowMillis = DateTime.toEpochMillis(now)
165
+ const localNow = localParts(nowMillis, timeZone)
166
+ const today: CalendarDate = {
167
+ year: localNow.year,
168
+ month: localNow.month,
169
+ day: localNow.day
170
+ }
171
+ const todayCandidate = epochForLocalTime(today, time, timeZone)
172
+ const resetMillis =
173
+ todayCandidate > nowMillis
174
+ ? todayCandidate
175
+ : epochForLocalTime(nextCalendarDate(today), time, timeZone)
176
+
177
+ return DateTime.makeUnsafe(resetMillis)
178
+ } catch {
179
+ return undefined
180
+ }
181
+ }
182
+
183
+ export const classifyUsageLimit = (
184
+ provider: string,
185
+ text: string,
186
+ now: DateTime.Utc,
187
+ timeZone: string
188
+ ): LlmError | undefined => {
189
+ switch (provider) {
190
+ case "codex": {
191
+ const resetAt = wallClockReset(text, codexAt, now, timeZone)
192
+ return resetAt === undefined
193
+ ? undefined
194
+ : UsageLimitError.make({
195
+ resetAt,
196
+ provider,
197
+ message: text
198
+ })
199
+ }
200
+ case "claude": {
201
+ const resetAt = wallClockReset(text, claudeAt, now, timeZone)
202
+ return resetAt === undefined
203
+ ? undefined
204
+ : UsageLimitError.make({
205
+ resetAt,
206
+ provider,
207
+ message: text
208
+ })
209
+ }
210
+ case "gemini": {
211
+ const resetSpecification = text.match(geminiReset)?.[1]
212
+ if (resetSpecification !== undefined) {
213
+ const duration = resetDuration(resetSpecification)
214
+ return Duration.toMillis(duration) <= Duration.toMillis(shortResetLimit)
215
+ ? RateLimitError.make({ retryAfter: duration })
216
+ : UsageLimitError.make({
217
+ resetAt: DateTime.addDuration(now, duration),
218
+ provider,
219
+ message: text
220
+ })
221
+ }
222
+
223
+ return isGeminiQuota(text)
224
+ ? UsageLimitError.make({
225
+ provider,
226
+ message: text
227
+ })
228
+ : undefined
229
+ }
230
+ case "grok":
231
+ case "cursor":
232
+ case "opencode":
233
+ return isGenericQuota(text)
234
+ ? UsageLimitError.make({
235
+ provider,
236
+ message: text
237
+ })
238
+ : undefined
239
+ default:
240
+ return undefined
241
+ }
242
+ }
@@ -0,0 +1,68 @@
1
+ import * as Effect from "effect/Effect"
2
+ import { DimensionScore, EvalResult } from "./Eval.ts"
3
+ import { makeEvaluator, type Evaluator } from "./Evaluator.ts"
4
+
5
+ const piiPatterns: ReadonlyArray<readonly [label: string, pattern: RegExp]> = [
6
+ ["email", /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/],
7
+ ["ssn", /\b\d{3}-\d{2}-\d{4}\b/],
8
+ ["credit-card", /\b(?:\d[ -]?){13,16}\b/],
9
+ ["phone", /\b\d{3}[-.\s]?\d{3}[-.\s]?\d{4}\b/],
10
+ ["ipv4", /\b(?:\d{1,3}\.){3}\d{1,3}\b/]
11
+ ]
12
+
13
+ const one = (name: string, ok: boolean, maxScore: number, why: string): EvalResult =>
14
+ EvalResult.make({
15
+ scores: [
16
+ DimensionScore.make({
17
+ name,
18
+ score: ok ? maxScore : 0,
19
+ reasoning: why
20
+ })
21
+ ]
22
+ })
23
+
24
+ export const noPii = (maxScore = 2): Evaluator<string> =>
25
+ makeEvaluator((input) => {
26
+ const hits = piiPatterns.flatMap(([label, pattern]) => (pattern.test(input) ? [label] : []))
27
+ return Effect.succeed(
28
+ one(
29
+ "no-pii",
30
+ hits.length === 0,
31
+ maxScore,
32
+ hits.length === 0 ? "no PII detected" : `matched: ${hits.join(", ")}`
33
+ )
34
+ )
35
+ })
36
+
37
+ export const validJson = (maxScore = 2): Evaluator<string> =>
38
+ makeEvaluator((input) => {
39
+ let valid = true
40
+ try {
41
+ JSON.parse(input)
42
+ } catch {
43
+ valid = false
44
+ }
45
+ return Effect.succeed(
46
+ one("valid-json", valid, maxScore, valid ? "parses as JSON" : "not valid JSON")
47
+ )
48
+ })
49
+
50
+ export const matches = (regex: string, name = "format", maxScore = 2): Evaluator<string> =>
51
+ makeEvaluator((input) => {
52
+ const ok = new RegExp(`^(?:${regex})$`).test(input)
53
+ return Effect.succeed(
54
+ one(name, ok, maxScore, ok ? `matches /${regex}/` : `does not match /${regex}/`)
55
+ )
56
+ })
57
+
58
+ export const lengthBetween = (min: number, max: number, maxScore = 2): Evaluator<string> =>
59
+ makeEvaluator((input) =>
60
+ Effect.succeed(
61
+ one(
62
+ "length",
63
+ input.length >= min && input.length <= max,
64
+ maxScore,
65
+ `length ${input.length}; bounds [${min}, ${max}]`
66
+ )
67
+ )
68
+ )
@@ -0,0 +1,112 @@
1
+ import * as Effect from "effect/Effect"
2
+ import * as Schema from "effect/Schema"
3
+ import type { LlmError } from "../Errors.ts"
4
+ import type { Evaluator } from "./Evaluator.ts"
5
+
6
+ export class Dimension extends Schema.Class<Dimension>("Dimension")({
7
+ name: Schema.String,
8
+ rubric: Schema.String,
9
+ maxScore: Schema.Int.pipe(Schema.withConstructorDefault(Effect.succeed(2)))
10
+ }) {}
11
+
12
+ export class DimensionScore extends Schema.Class<DimensionScore>("DimensionScore")({
13
+ name: Schema.String,
14
+ score: Schema.Int,
15
+ reasoning: Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed("")))
16
+ }) {}
17
+
18
+ export class EvalResult extends Schema.Class<EvalResult>("EvalResult")({
19
+ scores: Schema.Array(DimensionScore),
20
+ summary: Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed("")))
21
+ }) {
22
+ score(name: string): number | undefined {
23
+ return this.scores.find((item) => item.name === name)?.score
24
+ }
25
+
26
+ get total(): number {
27
+ return this.scores.reduce((sum, item) => sum + item.score, 0)
28
+ }
29
+
30
+ meets(minPerDimension: number): boolean {
31
+ return this.scores.every((item) => item.score >= minPerDimension)
32
+ }
33
+ }
34
+
35
+ export class Sample extends Schema.Class<Sample>("Sample")({
36
+ response: Schema.String,
37
+ query: Schema.optionalKey(Schema.String),
38
+ context: Schema.optionalKey(Schema.String),
39
+ expected: Schema.optionalKey(Schema.String)
40
+ }) {}
41
+
42
+ export class DimensionStats extends Schema.Class<DimensionStats>("DimensionStats")({
43
+ name: Schema.String,
44
+ median: Schema.Int,
45
+ min: Schema.Int,
46
+ max: Schema.Int
47
+ }) {
48
+ get spread(): number {
49
+ return this.max - this.min
50
+ }
51
+
52
+ isFlaky(threshold: number): boolean {
53
+ return this.spread > threshold
54
+ }
55
+ }
56
+
57
+ export class RepeatedEval extends Schema.Class<RepeatedEval>("RepeatedEval")({
58
+ runs: Schema.Array(EvalResult),
59
+ stats: Schema.Array(DimensionStats),
60
+ spreadThreshold: Schema.Int
61
+ }) {
62
+ get flakyDimensions(): ReadonlyArray<string> {
63
+ return this.stats.filter((stat) => stat.isFlaky(this.spreadThreshold)).map((stat) => stat.name)
64
+ }
65
+
66
+ get isFlaky(): boolean {
67
+ return this.flakyDimensions.length > 0
68
+ }
69
+
70
+ get aggregate(): EvalResult {
71
+ return EvalResult.make({
72
+ scores: this.stats.map((stat) =>
73
+ DimensionScore.make({
74
+ name: stat.name,
75
+ score: stat.median,
76
+ reasoning: `median of ${this.runs.length} runs`
77
+ })
78
+ ),
79
+ summary: `aggregate of ${this.runs.length} runs`
80
+ })
81
+ }
82
+ }
83
+
84
+ const median = (sorted: ReadonlyArray<number>): number =>
85
+ sorted.length === 0 ? 0 : (sorted[Math.floor((sorted.length - 1) / 2)] ?? 0)
86
+
87
+ export const repeat = Effect.fn("@llm4ts/core/eval/Eval.repeat")(function* <In>(
88
+ evaluator: Evaluator<In>,
89
+ input: In,
90
+ n = 3,
91
+ spreadThreshold = 1
92
+ ): Effect.fn.Return<RepeatedEval, LlmError> {
93
+ const runs = yield* Effect.forEach(Array.from({ length: Math.max(0, n) }), () =>
94
+ evaluator.evaluate(input)
95
+ )
96
+ const names = [...new Set(runs.flatMap((run) => run.scores.map((score) => score.name)))]
97
+ const stats = names.map((name) => {
98
+ const values = runs
99
+ .flatMap((run) => {
100
+ const score = run.score(name)
101
+ return score === undefined ? [] : [score]
102
+ })
103
+ .sort((left, right) => left - right)
104
+ return DimensionStats.make({
105
+ name,
106
+ median: median(values),
107
+ min: values.at(0) ?? 0,
108
+ max: values.at(-1) ?? 0
109
+ })
110
+ })
111
+ return RepeatedEval.make({ runs, stats, spreadThreshold })
112
+ })
@@ -0,0 +1,63 @@
1
+ import * as Effect from "effect/Effect"
2
+ import type { LlmError } from "../Errors.ts"
3
+ import { repeat } from "./Eval.ts"
4
+ import type { EvalResult, RepeatedEval } from "./Eval.ts"
5
+ import type { Evaluator } from "./Evaluator.ts"
6
+
7
+ export interface EvalCase<In> {
8
+ readonly name: string
9
+ readonly input: In
10
+ readonly minPerDimension?: number
11
+ }
12
+
13
+ export interface CaseReport {
14
+ readonly name: string
15
+ readonly result: EvalResult
16
+ readonly repeated: RepeatedEval | undefined
17
+ readonly passed: boolean
18
+ }
19
+
20
+ export interface SuiteReport {
21
+ readonly cases: ReadonlyArray<CaseReport>
22
+ readonly passed: boolean
23
+ readonly failures: ReadonlyArray<CaseReport>
24
+ }
25
+
26
+ export const run = Effect.fn("@llm4ts/core/eval/EvalSuite.run")(function* <In>(
27
+ evaluator: Evaluator<In>,
28
+ cases: ReadonlyArray<EvalCase<In>>,
29
+ repeats = 1,
30
+ spreadThreshold = 1
31
+ ): Effect.fn.Return<SuiteReport, LlmError> {
32
+ const reports = yield* Effect.forEach(cases, (testCase) => {
33
+ const minimum = testCase.minPerDimension ?? 1
34
+ if (repeats <= 1) {
35
+ return evaluator.evaluate(testCase.input).pipe(
36
+ Effect.map(
37
+ (result): CaseReport => ({
38
+ name: testCase.name,
39
+ result,
40
+ repeated: undefined,
41
+ passed: result.meets(minimum)
42
+ })
43
+ )
44
+ )
45
+ }
46
+ return repeat(evaluator, testCase.input, repeats, spreadThreshold).pipe(
47
+ Effect.map((repeated): CaseReport => {
48
+ const result = repeated.aggregate
49
+ return {
50
+ name: testCase.name,
51
+ result,
52
+ repeated,
53
+ passed: result.meets(minimum) && !repeated.isFlaky
54
+ }
55
+ })
56
+ )
57
+ })
58
+ return {
59
+ cases: reports,
60
+ passed: reports.every((report) => report.passed),
61
+ failures: reports.filter((report) => !report.passed)
62
+ }
63
+ })
@@ -0,0 +1,31 @@
1
+ import * as Effect from "effect/Effect"
2
+ import type { LlmError } from "../Errors.ts"
3
+ import { EvalResult } from "./Eval.ts"
4
+
5
+ export interface Evaluator<in In> {
6
+ readonly evaluate: (input: In) => Effect.Effect<EvalResult, LlmError>
7
+ }
8
+
9
+ export const makeEvaluator = <In>(evaluate: Evaluator<In>["evaluate"]): Evaluator<In> => ({
10
+ evaluate
11
+ })
12
+
13
+ export const contramap = <In, In2>(
14
+ evaluator: Evaluator<In>,
15
+ project: (input: In2) => In
16
+ ): Evaluator<In2> => makeEvaluator((input) => evaluator.evaluate(project(input)))
17
+
18
+ export const all = <In>(...evaluators: ReadonlyArray<Evaluator<In>>): Evaluator<In> =>
19
+ makeEvaluator((input) =>
20
+ Effect.forEach(evaluators, (evaluator) => evaluator.evaluate(input)).pipe(
21
+ Effect.map((results) =>
22
+ EvalResult.make({
23
+ scores: results.flatMap((result) => result.scores),
24
+ summary: results
25
+ .map((result) => result.summary)
26
+ .filter((summary) => summary.length > 0)
27
+ .join("; ")
28
+ })
29
+ )
30
+ )
31
+ )
@@ -0,0 +1,92 @@
1
+ import * as Effect from "effect/Effect"
2
+ import * as Schema from "effect/Schema"
3
+ import type { LlmServiceShape } from "../LlmService.ts"
4
+ import type { JsonSchema } from "../Models.ts"
5
+ import { DimensionScore, EvalResult } from "./Eval.ts"
6
+ import type { Dimension, Sample } from "./Eval.ts"
7
+ import { makeEvaluator, type Evaluator } from "./Evaluator.ts"
8
+
9
+ export class JudgeResponse extends Schema.Class<JudgeResponse>("JudgeResponse")({
10
+ scores: Schema.Array(DimensionScore)
11
+ }) {}
12
+
13
+ export const defaultSystem =
14
+ "You are an impartial evaluator. Score each dimension strictly on its rubric; when unsure, score lower. " +
15
+ "Return ONLY valid JSON, no prose."
16
+
17
+ const judgeJsonSchema: JsonSchema = {
18
+ type: "object",
19
+ properties: {
20
+ scores: {
21
+ type: "array",
22
+ items: {
23
+ type: "object",
24
+ properties: {
25
+ name: { type: "string" },
26
+ score: { type: "integer" },
27
+ reasoning: { type: "string" }
28
+ },
29
+ required: ["name", "score"]
30
+ }
31
+ }
32
+ },
33
+ required: ["scores"],
34
+ additionalProperties: false
35
+ }
36
+
37
+ export const buildPrompt = (
38
+ system: string,
39
+ dimensions: ReadonlyArray<Dimension>,
40
+ sample: Sample
41
+ ): string => {
42
+ const rubric = dimensions
43
+ .map((dimension) => `- ${dimension.name} (0..${dimension.maxScore}): ${dimension.rubric}`)
44
+ .join("\n")
45
+ const user = [
46
+ sample.query === undefined ? undefined : `Query: ${sample.query}`,
47
+ sample.context === undefined ? undefined : `Context: ${sample.context}`,
48
+ `Response: ${sample.response}`,
49
+ sample.expected === undefined ? undefined : `Expected: ${sample.expected}`
50
+ ]
51
+ .filter((line): line is string => line !== undefined)
52
+ .join("\n")
53
+ return `${system}
54
+
55
+ Score each dimension on its 0..max scale per the rubric:
56
+ ${rubric}
57
+
58
+ Return ONLY this JSON: {"scores":[{"name":"<dimension>","score":<int>,"reasoning":"<one sentence>"}]}
59
+
60
+ ${user}`
61
+ }
62
+
63
+ const toResult = (dimensions: ReadonlyArray<Dimension>, response: JudgeResponse): EvalResult => {
64
+ const byName = new Map(response.scores.map((score) => [score.name, score]))
65
+ return EvalResult.make({
66
+ scores: dimensions.map((dimension) => {
67
+ const score = byName.get(dimension.name)
68
+ return score === undefined
69
+ ? DimensionScore.make({
70
+ name: dimension.name,
71
+ score: 0,
72
+ reasoning: "missing"
73
+ })
74
+ : DimensionScore.make({
75
+ name: dimension.name,
76
+ score: Math.max(0, Math.min(dimension.maxScore, score.score)),
77
+ reasoning: score.reasoning
78
+ })
79
+ })
80
+ })
81
+ }
82
+
83
+ export const judge = (
84
+ llm: LlmServiceShape,
85
+ dimensions: ReadonlyArray<Dimension>,
86
+ system = defaultSystem
87
+ ): Evaluator<Sample> =>
88
+ makeEvaluator((sample) =>
89
+ llm
90
+ .executeStructured(buildPrompt(system, dimensions, sample), JudgeResponse, judgeJsonSchema)
91
+ .pipe(Effect.map((response) => toResult(dimensions, response)))
92
+ )