@codilore/llm 1.15.13
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/AGENTS.md +321 -0
- package/README.md +131 -0
- package/example/call-sites.md +591 -0
- package/example/tutorial.ts +255 -0
- package/package.json +50 -0
- package/script/recording-cost-report.ts +250 -0
- package/script/setup-recording-env.ts +542 -0
- package/src/cache-policy.ts +111 -0
- package/src/index.ts +32 -0
- package/src/llm.ts +186 -0
- package/src/protocols/anthropic-messages.ts +841 -0
- package/src/protocols/bedrock-converse.ts +649 -0
- package/src/protocols/bedrock-event-stream.ts +87 -0
- package/src/protocols/gemini.ts +465 -0
- package/src/protocols/index.ts +6 -0
- package/src/protocols/openai-chat.ts +431 -0
- package/src/protocols/openai-compatible-chat.ts +24 -0
- package/src/protocols/openai-responses.ts +987 -0
- package/src/protocols/shared.ts +283 -0
- package/src/protocols/utils/bedrock-auth.ts +70 -0
- package/src/protocols/utils/bedrock-cache.ts +37 -0
- package/src/protocols/utils/bedrock-media.ts +80 -0
- package/src/protocols/utils/cache.ts +16 -0
- package/src/protocols/utils/gemini-tool-schema.ts +101 -0
- package/src/protocols/utils/lifecycle.ts +102 -0
- package/src/protocols/utils/openai-options.ts +84 -0
- package/src/protocols/utils/tool-stream.ts +218 -0
- package/src/provider.ts +37 -0
- package/src/providers/amazon-bedrock.ts +43 -0
- package/src/providers/anthropic.ts +35 -0
- package/src/providers/azure.ts +110 -0
- package/src/providers/cloudflare.ts +127 -0
- package/src/providers/github-copilot.ts +66 -0
- package/src/providers/google.ts +35 -0
- package/src/providers/index.ts +11 -0
- package/src/providers/openai-compatible-profile.ts +20 -0
- package/src/providers/openai-compatible.ts +65 -0
- package/src/providers/openai-options.ts +81 -0
- package/src/providers/openai.ts +63 -0
- package/src/providers/openrouter.ts +98 -0
- package/src/providers/xai.ts +56 -0
- package/src/route/auth-options.ts +57 -0
- package/src/route/auth.ts +156 -0
- package/src/route/client.ts +434 -0
- package/src/route/endpoint.ts +53 -0
- package/src/route/executor.ts +374 -0
- package/src/route/framing.ts +27 -0
- package/src/route/index.ts +25 -0
- package/src/route/protocol.ts +84 -0
- package/src/route/transport/http.ts +108 -0
- package/src/route/transport/index.ts +33 -0
- package/src/route/transport/websocket.ts +280 -0
- package/src/schema/errors.ts +203 -0
- package/src/schema/events.ts +370 -0
- package/src/schema/ids.ts +43 -0
- package/src/schema/index.ts +5 -0
- package/src/schema/messages.ts +404 -0
- package/src/schema/options.ts +221 -0
- package/src/tool-runtime.ts +78 -0
- package/src/tool.ts +241 -0
- package/src/utils/record.ts +3 -0
- package/sst-env.d.ts +10 -0
- package/test/adapter.test.ts +164 -0
- package/test/auth-options.types.ts +168 -0
- package/test/auth.test.ts +103 -0
- package/test/cache-policy.test.ts +262 -0
- package/test/continuation-scenarios.ts +104 -0
- package/test/endpoint.test.ts +58 -0
- package/test/executor.test.ts +418 -0
- package/test/exports.test.ts +62 -0
- package/test/fixtures/media/restroom.png +0 -0
- package/test/fixtures/recordings/anthropic-messages/accepts-malformed-assistant-tool-order-with-default-patch.json +29 -0
- package/test/fixtures/recordings/anthropic-messages/anthropic-opus-4-7-image-tool-result.json +43 -0
- package/test/fixtures/recordings/anthropic-messages/claude-opus-4-7-drives-a-tool-loop.json +56 -0
- package/test/fixtures/recordings/anthropic-messages/rejects-malformed-assistant-tool-order-without-patch.json +29 -0
- package/test/fixtures/recordings/anthropic-messages/streams-text.json +29 -0
- package/test/fixtures/recordings/anthropic-messages/streams-tool-call.json +29 -0
- package/test/fixtures/recordings/anthropic-messages-cache/writes-then-reads-cache-control-on-identical-second-call.json +48 -0
- package/test/fixtures/recordings/bedrock-converse/drives-a-tool-loop.json +55 -0
- package/test/fixtures/recordings/bedrock-converse/streams-a-tool-call.json +29 -0
- package/test/fixtures/recordings/bedrock-converse/streams-text.json +29 -0
- package/test/fixtures/recordings/cloudflare-ai-gateway/cloudflare-ai-gateway-workers-ai-gpt-oss-20b-tools-tool-call.json +32 -0
- package/test/fixtures/recordings/cloudflare-ai-gateway/cloudflare-ai-gateway-workers-ai-llama-3-1-8b-text.json +32 -0
- package/test/fixtures/recordings/cloudflare-workers-ai/cloudflare-workers-ai-gpt-oss-20b-tools-tool-call.json +32 -0
- package/test/fixtures/recordings/cloudflare-workers-ai/cloudflare-workers-ai-llama-3-1-8b-text.json +32 -0
- package/test/fixtures/recordings/gemini/gemini-2-5-flash-image.json +32 -0
- package/test/fixtures/recordings/gemini/streams-text.json +28 -0
- package/test/fixtures/recordings/gemini/streams-tool-call.json +28 -0
- package/test/fixtures/recordings/gemini-cache/reports-cachedcontenttokencount-on-identical-second-call.json +46 -0
- package/test/fixtures/recordings/openai-chat/continues-after-tool-result.json +28 -0
- package/test/fixtures/recordings/openai-chat/drives-a-tool-loop-end-to-end.json +46 -0
- package/test/fixtures/recordings/openai-chat/streams-text.json +28 -0
- package/test/fixtures/recordings/openai-chat/streams-tool-call.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/deepseek-streams-text.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/groq-llama-3-3-70b-drives-a-tool-loop.json +53 -0
- package/test/fixtures/recordings/openai-compatible-chat/groq-streams-text.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/groq-streams-tool-call.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/openrouter-claude-opus-4-7-drives-a-tool-loop.json +54 -0
- package/test/fixtures/recordings/openai-compatible-chat/openrouter-gpt-4o-mini-drives-a-tool-loop.json +53 -0
- package/test/fixtures/recordings/openai-compatible-chat/openrouter-gpt-5-5-drives-a-tool-loop.json +54 -0
- package/test/fixtures/recordings/openai-compatible-chat/openrouter-streams-text.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/openrouter-streams-tool-call.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/togetherai-streams-text.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/togetherai-streams-tool-call.json +28 -0
- package/test/fixtures/recordings/openai-responses/gpt-5-5-drives-a-tool-loop.json +54 -0
- package/test/fixtures/recordings/openai-responses/gpt-5-5-streams-text.json +28 -0
- package/test/fixtures/recordings/openai-responses/gpt-5-5-streams-tool-call.json +28 -0
- package/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-image-tool-result.json +42 -0
- package/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning-continuation.json +58 -0
- package/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning.json +32 -0
- package/test/fixtures/recordings/openai-responses-cache/reports-cached-tokens-on-identical-second-call.json +46 -0
- package/test/generate-object.test.ts +184 -0
- package/test/lib/effect.ts +50 -0
- package/test/lib/http.ts +98 -0
- package/test/lib/openai-chunks.ts +27 -0
- package/test/lib/sse.ts +17 -0
- package/test/lib/tool-runtime.ts +146 -0
- package/test/llm.test.ts +167 -0
- package/test/provider/anthropic-messages-cache.recorded.test.ts +54 -0
- package/test/provider/anthropic-messages.recorded.test.ts +46 -0
- package/test/provider/anthropic-messages.test.ts +829 -0
- package/test/provider/bedrock-converse-cache.recorded.test.ts +54 -0
- package/test/provider/bedrock-converse.test.ts +707 -0
- package/test/provider/cloudflare.test.ts +230 -0
- package/test/provider/gemini-cache.recorded.test.ts +48 -0
- package/test/provider/gemini.test.ts +476 -0
- package/test/provider/golden.recorded.test.ts +219 -0
- package/test/provider/openai-chat.test.ts +446 -0
- package/test/provider/openai-compatible-chat.test.ts +238 -0
- package/test/provider/openai-responses-cache.recorded.test.ts +46 -0
- package/test/provider/openai-responses.test.ts +1322 -0
- package/test/provider/openrouter.test.ts +56 -0
- package/test/provider.types.ts +41 -0
- package/test/recorded-golden.ts +97 -0
- package/test/recorded-runner.ts +100 -0
- package/test/recorded-scenarios.ts +531 -0
- package/test/recorded-test.ts +74 -0
- package/test/recorded-utils.ts +56 -0
- package/test/recorded-websocket.ts +26 -0
- package/test/route.test.ts +43 -0
- package/test/schema.test.ts +97 -0
- package/test/tool-runtime.test.ts +802 -0
- package/test/tool-stream.test.ts +99 -0
- package/test/tool.types.ts +40 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { Schema } from "effect"
|
|
2
|
+
import { JsonSchema, MessageRole, ProviderMetadata } from "./ids"
|
|
3
|
+
import { CacheHint, CachePolicy, GenerationOptions, HttpOptions, ModelSchema, ProviderOptions } from "./options"
|
|
4
|
+
import { isRecord } from "../utils/record"
|
|
5
|
+
|
|
6
|
+
const systemPartSchema = Schema.Struct({
|
|
7
|
+
type: Schema.Literal("text"),
|
|
8
|
+
text: Schema.String,
|
|
9
|
+
cache: Schema.optional(CacheHint),
|
|
10
|
+
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
11
|
+
}).annotate({ identifier: "LLM.SystemPart" })
|
|
12
|
+
export type SystemPart = Schema.Schema.Type<typeof systemPartSchema>
|
|
13
|
+
|
|
14
|
+
const makeSystemPart = (text: string): SystemPart => ({ type: "text", text })
|
|
15
|
+
|
|
16
|
+
export const SystemPart = Object.assign(systemPartSchema, {
|
|
17
|
+
make: makeSystemPart,
|
|
18
|
+
content: (input?: string | SystemPart | ReadonlyArray<SystemPart>) => {
|
|
19
|
+
if (input === undefined) return []
|
|
20
|
+
return typeof input === "string" ? [makeSystemPart(input)] : Array.isArray(input) ? [...input] : [input]
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export const TextPart = Schema.Struct({
|
|
25
|
+
type: Schema.Literal("text"),
|
|
26
|
+
text: Schema.String,
|
|
27
|
+
cache: Schema.optional(CacheHint),
|
|
28
|
+
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
29
|
+
providerMetadata: Schema.optional(ProviderMetadata),
|
|
30
|
+
}).annotate({ identifier: "LLM.Content.Text" })
|
|
31
|
+
export type TextPart = Schema.Schema.Type<typeof TextPart>
|
|
32
|
+
|
|
33
|
+
export const MediaPart = Schema.Struct({
|
|
34
|
+
type: Schema.Literal("media"),
|
|
35
|
+
mediaType: Schema.String,
|
|
36
|
+
data: Schema.Union([Schema.String, Schema.Uint8Array]),
|
|
37
|
+
filename: Schema.optional(Schema.String),
|
|
38
|
+
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
39
|
+
}).annotate({ identifier: "LLM.Content.Media" })
|
|
40
|
+
export type MediaPart = Schema.Schema.Type<typeof MediaPart>
|
|
41
|
+
|
|
42
|
+
export const ToolResultMediaPart = Schema.Struct({
|
|
43
|
+
type: Schema.Literal("media"),
|
|
44
|
+
mediaType: Schema.String,
|
|
45
|
+
data: Schema.String,
|
|
46
|
+
filename: Schema.optional(Schema.String),
|
|
47
|
+
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
48
|
+
}).annotate({ identifier: "LLM.ToolResult.Media" })
|
|
49
|
+
export type ToolResultMediaPart = Schema.Schema.Type<typeof ToolResultMediaPart>
|
|
50
|
+
|
|
51
|
+
export const ToolResultContentPart = Schema.Union([TextPart, ToolResultMediaPart])
|
|
52
|
+
export type ToolResultContentPart = Schema.Schema.Type<typeof ToolResultContentPart>
|
|
53
|
+
|
|
54
|
+
export class ToolTextContent extends Schema.Class<ToolTextContent>("Tool.TextContent")({
|
|
55
|
+
type: Schema.Literal("text"),
|
|
56
|
+
text: Schema.String,
|
|
57
|
+
}) {}
|
|
58
|
+
|
|
59
|
+
export const ToolFileSource = Schema.Union([
|
|
60
|
+
Schema.Struct({ type: Schema.Literal("data"), data: Schema.String }),
|
|
61
|
+
Schema.Struct({ type: Schema.Literal("url"), url: Schema.String }),
|
|
62
|
+
Schema.Struct({ type: Schema.Literal("file"), uri: Schema.String }),
|
|
63
|
+
]).pipe(Schema.toTaggedUnion("type"))
|
|
64
|
+
export type ToolFileSource = Schema.Schema.Type<typeof ToolFileSource>
|
|
65
|
+
|
|
66
|
+
export class ToolFileContent extends Schema.Class<ToolFileContent>("Tool.FileContent")({
|
|
67
|
+
type: Schema.Literal("file"),
|
|
68
|
+
source: ToolFileSource,
|
|
69
|
+
mime: Schema.String,
|
|
70
|
+
name: Schema.optional(Schema.String),
|
|
71
|
+
}) {}
|
|
72
|
+
|
|
73
|
+
/** Ordered, provider-independent content shown to models and UIs after a tool succeeds. */
|
|
74
|
+
export const ToolContent = Schema.Union([ToolTextContent, ToolFileContent]).pipe(Schema.toTaggedUnion("type"))
|
|
75
|
+
export type ToolContent = Schema.Schema.Type<typeof ToolContent>
|
|
76
|
+
|
|
77
|
+
export const toolText = (value: ConstructorParameters<typeof ToolTextContent>[0]) => new ToolTextContent(value)
|
|
78
|
+
export const toolFile = (value: ConstructorParameters<typeof ToolFileContent>[0]) => new ToolFileContent(value)
|
|
79
|
+
|
|
80
|
+
const inlineData = (uri: string) => {
|
|
81
|
+
if (!uri.startsWith("data:")) return undefined
|
|
82
|
+
const match = /^data:[^;,]+;base64,(.*)$/s.exec(uri)
|
|
83
|
+
if (!match) throw new Error("Tool file data URI must contain raw base64 bytes")
|
|
84
|
+
return match[1]!
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const legacyInlineData = (value: string) => {
|
|
88
|
+
const data = inlineData(value)
|
|
89
|
+
if (data !== undefined) return data
|
|
90
|
+
if (/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) return value
|
|
91
|
+
throw new Error("Legacy tool-result media must contain raw base64 bytes or a base64 data URI")
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Convert a legacy attachment URI without guessing unknown string semantics. */
|
|
95
|
+
export const toolFileSourceFromUri = (uri: string): ToolFileSource => {
|
|
96
|
+
const data = inlineData(uri)
|
|
97
|
+
if (data !== undefined) return { type: "data", data }
|
|
98
|
+
const url = URL.parse(uri)
|
|
99
|
+
if (url?.protocol === "file:") return { type: "file", uri }
|
|
100
|
+
if (url?.protocol === "http:" || url?.protocol === "https:") return { type: "url", url: uri }
|
|
101
|
+
throw new Error(`Unsupported tool file URI: ${uri}`)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const isToolResultValue = (value: unknown): value is ToolResultValue =>
|
|
105
|
+
isRecord(value) &&
|
|
106
|
+
(value.type === "text" || value.type === "json" || value.type === "error" || value.type === "content") &&
|
|
107
|
+
"value" in value
|
|
108
|
+
|
|
109
|
+
export const ToolResultValue = Object.assign(
|
|
110
|
+
Schema.Union([
|
|
111
|
+
Schema.Struct({
|
|
112
|
+
type: Schema.Literal("json"),
|
|
113
|
+
value: Schema.Unknown,
|
|
114
|
+
}),
|
|
115
|
+
Schema.Struct({
|
|
116
|
+
type: Schema.Literal("text"),
|
|
117
|
+
value: Schema.Unknown,
|
|
118
|
+
}),
|
|
119
|
+
Schema.Struct({
|
|
120
|
+
type: Schema.Literal("error"),
|
|
121
|
+
value: Schema.Unknown,
|
|
122
|
+
}),
|
|
123
|
+
Schema.Struct({
|
|
124
|
+
type: Schema.Literal("content"),
|
|
125
|
+
value: Schema.Array(ToolResultContentPart),
|
|
126
|
+
}),
|
|
127
|
+
]).annotate({ identifier: "LLM.ToolResult" }),
|
|
128
|
+
{
|
|
129
|
+
is: isToolResultValue,
|
|
130
|
+
make: (value: unknown, type: ToolResultValue["type"] = "json"): ToolResultValue => {
|
|
131
|
+
if (isToolResultValue(value)) return value
|
|
132
|
+
if (type === "content") return { type, value: Array.isArray(value) ? value : [] }
|
|
133
|
+
return { type, value }
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
export type ToolResultValue = Schema.Schema.Type<typeof ToolResultValue>
|
|
138
|
+
|
|
139
|
+
export interface ToolOutput {
|
|
140
|
+
readonly structured: unknown
|
|
141
|
+
readonly content: ReadonlyArray<ToolContent>
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const ToolOutput = Object.assign(
|
|
145
|
+
Schema.Struct({
|
|
146
|
+
structured: Schema.Unknown,
|
|
147
|
+
content: Schema.Array(ToolContent),
|
|
148
|
+
}).annotate({ identifier: "LLM.ToolOutput" }),
|
|
149
|
+
{
|
|
150
|
+
make: (structured: unknown, content: ReadonlyArray<ToolContent> = []): ToolOutput => ({
|
|
151
|
+
structured,
|
|
152
|
+
content: content.map((item) =>
|
|
153
|
+
item.type === "text"
|
|
154
|
+
? toolText({ type: "text", text: item.text })
|
|
155
|
+
: toolFile({ type: "file", source: item.source, mime: item.mime, name: item.name }),
|
|
156
|
+
),
|
|
157
|
+
}),
|
|
158
|
+
fromResultValue: (result: ToolResultValue): ToolOutput | undefined => {
|
|
159
|
+
switch (result.type) {
|
|
160
|
+
case "json":
|
|
161
|
+
return { structured: result.value, content: [] }
|
|
162
|
+
case "text":
|
|
163
|
+
return { structured: {}, content: [toolText({ type: "text", text: toolResultText(result.value) })] }
|
|
164
|
+
case "content":
|
|
165
|
+
return {
|
|
166
|
+
structured: {},
|
|
167
|
+
content: result.value.map((item) =>
|
|
168
|
+
item.type === "text"
|
|
169
|
+
? toolText({ type: "text", text: item.text })
|
|
170
|
+
: toolFile({
|
|
171
|
+
type: "file",
|
|
172
|
+
source: { type: "data", data: legacyInlineData(item.data) },
|
|
173
|
+
mime: item.mediaType,
|
|
174
|
+
name: item.filename,
|
|
175
|
+
}),
|
|
176
|
+
),
|
|
177
|
+
}
|
|
178
|
+
case "error":
|
|
179
|
+
return undefined
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
toResultValue: (output: ToolOutput): ToolResultValue => {
|
|
183
|
+
if (output.content.length === 0) return { type: "json", value: output.structured }
|
|
184
|
+
if (output.content.length === 1 && output.content[0]?.type === "text")
|
|
185
|
+
return { type: "text", value: output.content[0].text }
|
|
186
|
+
const unsupported = output.content.find((item) => item.type === "file" && item.source.type !== "data")
|
|
187
|
+
if (unsupported?.type === "file")
|
|
188
|
+
return {
|
|
189
|
+
type: "error",
|
|
190
|
+
value: `Tool file source "${unsupported.source.type}" must be materialized to inline data before provider conversion`,
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
type: "content",
|
|
194
|
+
value: output.content.map((item) => {
|
|
195
|
+
if (item.type === "text") return { type: "text", text: item.text }
|
|
196
|
+
if (item.source.type !== "data")
|
|
197
|
+
throw new Error("Unmaterialized tool file source reached provider conversion")
|
|
198
|
+
return { type: "media", mediaType: item.mime, data: item.source.data, filename: item.name }
|
|
199
|
+
}),
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
const toolResultText = (value: unknown) => {
|
|
206
|
+
if (typeof value === "string") return value
|
|
207
|
+
try {
|
|
208
|
+
return JSON.stringify(value) ?? String(value)
|
|
209
|
+
} catch {
|
|
210
|
+
return String(value)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export const ToolCallPart = Object.assign(
|
|
215
|
+
Schema.Struct({
|
|
216
|
+
type: Schema.Literal("tool-call"),
|
|
217
|
+
id: Schema.String,
|
|
218
|
+
name: Schema.String,
|
|
219
|
+
input: Schema.Unknown,
|
|
220
|
+
providerExecuted: Schema.optional(Schema.Boolean),
|
|
221
|
+
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
222
|
+
providerMetadata: Schema.optional(ProviderMetadata),
|
|
223
|
+
}).annotate({ identifier: "LLM.Content.ToolCall" }),
|
|
224
|
+
{
|
|
225
|
+
make: (input: Omit<ToolCallPart, "type">): ToolCallPart => ({ type: "tool-call", ...input }),
|
|
226
|
+
},
|
|
227
|
+
)
|
|
228
|
+
export type ToolCallPart = Schema.Schema.Type<typeof ToolCallPart>
|
|
229
|
+
|
|
230
|
+
export const ToolResultPart = Object.assign(
|
|
231
|
+
Schema.Struct({
|
|
232
|
+
type: Schema.Literal("tool-result"),
|
|
233
|
+
id: Schema.String,
|
|
234
|
+
name: Schema.String,
|
|
235
|
+
result: ToolResultValue,
|
|
236
|
+
providerExecuted: Schema.optional(Schema.Boolean),
|
|
237
|
+
cache: Schema.optional(CacheHint),
|
|
238
|
+
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
239
|
+
providerMetadata: Schema.optional(ProviderMetadata),
|
|
240
|
+
}).annotate({ identifier: "LLM.Content.ToolResult" }),
|
|
241
|
+
{
|
|
242
|
+
make: (
|
|
243
|
+
input: Omit<ToolResultPart, "type" | "result"> & {
|
|
244
|
+
readonly result: unknown
|
|
245
|
+
readonly resultType?: ToolResultValue["type"]
|
|
246
|
+
},
|
|
247
|
+
): ToolResultPart => ({
|
|
248
|
+
type: "tool-result",
|
|
249
|
+
id: input.id,
|
|
250
|
+
name: input.name,
|
|
251
|
+
result: ToolResultValue.make(input.result, input.resultType),
|
|
252
|
+
providerExecuted: input.providerExecuted,
|
|
253
|
+
cache: input.cache,
|
|
254
|
+
metadata: input.metadata,
|
|
255
|
+
providerMetadata: input.providerMetadata,
|
|
256
|
+
}),
|
|
257
|
+
},
|
|
258
|
+
)
|
|
259
|
+
export type ToolResultPart = Schema.Schema.Type<typeof ToolResultPart>
|
|
260
|
+
|
|
261
|
+
export const ReasoningPart = Schema.Struct({
|
|
262
|
+
type: Schema.Literal("reasoning"),
|
|
263
|
+
text: Schema.String,
|
|
264
|
+
encrypted: Schema.optional(Schema.String),
|
|
265
|
+
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
266
|
+
providerMetadata: Schema.optional(ProviderMetadata),
|
|
267
|
+
}).annotate({ identifier: "LLM.Content.Reasoning" })
|
|
268
|
+
export type ReasoningPart = Schema.Schema.Type<typeof ReasoningPart>
|
|
269
|
+
|
|
270
|
+
export const ContentPart = Schema.Union([TextPart, MediaPart, ToolCallPart, ToolResultPart, ReasoningPart]).pipe(
|
|
271
|
+
Schema.toTaggedUnion("type"),
|
|
272
|
+
)
|
|
273
|
+
export type ContentPart = Schema.Schema.Type<typeof ContentPart>
|
|
274
|
+
|
|
275
|
+
export class Message extends Schema.Class<Message>("LLM.Message")({
|
|
276
|
+
id: Schema.optional(Schema.String),
|
|
277
|
+
role: MessageRole,
|
|
278
|
+
content: Schema.Array(ContentPart),
|
|
279
|
+
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
280
|
+
native: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
281
|
+
}) {}
|
|
282
|
+
|
|
283
|
+
export namespace Message {
|
|
284
|
+
export type ContentInput = string | ContentPart | ReadonlyArray<ContentPart>
|
|
285
|
+
export type SystemContentInput = string | TextPart | ReadonlyArray<TextPart>
|
|
286
|
+
export type Input = Omit<ConstructorParameters<typeof Message>[0], "content"> & {
|
|
287
|
+
readonly content: ContentInput
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export const text = (value: string): ContentPart => ({ type: "text", text: value })
|
|
291
|
+
|
|
292
|
+
export const content = (input: ContentInput) =>
|
|
293
|
+
typeof input === "string" ? [text(input)] : Array.isArray(input) ? [...input] : [input]
|
|
294
|
+
|
|
295
|
+
export const make = (input: Message | Input) => {
|
|
296
|
+
if (input instanceof Message) return input
|
|
297
|
+
return new Message({ ...input, content: content(input.content) })
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export const user = (content: ContentInput) => make({ role: "user", content })
|
|
301
|
+
|
|
302
|
+
export const assistant = (content: ContentInput) => make({ role: "assistant", content })
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Add an operator-authored instruction at this chronological point in the
|
|
306
|
+
* conversation. This is distinct from the initial `LLMRequest.system`
|
|
307
|
+
* prompt. Keep raw retrieved, tool, and web content out of privileged system
|
|
308
|
+
* updates; pass that untrusted content through ordinary user/tool channels.
|
|
309
|
+
*/
|
|
310
|
+
export const system = (content: SystemContentInput) => make({ role: "system", content })
|
|
311
|
+
|
|
312
|
+
export const tool = (result: ToolResultPart | Parameters<typeof ToolResultPart.make>[0]) =>
|
|
313
|
+
make({ role: "tool", content: ["type" in result ? result : ToolResultPart.make(result)] })
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export class ToolDefinition extends Schema.Class<ToolDefinition>("LLM.ToolDefinition")({
|
|
317
|
+
name: Schema.String,
|
|
318
|
+
description: Schema.String,
|
|
319
|
+
inputSchema: JsonSchema,
|
|
320
|
+
outputSchema: Schema.optional(JsonSchema),
|
|
321
|
+
cache: Schema.optional(CacheHint),
|
|
322
|
+
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
323
|
+
native: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
324
|
+
}) {}
|
|
325
|
+
|
|
326
|
+
export namespace ToolDefinition {
|
|
327
|
+
export type Input = ToolDefinition | ConstructorParameters<typeof ToolDefinition>[0]
|
|
328
|
+
|
|
329
|
+
/** Normalize tool definition input into the canonical `ToolDefinition` class. */
|
|
330
|
+
export const make = (input: Input) => (input instanceof ToolDefinition ? input : new ToolDefinition(input))
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export class ToolChoice extends Schema.Class<ToolChoice>("LLM.ToolChoice")({
|
|
334
|
+
type: Schema.Literals(["auto", "none", "required", "tool"]),
|
|
335
|
+
name: Schema.optional(Schema.String),
|
|
336
|
+
}) {}
|
|
337
|
+
|
|
338
|
+
export namespace ToolChoice {
|
|
339
|
+
export type Mode = Exclude<ToolChoice["type"], "tool">
|
|
340
|
+
export type Input = ToolChoice | ConstructorParameters<typeof ToolChoice>[0] | ToolDefinition | string
|
|
341
|
+
|
|
342
|
+
const isMode = (value: string): value is Mode => value === "auto" || value === "none" || value === "required"
|
|
343
|
+
|
|
344
|
+
/** Select a specific named tool. */
|
|
345
|
+
export const named = (value: string) => new ToolChoice({ type: "tool", name: value })
|
|
346
|
+
|
|
347
|
+
/** Normalize ergonomic tool-choice inputs into the canonical `ToolChoice` class. */
|
|
348
|
+
export const make = (input: Input) => {
|
|
349
|
+
if (input instanceof ToolChoice) return input
|
|
350
|
+
if (input instanceof ToolDefinition) return named(input.name)
|
|
351
|
+
if (typeof input === "string") return isMode(input) ? new ToolChoice({ type: input }) : named(input)
|
|
352
|
+
return new ToolChoice(input)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export const ResponseFormat = Schema.Union([
|
|
357
|
+
Schema.Struct({ type: Schema.Literal("text") }),
|
|
358
|
+
Schema.Struct({ type: Schema.Literal("json"), schema: JsonSchema }),
|
|
359
|
+
Schema.Struct({ type: Schema.Literal("tool"), tool: ToolDefinition }),
|
|
360
|
+
]).pipe(Schema.toTaggedUnion("type"))
|
|
361
|
+
export type ResponseFormat = Schema.Schema.Type<typeof ResponseFormat>
|
|
362
|
+
|
|
363
|
+
export class LLMRequest extends Schema.Class<LLMRequest>("LLM.Request")({
|
|
364
|
+
id: Schema.optional(Schema.String),
|
|
365
|
+
model: ModelSchema,
|
|
366
|
+
system: Schema.Array(SystemPart),
|
|
367
|
+
messages: Schema.Array(Message),
|
|
368
|
+
tools: Schema.Array(ToolDefinition),
|
|
369
|
+
toolChoice: Schema.optional(ToolChoice),
|
|
370
|
+
generation: Schema.optional(GenerationOptions),
|
|
371
|
+
providerOptions: Schema.optional(ProviderOptions),
|
|
372
|
+
http: Schema.optional(HttpOptions),
|
|
373
|
+
responseFormat: Schema.optional(ResponseFormat),
|
|
374
|
+
cache: Schema.optional(CachePolicy),
|
|
375
|
+
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
376
|
+
}) {}
|
|
377
|
+
|
|
378
|
+
export namespace LLMRequest {
|
|
379
|
+
export type Input = ConstructorParameters<typeof LLMRequest>[0]
|
|
380
|
+
|
|
381
|
+
export const input = (request: LLMRequest): Input => ({
|
|
382
|
+
id: request.id,
|
|
383
|
+
model: request.model,
|
|
384
|
+
system: request.system,
|
|
385
|
+
messages: request.messages,
|
|
386
|
+
tools: request.tools,
|
|
387
|
+
toolChoice: request.toolChoice,
|
|
388
|
+
generation: request.generation,
|
|
389
|
+
providerOptions: request.providerOptions,
|
|
390
|
+
http: request.http,
|
|
391
|
+
responseFormat: request.responseFormat,
|
|
392
|
+
cache: request.cache,
|
|
393
|
+
metadata: request.metadata,
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
export const update = (request: LLMRequest, patch: Partial<Input>) => {
|
|
397
|
+
if (Object.keys(patch).length === 0) return request
|
|
398
|
+
return new LLMRequest({
|
|
399
|
+
...input(request),
|
|
400
|
+
...patch,
|
|
401
|
+
model: patch.model ?? request.model,
|
|
402
|
+
})
|
|
403
|
+
}
|
|
404
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { Schema } from "effect"
|
|
2
|
+
import { JsonSchema, ModelID, ProviderID } from "./ids"
|
|
3
|
+
import type { AnyRoute } from "../route/client"
|
|
4
|
+
import { isRecord } from "../utils/record"
|
|
5
|
+
|
|
6
|
+
export const mergeJsonRecords = (
|
|
7
|
+
...items: ReadonlyArray<Record<string, unknown> | undefined>
|
|
8
|
+
): Record<string, unknown> | undefined => {
|
|
9
|
+
const defined = items.filter((item): item is Record<string, unknown> => item !== undefined)
|
|
10
|
+
if (defined.length === 0) return undefined
|
|
11
|
+
if (defined.length === 1 && Object.values(defined[0]).every((value) => value !== undefined)) return defined[0]
|
|
12
|
+
const result: Record<string, unknown> = {}
|
|
13
|
+
for (const item of defined) {
|
|
14
|
+
for (const [key, value] of Object.entries(item)) {
|
|
15
|
+
if (value === undefined) continue
|
|
16
|
+
result[key] = isRecord(result[key]) && isRecord(value) ? mergeJsonRecords(result[key], value) : value
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return Object.keys(result).length === 0 ? undefined : result
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const mergeStringRecords = (
|
|
23
|
+
...items: ReadonlyArray<Record<string, string> | undefined>
|
|
24
|
+
): Record<string, string> | undefined => {
|
|
25
|
+
const defined = items.filter((item): item is Record<string, string> => item !== undefined)
|
|
26
|
+
if (defined.length === 0) return undefined
|
|
27
|
+
if (defined.length === 1) return defined[0]
|
|
28
|
+
const result = Object.fromEntries(
|
|
29
|
+
defined.flatMap((item) =>
|
|
30
|
+
Object.entries(item).filter((entry): entry is [string, string] => entry[1] !== undefined),
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
return Object.keys(result).length === 0 ? undefined : result
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const ProviderOptions = Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown))
|
|
37
|
+
export type ProviderOptions = Schema.Schema.Type<typeof ProviderOptions>
|
|
38
|
+
|
|
39
|
+
export const mergeProviderOptions = (
|
|
40
|
+
...items: ReadonlyArray<ProviderOptions | undefined>
|
|
41
|
+
): ProviderOptions | undefined => {
|
|
42
|
+
const result: Record<string, Record<string, unknown>> = {}
|
|
43
|
+
for (const item of items) {
|
|
44
|
+
if (!item) continue
|
|
45
|
+
for (const [provider, options] of Object.entries(item)) {
|
|
46
|
+
const merged = mergeJsonRecords(result[provider], options)
|
|
47
|
+
if (merged) result[provider] = merged
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return Object.keys(result).length === 0 ? undefined : result
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class HttpOptions extends Schema.Class<HttpOptions>("LLM.HttpOptions")({
|
|
54
|
+
body: Schema.optional(JsonSchema),
|
|
55
|
+
headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
56
|
+
query: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
57
|
+
}) {}
|
|
58
|
+
|
|
59
|
+
export namespace HttpOptions {
|
|
60
|
+
export type Input = HttpOptions | ConstructorParameters<typeof HttpOptions>[0]
|
|
61
|
+
|
|
62
|
+
/** Normalize HTTP option input into the canonical `HttpOptions` class. */
|
|
63
|
+
export const make = (input: Input) => (input instanceof HttpOptions ? input : new HttpOptions(input))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const mergeHttpOptions = (...items: ReadonlyArray<HttpOptions | undefined>): HttpOptions | undefined => {
|
|
67
|
+
const body = mergeJsonRecords(...items.map((item) => item?.body))
|
|
68
|
+
const headers = mergeStringRecords(...items.map((item) => item?.headers))
|
|
69
|
+
const query = mergeStringRecords(...items.map((item) => item?.query))
|
|
70
|
+
if (!body && !headers && !query) return undefined
|
|
71
|
+
return new HttpOptions({ body, headers, query })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class GenerationOptions extends Schema.Class<GenerationOptions>("LLM.GenerationOptions")({
|
|
75
|
+
maxTokens: Schema.optional(Schema.Number),
|
|
76
|
+
temperature: Schema.optional(Schema.Number),
|
|
77
|
+
topP: Schema.optional(Schema.Number),
|
|
78
|
+
topK: Schema.optional(Schema.Number),
|
|
79
|
+
frequencyPenalty: Schema.optional(Schema.Number),
|
|
80
|
+
presencePenalty: Schema.optional(Schema.Number),
|
|
81
|
+
seed: Schema.optional(Schema.Number),
|
|
82
|
+
stop: Schema.optional(Schema.Array(Schema.String)),
|
|
83
|
+
}) {}
|
|
84
|
+
|
|
85
|
+
export namespace GenerationOptions {
|
|
86
|
+
export type Input = GenerationOptions | ConstructorParameters<typeof GenerationOptions>[0]
|
|
87
|
+
|
|
88
|
+
/** Normalize generation option input into the canonical `GenerationOptions` class. */
|
|
89
|
+
export const make = (input: Input = {}) => (input instanceof GenerationOptions ? input : new GenerationOptions(input))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type GenerationOptionsFields = {
|
|
93
|
+
readonly maxTokens?: number
|
|
94
|
+
readonly temperature?: number
|
|
95
|
+
readonly topP?: number
|
|
96
|
+
readonly topK?: number
|
|
97
|
+
readonly frequencyPenalty?: number
|
|
98
|
+
readonly presencePenalty?: number
|
|
99
|
+
readonly seed?: number
|
|
100
|
+
readonly stop?: ReadonlyArray<string>
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type GenerationOptionsInput = GenerationOptions | GenerationOptionsFields
|
|
104
|
+
|
|
105
|
+
const latestGeneration = <Key extends keyof GenerationOptionsFields>(
|
|
106
|
+
items: ReadonlyArray<GenerationOptionsInput | undefined>,
|
|
107
|
+
key: Key,
|
|
108
|
+
) => items.findLast((item) => item?.[key] !== undefined)?.[key]
|
|
109
|
+
|
|
110
|
+
export const mergeGenerationOptions = (...items: ReadonlyArray<GenerationOptionsInput | undefined>) => {
|
|
111
|
+
const result = new GenerationOptions({
|
|
112
|
+
maxTokens: latestGeneration(items, "maxTokens"),
|
|
113
|
+
temperature: latestGeneration(items, "temperature"),
|
|
114
|
+
topP: latestGeneration(items, "topP"),
|
|
115
|
+
topK: latestGeneration(items, "topK"),
|
|
116
|
+
frequencyPenalty: latestGeneration(items, "frequencyPenalty"),
|
|
117
|
+
presencePenalty: latestGeneration(items, "presencePenalty"),
|
|
118
|
+
seed: latestGeneration(items, "seed"),
|
|
119
|
+
stop: latestGeneration(items, "stop"),
|
|
120
|
+
})
|
|
121
|
+
return Object.values(result).some((value) => value !== undefined) ? result : undefined
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export class ModelLimits extends Schema.Class<ModelLimits>("LLM.ModelLimits")({
|
|
125
|
+
context: Schema.optional(Schema.Number),
|
|
126
|
+
output: Schema.optional(Schema.Number),
|
|
127
|
+
}) {}
|
|
128
|
+
|
|
129
|
+
export namespace ModelLimits {
|
|
130
|
+
export type Input = ModelLimits | ConstructorParameters<typeof ModelLimits>[0]
|
|
131
|
+
|
|
132
|
+
/** Normalize model limit input into the canonical `ModelLimits` class. */
|
|
133
|
+
export const make = (input: Input | undefined) =>
|
|
134
|
+
input instanceof ModelLimits ? input : new ModelLimits(input ?? {})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export class Model {
|
|
138
|
+
readonly id: ModelID
|
|
139
|
+
readonly provider: ProviderID
|
|
140
|
+
readonly route: AnyRoute
|
|
141
|
+
|
|
142
|
+
constructor(input: Model.ConstructorInput) {
|
|
143
|
+
this.id = input.id
|
|
144
|
+
this.provider = input.provider
|
|
145
|
+
this.route = input.route
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
static make(input: Model.Input) {
|
|
149
|
+
return new Model({
|
|
150
|
+
id: ModelID.make(input.id),
|
|
151
|
+
provider: ProviderID.make(input.provider),
|
|
152
|
+
route: input.route,
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
static input(model: Model): Model.ConstructorInput {
|
|
157
|
+
return {
|
|
158
|
+
id: model.id,
|
|
159
|
+
provider: model.provider,
|
|
160
|
+
route: model.route,
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
static update(model: Model, patch: Partial<Model.Input>) {
|
|
165
|
+
if (Object.keys(patch).length === 0) return model
|
|
166
|
+
return Model.make({
|
|
167
|
+
...Model.input(model),
|
|
168
|
+
...patch,
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export namespace Model {
|
|
174
|
+
export type ConstructorInput = {
|
|
175
|
+
readonly id: ModelID
|
|
176
|
+
readonly provider: ProviderID
|
|
177
|
+
readonly route: AnyRoute
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export type Input = Omit<ConstructorInput, "id" | "provider"> & {
|
|
181
|
+
readonly id: string | ModelID
|
|
182
|
+
readonly provider: string | ProviderID
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export type ModelInput = Model.Input
|
|
187
|
+
|
|
188
|
+
export const ModelSchema = Schema.declare((value): value is Model => value instanceof Model, { expected: "LLM.Model" })
|
|
189
|
+
|
|
190
|
+
export class CacheHint extends Schema.Class<CacheHint>("LLM.CacheHint")({
|
|
191
|
+
type: Schema.Literals(["ephemeral", "persistent"]),
|
|
192
|
+
ttlSeconds: Schema.optional(Schema.Number),
|
|
193
|
+
}) {}
|
|
194
|
+
|
|
195
|
+
// Auto-placement policy for prompt caching. The protocol-neutral lowering step
|
|
196
|
+
// reads this and injects `CacheHint`s at the configured boundaries; the
|
|
197
|
+
// per-protocol body builders then translate those hints into wire markers as
|
|
198
|
+
// usual. `"auto"` is the recommended default for agent loops — it places one
|
|
199
|
+
// breakpoint at the last tool definition, one at the last system part, and one
|
|
200
|
+
// at the latest user message. The combination of provider invalidation
|
|
201
|
+
// hierarchy (tools → system → messages) and Anthropic/Bedrock's 20-block
|
|
202
|
+
// lookback means three trailing breakpoints reliably cover the static prefix.
|
|
203
|
+
//
|
|
204
|
+
// Pass `"none"` to opt out entirely (the legacy behavior). Pass the granular
|
|
205
|
+
// object form to override individual choices.
|
|
206
|
+
export const CachePolicyObject = Schema.Struct({
|
|
207
|
+
tools: Schema.optional(Schema.Boolean),
|
|
208
|
+
system: Schema.optional(Schema.Boolean),
|
|
209
|
+
messages: Schema.optional(
|
|
210
|
+
Schema.Union([
|
|
211
|
+
Schema.Literal("latest-user-message"),
|
|
212
|
+
Schema.Literal("latest-assistant"),
|
|
213
|
+
Schema.Struct({ tail: Schema.Number }),
|
|
214
|
+
]),
|
|
215
|
+
),
|
|
216
|
+
ttlSeconds: Schema.optional(Schema.Number),
|
|
217
|
+
})
|
|
218
|
+
export type CachePolicyObject = Schema.Schema.Type<typeof CachePolicyObject>
|
|
219
|
+
|
|
220
|
+
export const CachePolicy = Schema.Union([Schema.Literal("auto"), Schema.Literal("none"), CachePolicyObject])
|
|
221
|
+
export type CachePolicy = Schema.Schema.Type<typeof CachePolicy>
|