@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.
- package/LICENSE +21 -0
- package/README.md +47 -0
- package/dist/AgentSession.d.ts +66 -0
- package/dist/AgentSession.d.ts.map +1 -0
- package/dist/AgentSession.js +53 -0
- package/dist/AgentSession.js.map +1 -0
- package/dist/Capability.d.ts +101 -0
- package/dist/Capability.d.ts.map +1 -0
- package/dist/Capability.js +210 -0
- package/dist/Capability.js.map +1 -0
- package/dist/Connector.d.ts +69 -0
- package/dist/Connector.d.ts.map +1 -0
- package/dist/Connector.js +86 -0
- package/dist/Connector.js.map +1 -0
- package/dist/ConnectorConfig.d.ts +70 -0
- package/dist/ConnectorConfig.d.ts.map +1 -0
- package/dist/ConnectorConfig.js +110 -0
- package/dist/ConnectorConfig.js.map +1 -0
- package/dist/ConnectorRegistry.d.ts +28 -0
- package/dist/ConnectorRegistry.d.ts.map +1 -0
- package/dist/ConnectorRegistry.js +87 -0
- package/dist/ConnectorRegistry.js.map +1 -0
- package/dist/ContextManagement.d.ts +78 -0
- package/dist/ContextManagement.d.ts.map +1 -0
- package/dist/ContextManagement.js +221 -0
- package/dist/ContextManagement.js.map +1 -0
- package/dist/Conversation.d.ts +18 -0
- package/dist/Conversation.d.ts.map +1 -0
- package/dist/Conversation.js +17 -0
- package/dist/Conversation.js.map +1 -0
- package/dist/Errors.d.ts +64 -0
- package/dist/Errors.d.ts.map +1 -0
- package/dist/Errors.js +76 -0
- package/dist/Errors.js.map +1 -0
- package/dist/HttpClient.d.ts +43 -0
- package/dist/HttpClient.d.ts.map +1 -0
- package/dist/HttpClient.js +99 -0
- package/dist/HttpClient.js.map +1 -0
- package/dist/LlmService.d.ts +24 -0
- package/dist/LlmService.d.ts.map +1 -0
- package/dist/LlmService.js +4 -0
- package/dist/LlmService.js.map +1 -0
- package/dist/Models.d.ts +138 -0
- package/dist/Models.d.ts.map +1 -0
- package/dist/Models.js +209 -0
- package/dist/Models.js.map +1 -0
- package/dist/ProcessExecutor.d.ts +57 -0
- package/dist/ProcessExecutor.d.ts.map +1 -0
- package/dist/ProcessExecutor.js +71 -0
- package/dist/ProcessExecutor.js.map +1 -0
- package/dist/RateLimiter.d.ts +44 -0
- package/dist/RateLimiter.d.ts.map +1 -0
- package/dist/RateLimiter.js +155 -0
- package/dist/RateLimiter.js.map +1 -0
- package/dist/Streaming.d.ts +17 -0
- package/dist/Streaming.d.ts.map +1 -0
- package/dist/Streaming.js +65 -0
- package/dist/Streaming.js.map +1 -0
- package/dist/StructuredOutput.d.ts +10 -0
- package/dist/StructuredOutput.d.ts.map +1 -0
- package/dist/StructuredOutput.js +97 -0
- package/dist/StructuredOutput.js.map +1 -0
- package/dist/TemporaryFiles.d.ts +20 -0
- package/dist/TemporaryFiles.d.ts.map +1 -0
- package/dist/TemporaryFiles.js +18 -0
- package/dist/TemporaryFiles.js.map +1 -0
- package/dist/UsageLimits.d.ts +5 -0
- package/dist/UsageLimits.d.ts.map +1 -0
- package/dist/UsageLimits.js +187 -0
- package/dist/UsageLimits.js.map +1 -0
- package/dist/eval/Checks.d.ts +6 -0
- package/dist/eval/Checks.d.ts.map +1 -0
- package/dist/eval/Checks.js +39 -0
- package/dist/eval/Checks.js.map +1 -0
- package/dist/eval/Eval.d.ts +57 -0
- package/dist/eval/Eval.d.ts.map +1 -0
- package/dist/eval/Eval.js +91 -0
- package/dist/eval/Eval.js.map +1 -0
- package/dist/eval/EvalSuite.d.ts +21 -0
- package/dist/eval/EvalSuite.d.ts.map +1 -0
- package/dist/eval/EvalSuite.js +30 -0
- package/dist/eval/EvalSuite.js.map +1 -0
- package/dist/eval/Evaluator.d.ts +10 -0
- package/dist/eval/Evaluator.d.ts.map +1 -0
- package/dist/eval/Evaluator.js +14 -0
- package/dist/eval/Evaluator.js.map +1 -0
- package/dist/eval/Judge.d.ts +15 -0
- package/dist/eval/Judge.d.ts.map +1 -0
- package/dist/eval/Judge.js +73 -0
- package/dist/eval/Judge.js.map +1 -0
- package/dist/observability/MeteredLlmService.d.ts +16 -0
- package/dist/observability/MeteredLlmService.d.ts.map +1 -0
- package/dist/observability/MeteredLlmService.js +66 -0
- package/dist/observability/MeteredLlmService.js.map +1 -0
- package/dist/observability/Metrics.d.ts +71 -0
- package/dist/observability/Metrics.d.ts.map +1 -0
- package/dist/observability/Metrics.js +147 -0
- package/dist/observability/Metrics.js.map +1 -0
- package/dist/observability/Redaction.d.ts +8 -0
- package/dist/observability/Redaction.d.ts.map +1 -0
- package/dist/observability/Redaction.js +38 -0
- package/dist/observability/Redaction.js.map +1 -0
- package/dist/observability/StreamRecorder.d.ts +9 -0
- package/dist/observability/StreamRecorder.d.ts.map +1 -0
- package/dist/observability/StreamRecorder.js +10 -0
- package/dist/observability/StreamRecorder.js.map +1 -0
- package/dist/observability/StructuredLogger.d.ts +31 -0
- package/dist/observability/StructuredLogger.d.ts.map +1 -0
- package/dist/observability/StructuredLogger.js +34 -0
- package/dist/observability/StructuredLogger.js.map +1 -0
- package/dist/observability/Tracing.d.ts +38 -0
- package/dist/observability/Tracing.d.ts.map +1 -0
- package/dist/observability/Tracing.js +75 -0
- package/dist/observability/Tracing.js.map +1 -0
- package/dist/providers/AnthropicModels.d.ts +95 -0
- package/dist/providers/AnthropicModels.d.ts.map +1 -0
- package/dist/providers/AnthropicModels.js +83 -0
- package/dist/providers/AnthropicModels.js.map +1 -0
- package/dist/providers/AnthropicProvider.d.ts +13 -0
- package/dist/providers/AnthropicProvider.d.ts.map +1 -0
- package/dist/providers/AnthropicProvider.js +177 -0
- package/dist/providers/AnthropicProvider.js.map +1 -0
- package/dist/providers/AntigravityConnector.d.ts +7 -0
- package/dist/providers/AntigravityConnector.d.ts.map +1 -0
- package/dist/providers/AntigravityConnector.js +52 -0
- package/dist/providers/AntigravityConnector.js.map +1 -0
- package/dist/providers/ClaudeAgentSession.d.ts +11 -0
- package/dist/providers/ClaudeAgentSession.d.ts.map +1 -0
- package/dist/providers/ClaudeAgentSession.js +158 -0
- package/dist/providers/ClaudeAgentSession.js.map +1 -0
- package/dist/providers/ClaudeCliConnector.d.ts +9 -0
- package/dist/providers/ClaudeCliConnector.d.ts.map +1 -0
- package/dist/providers/ClaudeCliConnector.js +123 -0
- package/dist/providers/ClaudeCliConnector.js.map +1 -0
- package/dist/providers/CliSupport.d.ts +21 -0
- package/dist/providers/CliSupport.d.ts.map +1 -0
- package/dist/providers/CliSupport.js +62 -0
- package/dist/providers/CliSupport.js.map +1 -0
- package/dist/providers/CodexConnector.d.ts +12 -0
- package/dist/providers/CodexConnector.d.ts.map +1 -0
- package/dist/providers/CodexConnector.js +175 -0
- package/dist/providers/CodexConnector.js.map +1 -0
- package/dist/providers/ConnectorFactories.d.ts +15 -0
- package/dist/providers/ConnectorFactories.d.ts.map +1 -0
- package/dist/providers/ConnectorFactories.js +74 -0
- package/dist/providers/ConnectorFactories.js.map +1 -0
- package/dist/providers/CopilotConnector.d.ts +6 -0
- package/dist/providers/CopilotConnector.d.ts.map +1 -0
- package/dist/providers/CopilotConnector.js +35 -0
- package/dist/providers/CopilotConnector.js.map +1 -0
- package/dist/providers/CursorConnector.d.ts +8 -0
- package/dist/providers/CursorConnector.d.ts.map +1 -0
- package/dist/providers/CursorConnector.js +82 -0
- package/dist/providers/CursorConnector.js.map +1 -0
- package/dist/providers/GeminiApiProvider.d.ts +9 -0
- package/dist/providers/GeminiApiProvider.d.ts.map +1 -0
- package/dist/providers/GeminiApiProvider.js +181 -0
- package/dist/providers/GeminiApiProvider.js.map +1 -0
- package/dist/providers/GeminiCliProvider.d.ts +106 -0
- package/dist/providers/GeminiCliProvider.d.ts.map +1 -0
- package/dist/providers/GeminiCliProvider.js +596 -0
- package/dist/providers/GeminiCliProvider.js.map +1 -0
- package/dist/providers/GeminiModels.d.ts +92 -0
- package/dist/providers/GeminiModels.d.ts.map +1 -0
- package/dist/providers/GeminiModels.js +80 -0
- package/dist/providers/GeminiModels.js.map +1 -0
- package/dist/providers/GrokCliConnector.d.ts +8 -0
- package/dist/providers/GrokCliConnector.d.ts.map +1 -0
- package/dist/providers/GrokCliConnector.js +82 -0
- package/dist/providers/GrokCliConnector.js.map +1 -0
- package/dist/providers/LmStudioModels.d.ts +114 -0
- package/dist/providers/LmStudioModels.d.ts.map +1 -0
- package/dist/providers/LmStudioModels.js +102 -0
- package/dist/providers/LmStudioModels.js.map +1 -0
- package/dist/providers/LmStudioProvider.d.ts +13 -0
- package/dist/providers/LmStudioProvider.d.ts.map +1 -0
- package/dist/providers/LmStudioProvider.js +158 -0
- package/dist/providers/LmStudioProvider.js.map +1 -0
- package/dist/providers/MockProvider.d.ts +6 -0
- package/dist/providers/MockProvider.d.ts.map +1 -0
- package/dist/providers/MockProvider.js +271 -0
- package/dist/providers/MockProvider.js.map +1 -0
- package/dist/providers/OllamaModels.d.ts +68 -0
- package/dist/providers/OllamaModels.d.ts.map +1 -0
- package/dist/providers/OllamaModels.js +59 -0
- package/dist/providers/OllamaModels.js.map +1 -0
- package/dist/providers/OllamaProvider.d.ts +7 -0
- package/dist/providers/OllamaProvider.d.ts.map +1 -0
- package/dist/providers/OllamaProvider.js +134 -0
- package/dist/providers/OllamaProvider.js.map +1 -0
- package/dist/providers/OpenAIModels.d.ts +139 -0
- package/dist/providers/OpenAIModels.d.ts.map +1 -0
- package/dist/providers/OpenAIModels.js +123 -0
- package/dist/providers/OpenAIModels.js.map +1 -0
- package/dist/providers/OpenAIProvider.d.ts +9 -0
- package/dist/providers/OpenAIProvider.d.ts.map +1 -0
- package/dist/providers/OpenAIProvider.js +185 -0
- package/dist/providers/OpenAIProvider.js.map +1 -0
- package/dist/providers/OpenCodeCliConnector.d.ts +8 -0
- package/dist/providers/OpenCodeCliConnector.d.ts.map +1 -0
- package/dist/providers/OpenCodeCliConnector.js +93 -0
- package/dist/providers/OpenCodeCliConnector.js.map +1 -0
- package/dist/providers/OpenCodeModels.d.ts +61 -0
- package/dist/providers/OpenCodeModels.d.ts.map +1 -0
- package/dist/providers/OpenCodeModels.js +53 -0
- package/dist/providers/OpenCodeModels.js.map +1 -0
- package/dist/providers/OpenCodeProvider.d.ts +10 -0
- package/dist/providers/OpenCodeProvider.d.ts.map +1 -0
- package/dist/providers/OpenCodeProvider.js +154 -0
- package/dist/providers/OpenCodeProvider.js.map +1 -0
- package/dist/providers/PiConnector.d.ts +8 -0
- package/dist/providers/PiConnector.d.ts.map +1 -0
- package/dist/providers/PiConnector.js +99 -0
- package/dist/providers/PiConnector.js.map +1 -0
- package/dist/tools/Tool.d.ts +83 -0
- package/dist/tools/Tool.d.ts.map +1 -0
- package/dist/tools/Tool.js +278 -0
- package/dist/tools/Tool.js.map +1 -0
- package/dist/tools/ToolCallingExecutor.d.ts +18 -0
- package/dist/tools/ToolCallingExecutor.d.ts.map +1 -0
- package/dist/tools/ToolCallingExecutor.js +64 -0
- package/dist/tools/ToolCallingExecutor.js.map +1 -0
- package/dist/tools/ToolRegistry.d.ts +45 -0
- package/dist/tools/ToolRegistry.d.ts.map +1 -0
- package/dist/tools/ToolRegistry.js +133 -0
- package/dist/tools/ToolRegistry.js.map +1 -0
- package/package.json +105 -0
- package/src/.gitkeep +1 -0
- package/src/AgentSession.ts +69 -0
- package/src/Capability.ts +240 -0
- package/src/Connector.ts +220 -0
- package/src/ConnectorConfig.ts +146 -0
- package/src/ConnectorRegistry.ts +184 -0
- package/src/ContextManagement.ts +372 -0
- package/src/Conversation.ts +21 -0
- package/src/Errors.ts +87 -0
- package/src/HttpClient.ts +202 -0
- package/src/LlmService.ts +45 -0
- package/src/Models.ts +250 -0
- package/src/ProcessExecutor.ts +157 -0
- package/src/RateLimiter.ts +238 -0
- package/src/Streaming.ts +164 -0
- package/src/StructuredOutput.ts +138 -0
- package/src/TemporaryFiles.ts +51 -0
- package/src/UsageLimits.ts +242 -0
- package/src/eval/Checks.ts +68 -0
- package/src/eval/Eval.ts +112 -0
- package/src/eval/EvalSuite.ts +63 -0
- package/src/eval/Evaluator.ts +31 -0
- package/src/eval/Judge.ts +92 -0
- package/src/observability/MeteredLlmService.ts +156 -0
- package/src/observability/Metrics.ts +195 -0
- package/src/observability/Redaction.ts +55 -0
- package/src/observability/StreamRecorder.ts +32 -0
- package/src/observability/StructuredLogger.ts +62 -0
- package/src/observability/Tracing.ts +141 -0
- package/src/providers/AnthropicModels.ts +96 -0
- package/src/providers/AnthropicProvider.ts +320 -0
- package/src/providers/AntigravityConnector.ts +77 -0
- package/src/providers/ClaudeAgentSession.ts +226 -0
- package/src/providers/ClaudeCliConnector.ts +201 -0
- package/src/providers/CliSupport.ts +100 -0
- package/src/providers/CodexConnector.ts +251 -0
- package/src/providers/ConnectorFactories.ts +142 -0
- package/src/providers/CopilotConnector.ts +47 -0
- package/src/providers/CursorConnector.ts +120 -0
- package/src/providers/GeminiApiProvider.ts +337 -0
- package/src/providers/GeminiCliProvider.ts +793 -0
- package/src/providers/GeminiModels.ts +97 -0
- package/src/providers/GrokCliConnector.ts +117 -0
- package/src/providers/LmStudioModels.ts +117 -0
- package/src/providers/LmStudioProvider.ts +260 -0
- package/src/providers/MockProvider.ts +322 -0
- package/src/providers/OllamaModels.ts +64 -0
- package/src/providers/OllamaProvider.ts +236 -0
- package/src/providers/OpenAIModels.ts +152 -0
- package/src/providers/OpenAIProvider.ts +331 -0
- package/src/providers/OpenCodeCliConnector.ts +135 -0
- package/src/providers/OpenCodeModels.ts +60 -0
- package/src/providers/OpenCodeProvider.ts +278 -0
- package/src/providers/PiConnector.ts +142 -0
- package/src/tools/Tool.ts +379 -0
- package/src/tools/ToolCallingExecutor.ts +106 -0
- package/src/tools/ToolRegistry.ts +224 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import * as Context from "effect/Context"
|
|
2
|
+
import * as Effect from "effect/Effect"
|
|
3
|
+
import type * as Queue from "effect/Queue"
|
|
4
|
+
import * as Ref from "effect/Ref"
|
|
5
|
+
import * as Schema from "effect/Schema"
|
|
6
|
+
import type * as Scope from "effect/Scope"
|
|
7
|
+
import * as Stream from "effect/Stream"
|
|
8
|
+
import { InvalidRequestError, ProviderError, type LlmError } from "./Errors.ts"
|
|
9
|
+
|
|
10
|
+
const emptyLines: ReadonlyArray<string> = Object.freeze([])
|
|
11
|
+
const emptyEnvironment: Readonly<Record<string, string>> = Object.freeze({})
|
|
12
|
+
|
|
13
|
+
export class ProcessResult extends Schema.Class<ProcessResult>("ProcessResult")({
|
|
14
|
+
stdout: Schema.Array(Schema.String),
|
|
15
|
+
exitCode: Schema.Int,
|
|
16
|
+
stderr: Schema.Array(Schema.String).pipe(
|
|
17
|
+
Schema.withConstructorDefault(Effect.succeed(emptyLines))
|
|
18
|
+
)
|
|
19
|
+
}) {}
|
|
20
|
+
|
|
21
|
+
export class ProcessInvocation extends Schema.Class<ProcessInvocation>("ProcessInvocation")({
|
|
22
|
+
argv: Schema.Array(Schema.String),
|
|
23
|
+
cwd: Schema.String,
|
|
24
|
+
envVars: Schema.Record(Schema.String, Schema.String).pipe(
|
|
25
|
+
Schema.withConstructorDefault(Effect.succeed(emptyEnvironment))
|
|
26
|
+
),
|
|
27
|
+
stdin: Schema.optionalKey(Schema.String),
|
|
28
|
+
streaming: Schema.Boolean
|
|
29
|
+
}) {}
|
|
30
|
+
|
|
31
|
+
export type BidirectionalProcess = readonly [
|
|
32
|
+
stdin: Queue.Queue<string>,
|
|
33
|
+
stdout: Stream.Stream<string, LlmError>
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
export interface ProcessExecutorShape {
|
|
37
|
+
readonly run: (
|
|
38
|
+
argv: ReadonlyArray<string>,
|
|
39
|
+
cwd: string,
|
|
40
|
+
envVars: Readonly<Record<string, string>>
|
|
41
|
+
) => Effect.Effect<ProcessResult, LlmError>
|
|
42
|
+
readonly runStreaming: (
|
|
43
|
+
argv: ReadonlyArray<string>,
|
|
44
|
+
cwd: string,
|
|
45
|
+
envVars: Readonly<Record<string, string>>
|
|
46
|
+
) => Stream.Stream<string, LlmError>
|
|
47
|
+
readonly runWithStdin: (
|
|
48
|
+
argv: ReadonlyArray<string>,
|
|
49
|
+
cwd: string,
|
|
50
|
+
envVars: Readonly<Record<string, string>>,
|
|
51
|
+
stdin: string
|
|
52
|
+
) => Effect.Effect<ProcessResult, LlmError>
|
|
53
|
+
readonly runStreamingWithStdin: (
|
|
54
|
+
argv: ReadonlyArray<string>,
|
|
55
|
+
cwd: string,
|
|
56
|
+
envVars: Readonly<Record<string, string>>,
|
|
57
|
+
stdin: string
|
|
58
|
+
) => Stream.Stream<string, LlmError>
|
|
59
|
+
readonly runBidirectional: (
|
|
60
|
+
argv: ReadonlyArray<string>,
|
|
61
|
+
cwd: string,
|
|
62
|
+
envVars: Readonly<Record<string, string>>
|
|
63
|
+
) => Effect.Effect<BidirectionalProcess, LlmError, Scope.Scope>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class ProcessExecutor extends Context.Service<ProcessExecutor, ProcessExecutorShape>()(
|
|
67
|
+
"@llm4ts/core/ProcessExecutor"
|
|
68
|
+
) {}
|
|
69
|
+
|
|
70
|
+
export interface ProcessExecutorPrimitives {
|
|
71
|
+
readonly run: ProcessExecutorShape["run"]
|
|
72
|
+
readonly runStreaming: ProcessExecutorShape["runStreaming"]
|
|
73
|
+
readonly runWithStdin?: ProcessExecutorShape["runWithStdin"]
|
|
74
|
+
readonly runStreamingWithStdin?: ProcessExecutorShape["runStreamingWithStdin"]
|
|
75
|
+
readonly runBidirectional?: ProcessExecutorShape["runBidirectional"]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const makeProcessExecutor = (
|
|
79
|
+
primitives: ProcessExecutorPrimitives
|
|
80
|
+
): ProcessExecutorShape => ({
|
|
81
|
+
run: primitives.run,
|
|
82
|
+
runStreaming: primitives.runStreaming,
|
|
83
|
+
runWithStdin:
|
|
84
|
+
primitives.runWithStdin ?? ((argv, cwd, envVars, _stdin) => primitives.run(argv, cwd, envVars)),
|
|
85
|
+
runStreamingWithStdin:
|
|
86
|
+
primitives.runStreamingWithStdin ??
|
|
87
|
+
((argv, cwd, envVars, _stdin) => primitives.runStreaming(argv, cwd, envVars)),
|
|
88
|
+
runBidirectional:
|
|
89
|
+
primitives.runBidirectional ??
|
|
90
|
+
((argv, _cwd, _envVars) =>
|
|
91
|
+
Effect.fail(
|
|
92
|
+
InvalidRequestError.make({
|
|
93
|
+
message: `${argv[0] ?? "process"} does not support bidirectional sessions`
|
|
94
|
+
})
|
|
95
|
+
))
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
export const processCommandKey = (argv: ReadonlyArray<string>): string => JSON.stringify(argv)
|
|
99
|
+
|
|
100
|
+
export interface FakeProcessExecutor {
|
|
101
|
+
readonly executor: ProcessExecutorShape
|
|
102
|
+
readonly recorded: Effect.Effect<ReadonlyArray<ProcessInvocation>>
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface FakeProcessPlan {
|
|
106
|
+
readonly responses?: ReadonlyMap<string, ProcessResult>
|
|
107
|
+
readonly streamResponses?: ReadonlyMap<string, ReadonlyArray<string>>
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const makeFakeProcessExecutor = Effect.fn("@llm4ts/core/ProcessExecutor.makeFake")(
|
|
111
|
+
function* (plan: FakeProcessPlan = {}): Effect.fn.Return<FakeProcessExecutor> {
|
|
112
|
+
const invocations = yield* Ref.make<ReadonlyArray<ProcessInvocation>>([])
|
|
113
|
+
const record = (invocation: ProcessInvocation): Effect.Effect<void> =>
|
|
114
|
+
Ref.update(invocations, (current) => [...current, invocation])
|
|
115
|
+
|
|
116
|
+
const executor = makeProcessExecutor({
|
|
117
|
+
run: (argv, cwd, envVars) => {
|
|
118
|
+
const invocation = ProcessInvocation.make({
|
|
119
|
+
argv,
|
|
120
|
+
cwd,
|
|
121
|
+
envVars,
|
|
122
|
+
streaming: false
|
|
123
|
+
})
|
|
124
|
+
const response = plan.responses?.get(processCommandKey(argv))
|
|
125
|
+
return record(invocation).pipe(
|
|
126
|
+
Effect.andThen(
|
|
127
|
+
response === undefined
|
|
128
|
+
? Effect.fail(
|
|
129
|
+
ProviderError.make({
|
|
130
|
+
message: `No fake response for argv: ${argv.join(" ")}`
|
|
131
|
+
})
|
|
132
|
+
)
|
|
133
|
+
: Effect.succeed(response)
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
},
|
|
137
|
+
runStreaming: (argv, cwd, envVars) => {
|
|
138
|
+
const invocation = ProcessInvocation.make({
|
|
139
|
+
argv,
|
|
140
|
+
cwd,
|
|
141
|
+
envVars,
|
|
142
|
+
streaming: true
|
|
143
|
+
})
|
|
144
|
+
const lines = plan.streamResponses?.get(processCommandKey(argv)) ?? emptyLines
|
|
145
|
+
return Stream.concat(
|
|
146
|
+
Stream.drain(Stream.fromEffect(record(invocation))),
|
|
147
|
+
Stream.fromIterable(lines)
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
executor,
|
|
154
|
+
recorded: Ref.get(invocations)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
)
|
|
@@ -0,0 +1,238 @@
|
|
|
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.TaggedErrorClass<RateLimiterAcquireTimeout>()(
|
|
11
|
+
"AcquireTimeout",
|
|
12
|
+
{
|
|
13
|
+
timeout: Schema.Duration
|
|
14
|
+
}
|
|
15
|
+
) {}
|
|
16
|
+
|
|
17
|
+
export class RateLimiterInvalidConfig extends Schema.TaggedErrorClass<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))
|
package/src/Streaming.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import * as Clock from "effect/Clock"
|
|
2
|
+
import * as Deferred from "effect/Deferred"
|
|
3
|
+
import * as Duration from "effect/Duration"
|
|
4
|
+
import * as Effect from "effect/Effect"
|
|
5
|
+
import * as Schema from "effect/Schema"
|
|
6
|
+
import * as Stream from "effect/Stream"
|
|
7
|
+
import { ParseError, TimeoutError, type LlmError } from "./Errors.ts"
|
|
8
|
+
import { LlmChunk, LlmResponse, StreamProgress } from "./Models.ts"
|
|
9
|
+
|
|
10
|
+
const appendChunk = (response: LlmResponse, chunk: LlmChunk): LlmResponse => {
|
|
11
|
+
const usage = chunk.usage ?? response.usage
|
|
12
|
+
|
|
13
|
+
return LlmResponse.make({
|
|
14
|
+
content: response.content + chunk.delta,
|
|
15
|
+
metadata: {
|
|
16
|
+
...response.metadata,
|
|
17
|
+
...chunk.metadata
|
|
18
|
+
},
|
|
19
|
+
...(usage === undefined ? {} : { usage })
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const collect = Effect.fn("@llm4ts/core/Streaming.collect")(
|
|
24
|
+
<R>(stream: Stream.Stream<LlmChunk, LlmError, R>) =>
|
|
25
|
+
Stream.runFold(stream, () => LlmResponse.make({ content: "" }), appendChunk)
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
const estimateTokens = (text: string): number => Math.max(1, Math.floor(text.length / 4))
|
|
29
|
+
|
|
30
|
+
export const trackProgress = <R, R2>(
|
|
31
|
+
stream: Stream.Stream<LlmChunk, LlmError, R>,
|
|
32
|
+
onProgress: (progress: StreamProgress) => Effect.Effect<void, never, R2>
|
|
33
|
+
): Stream.Stream<LlmChunk, LlmError, R | R2> =>
|
|
34
|
+
Stream.unwrap(
|
|
35
|
+
Effect.map(Clock.currentTimeMillis, (startTime) =>
|
|
36
|
+
stream.pipe(
|
|
37
|
+
Stream.mapAccumEffect(
|
|
38
|
+
() => 0,
|
|
39
|
+
(tokensProcessed, chunk) =>
|
|
40
|
+
Effect.gen(function* () {
|
|
41
|
+
const now = yield* Clock.currentTimeMillis
|
|
42
|
+
const elapsedMs = now - startTime
|
|
43
|
+
const nextTokensProcessed = tokensProcessed + estimateTokens(chunk.delta)
|
|
44
|
+
const elapsedSeconds = elapsedMs / 1_000
|
|
45
|
+
const tokensPerSecond = elapsedSeconds > 0 ? nextTokensProcessed / elapsedSeconds : 0
|
|
46
|
+
|
|
47
|
+
yield* onProgress(
|
|
48
|
+
StreamProgress.make({
|
|
49
|
+
tokensProcessed: nextTokensProcessed,
|
|
50
|
+
tokensPerSecond,
|
|
51
|
+
elapsedMs
|
|
52
|
+
})
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return [nextTokensProcessed, [chunk]] as const
|
|
56
|
+
})
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
export const parsePartialJson = <A, E, RD, RE, R>(
|
|
63
|
+
stream: Stream.Stream<string, LlmError, R>,
|
|
64
|
+
schema: Schema.ConstraintCodec<A, E, RD, RE>
|
|
65
|
+
): Stream.Stream<A, LlmError, R | RD> =>
|
|
66
|
+
stream.pipe(
|
|
67
|
+
Stream.mapAccumEffect(
|
|
68
|
+
() => "",
|
|
69
|
+
(accumulated, chunk) => {
|
|
70
|
+
const next = accumulated + chunk
|
|
71
|
+
return Schema.decodeUnknownEffect(Schema.fromJsonString(schema))(next).pipe(
|
|
72
|
+
Effect.match({
|
|
73
|
+
onFailure: () => [next, []] as const,
|
|
74
|
+
onSuccess: (value) => [next, [value]] as const
|
|
75
|
+
})
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
const timeoutError = (duration: Duration.Input): TimeoutError =>
|
|
82
|
+
TimeoutError.make({ duration: Duration.fromInputUnsafe(duration) })
|
|
83
|
+
|
|
84
|
+
export const withTimeout = <R>(
|
|
85
|
+
stream: Stream.Stream<LlmChunk, LlmError, R>,
|
|
86
|
+
duration: Duration.Input
|
|
87
|
+
): Stream.Stream<LlmChunk, LlmError, R> =>
|
|
88
|
+
Stream.timeoutOrElse(stream, {
|
|
89
|
+
duration,
|
|
90
|
+
orElse: () => Stream.fail(timeoutError(duration))
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
export const cancellable = <R>(
|
|
94
|
+
stream: Stream.Stream<LlmChunk, LlmError, R>
|
|
95
|
+
): Effect.Effect<
|
|
96
|
+
readonly [stream: Stream.Stream<LlmChunk, LlmError, R>, cancel: Effect.Effect<void>]
|
|
97
|
+
> =>
|
|
98
|
+
Effect.map(Deferred.make<void>(), (deferred) => [
|
|
99
|
+
Stream.interruptWhen(stream, Deferred.await(deferred)),
|
|
100
|
+
Deferred.succeed(deferred, undefined).pipe(Effect.asVoid)
|
|
101
|
+
])
|
|
102
|
+
|
|
103
|
+
export const withSnapshots = <R>(
|
|
104
|
+
stream: Stream.Stream<LlmChunk, LlmError, R>,
|
|
105
|
+
snapshotInterval: Duration.Input = "500 millis"
|
|
106
|
+
): Stream.Stream<string, LlmError, R> =>
|
|
107
|
+
stream.pipe(
|
|
108
|
+
Stream.scan("", (content, chunk) => content + chunk.delta),
|
|
109
|
+
Stream.debounce(snapshotInterval)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
const chunkJson = Schema.fromJsonString(Schema.toCodecJson(LlmChunk))
|
|
113
|
+
|
|
114
|
+
export const toSSE = <R>(
|
|
115
|
+
stream: Stream.Stream<LlmChunk, LlmError, R>
|
|
116
|
+
): Stream.Stream<string, LlmError, R> =>
|
|
117
|
+
Stream.mapEffect(stream, (chunk) =>
|
|
118
|
+
Schema.encodeEffect(chunkJson)(chunk).pipe(
|
|
119
|
+
Effect.map((encoded) => `data: ${encoded}\n\n`),
|
|
120
|
+
Effect.mapError((error) =>
|
|
121
|
+
ParseError.make({
|
|
122
|
+
message: `Failed to encode SSE chunk: ${String(error)}`,
|
|
123
|
+
raw: chunk.delta
|
|
124
|
+
})
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
export const fromSSE = <R>(
|
|
130
|
+
stream: Stream.Stream<string, LlmError, R>
|
|
131
|
+
): Stream.Stream<LlmChunk, LlmError, R> =>
|
|
132
|
+
stream.pipe(
|
|
133
|
+
Stream.filter((line) => line.startsWith("data: ")),
|
|
134
|
+
Stream.map((line) => line.slice("data: ".length).trim()),
|
|
135
|
+
Stream.filter((payload) => payload.length > 0 && payload !== "[DONE]"),
|
|
136
|
+
Stream.mapEffect((payload) =>
|
|
137
|
+
Schema.decodeUnknownEffect(chunkJson)(payload).pipe(
|
|
138
|
+
Effect.mapError((error) =>
|
|
139
|
+
ParseError.make({
|
|
140
|
+
message: `Failed to parse SSE chunk: ${String(error)}`,
|
|
141
|
+
raw: payload
|
|
142
|
+
})
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
export const withHeartbeat = <R>(
|
|
149
|
+
stream: Stream.Stream<LlmChunk, LlmError, R>,
|
|
150
|
+
heartbeatTimeout: Duration.Input
|
|
151
|
+
): Stream.Stream<LlmChunk, LlmError, R> =>
|
|
152
|
+
withTimeout(stream, heartbeatTimeout).pipe(Stream.rechunk(1))
|
|
153
|
+
|
|
154
|
+
export const parallelStream = <A, B, R>(
|
|
155
|
+
inputs: Iterable<A>,
|
|
156
|
+
parallelism: number,
|
|
157
|
+
f: (input: A) => Stream.Stream<B, LlmError, R>
|
|
158
|
+
): Stream.Stream<B, LlmError, R> =>
|
|
159
|
+
Stream.fromIterable(inputs).pipe(
|
|
160
|
+
Stream.mapEffect((input) => Stream.runCollect(f(input)), {
|
|
161
|
+
concurrency: parallelism
|
|
162
|
+
}),
|
|
163
|
+
Stream.flatMap(Stream.fromIterable)
|
|
164
|
+
)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect"
|
|
2
|
+
import * as Schema from "effect/Schema"
|
|
3
|
+
import type { SchemaError } from "effect/SchemaError"
|
|
4
|
+
import { ParseError, ProviderError, type LlmError } from "./Errors.ts"
|
|
5
|
+
import type { JsonSchema } from "./Models.ts"
|
|
6
|
+
|
|
7
|
+
const snippet = (text: string): string => {
|
|
8
|
+
const trimmed = text.trim()
|
|
9
|
+
return trimmed.length <= 120 ? trimmed : `${trimmed.slice(0, 117)}...`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const extractJsonFromMarkdownFences = (raw: string): ReadonlyArray<string> => {
|
|
13
|
+
const pattern = /```(?:[\w.+-]+)?\s*(.*?)\s*```/gis
|
|
14
|
+
const results: Array<string> = []
|
|
15
|
+
|
|
16
|
+
for (const match of raw.matchAll(pattern)) {
|
|
17
|
+
const content = match[1]?.trim()
|
|
18
|
+
if (content !== undefined && content.length > 0) {
|
|
19
|
+
results.push(content)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return results
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const extractBalancedJsonObjects = (raw: string): ReadonlyArray<string> => {
|
|
27
|
+
const results: Array<string> = []
|
|
28
|
+
let startIndex: number | undefined
|
|
29
|
+
let depth = 0
|
|
30
|
+
let inString = false
|
|
31
|
+
let escaping = false
|
|
32
|
+
|
|
33
|
+
for (let index = 0; index < raw.length; index += 1) {
|
|
34
|
+
const character = raw.charAt(index)
|
|
35
|
+
|
|
36
|
+
if (inString) {
|
|
37
|
+
if (escaping) {
|
|
38
|
+
escaping = false
|
|
39
|
+
} else if (character === "\\") {
|
|
40
|
+
escaping = true
|
|
41
|
+
} else if (character === '"') {
|
|
42
|
+
inString = false
|
|
43
|
+
}
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (character === '"') {
|
|
48
|
+
inString = true
|
|
49
|
+
} else if (character === "{") {
|
|
50
|
+
startIndex ??= index
|
|
51
|
+
depth += 1
|
|
52
|
+
} else if (character === "}" && depth > 0) {
|
|
53
|
+
depth -= 1
|
|
54
|
+
if (depth === 0 && startIndex !== undefined) {
|
|
55
|
+
results.push(raw.slice(startIndex, index + 1))
|
|
56
|
+
startIndex = undefined
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return results
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const jsonCandidates = (raw: string): ReadonlyArray<string> => {
|
|
65
|
+
const fenced = extractJsonFromMarkdownFences(raw)
|
|
66
|
+
const balanced = [
|
|
67
|
+
...extractBalancedJsonObjects(raw),
|
|
68
|
+
...fenced.flatMap(extractBalancedJsonObjects)
|
|
69
|
+
]
|
|
70
|
+
const seen = new Set<string>()
|
|
71
|
+
const candidates: Array<string> = []
|
|
72
|
+
|
|
73
|
+
for (const candidate of [raw, ...fenced, ...balanced]) {
|
|
74
|
+
const trimmed = candidate.trim()
|
|
75
|
+
if (trimmed.length > 0 && !seen.has(trimmed)) {
|
|
76
|
+
seen.add(trimmed)
|
|
77
|
+
candidates.push(trimmed)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return candidates
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const withSchemaHint = (prompt: string, schema: JsonSchema): string => {
|
|
85
|
+
if (Object.keys(schema).length === 0) {
|
|
86
|
+
return prompt
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const encoded = JSON.stringify(schema) ?? "{}"
|
|
90
|
+
return `${prompt}\n\nReturn JSON matching this schema:\n${encoded}`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface CandidateFailure {
|
|
94
|
+
readonly candidate: string
|
|
95
|
+
readonly error: SchemaError
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const parseFromText = Effect.fn("@llm4ts/core/StructuredOutput.parseFromText")(<
|
|
99
|
+
A,
|
|
100
|
+
E,
|
|
101
|
+
RD,
|
|
102
|
+
RE
|
|
103
|
+
>(
|
|
104
|
+
raw: string,
|
|
105
|
+
schema: Schema.ConstraintCodec<A, E, RD, RE>,
|
|
106
|
+
_jsonSchema: JsonSchema
|
|
107
|
+
): Effect.Effect<A, LlmError, RD> => {
|
|
108
|
+
if (raw.trim().length === 0) {
|
|
109
|
+
return Effect.fail(
|
|
110
|
+
ProviderError.make({
|
|
111
|
+
message: "empty response from provider — no text to parse as structured output"
|
|
112
|
+
})
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const candidates = jsonCandidates(raw)
|
|
117
|
+
const attempts = candidates.map((candidate) =>
|
|
118
|
+
Schema.decodeUnknownEffect(Schema.fromJsonString(schema))(candidate).pipe(
|
|
119
|
+
Effect.mapError(
|
|
120
|
+
(error): CandidateFailure => ({
|
|
121
|
+
candidate,
|
|
122
|
+
error
|
|
123
|
+
})
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return Effect.firstSuccessOf(attempts).pipe(
|
|
129
|
+
Effect.mapError(({ candidate, error }) =>
|
|
130
|
+
ParseError.make({
|
|
131
|
+
message:
|
|
132
|
+
`Failed to parse response as structured output: ${String(error)} ` +
|
|
133
|
+
`(tried ${attempts.length} candidate(s); last: ${snippet(candidate)})`,
|
|
134
|
+
raw
|
|
135
|
+
})
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
})
|