@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,542 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { NodeFileSystem } from "@effect/platform-node"
|
|
4
|
+
import * as path from "node:path"
|
|
5
|
+
import * as prompts from "@clack/prompts"
|
|
6
|
+
import { AwsV4Signer } from "aws4fetch"
|
|
7
|
+
import { Config, ConfigProvider, Effect, FileSystem, PlatformError, Redacted } from "effect"
|
|
8
|
+
import { FetchHttpClient, HttpClient, HttpClientRequest, type HttpClientResponse } from "effect/unstable/http"
|
|
9
|
+
import * as ProviderShared from "../src/protocols/shared"
|
|
10
|
+
import * as Cloudflare from "../src/providers/cloudflare"
|
|
11
|
+
|
|
12
|
+
type Provider = {
|
|
13
|
+
readonly id: string
|
|
14
|
+
readonly label: string
|
|
15
|
+
readonly tier: "core" | "canary" | "compatible" | "optional"
|
|
16
|
+
readonly note: string
|
|
17
|
+
readonly vars: ReadonlyArray<{
|
|
18
|
+
readonly name: string
|
|
19
|
+
readonly label?: string
|
|
20
|
+
readonly optional?: boolean
|
|
21
|
+
readonly secret?: boolean
|
|
22
|
+
}>
|
|
23
|
+
readonly validate?: (env: Env) => Effect.Effect<string | undefined, unknown, HttpClient.HttpClient>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type Env = Record<string, string>
|
|
27
|
+
|
|
28
|
+
const PROVIDERS: ReadonlyArray<Provider> = [
|
|
29
|
+
{
|
|
30
|
+
id: "openai",
|
|
31
|
+
label: "OpenAI",
|
|
32
|
+
tier: "core",
|
|
33
|
+
note: "Native OpenAI Chat / Responses recorded tests",
|
|
34
|
+
vars: [{ name: "OPENAI_API_KEY" }],
|
|
35
|
+
validate: (env) => validateBearer("https://api.openai.com/v1/models", Redacted.make(env.OPENAI_API_KEY)),
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "anthropic",
|
|
39
|
+
label: "Anthropic",
|
|
40
|
+
tier: "core",
|
|
41
|
+
note: "Native Anthropic Messages recorded tests",
|
|
42
|
+
vars: [{ name: "ANTHROPIC_API_KEY" }],
|
|
43
|
+
validate: (env) =>
|
|
44
|
+
HttpClientRequest.get("https://api.anthropic.com/v1/models").pipe(
|
|
45
|
+
HttpClientRequest.setHeaders({
|
|
46
|
+
"anthropic-version": "2023-06-01",
|
|
47
|
+
"x-api-key": Redacted.value(Redacted.make(env.ANTHROPIC_API_KEY)),
|
|
48
|
+
}),
|
|
49
|
+
executeRequest,
|
|
50
|
+
),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "google",
|
|
54
|
+
label: "Google Gemini",
|
|
55
|
+
tier: "core",
|
|
56
|
+
note: "Native Gemini recorded tests",
|
|
57
|
+
vars: [{ name: "GOOGLE_GENERATIVE_AI_API_KEY" }],
|
|
58
|
+
validate: (env) =>
|
|
59
|
+
HttpClientRequest.get(
|
|
60
|
+
`https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(env.GOOGLE_GENERATIVE_AI_API_KEY)}`,
|
|
61
|
+
).pipe(executeRequest),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "bedrock",
|
|
65
|
+
label: "Amazon Bedrock",
|
|
66
|
+
tier: "core",
|
|
67
|
+
note: "Native Bedrock Converse recorded tests",
|
|
68
|
+
vars: [
|
|
69
|
+
{ name: "AWS_ACCESS_KEY_ID" },
|
|
70
|
+
{ name: "AWS_SECRET_ACCESS_KEY" },
|
|
71
|
+
{ name: "AWS_SESSION_TOKEN", optional: true },
|
|
72
|
+
{ name: "BEDROCK_RECORDING_REGION", optional: true },
|
|
73
|
+
{ name: "BEDROCK_MODEL_ID", optional: true },
|
|
74
|
+
],
|
|
75
|
+
validate: (env) => validateBedrock(env),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "groq",
|
|
79
|
+
label: "Groq",
|
|
80
|
+
tier: "canary",
|
|
81
|
+
note: "Fast OpenAI-compatible canary for text/tool streaming",
|
|
82
|
+
vars: [{ name: "GROQ_API_KEY" }],
|
|
83
|
+
validate: (env) => validateBearer("https://api.groq.com/openai/v1/models", Redacted.make(env.GROQ_API_KEY)),
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "openrouter",
|
|
87
|
+
label: "OpenRouter",
|
|
88
|
+
tier: "canary",
|
|
89
|
+
note: "Router canary for OpenAI-compatible text/tool streaming",
|
|
90
|
+
vars: [{ name: "OPENROUTER_API_KEY" }],
|
|
91
|
+
validate: (env) =>
|
|
92
|
+
validateChat({
|
|
93
|
+
url: "https://openrouter.ai/api/v1/chat/completions",
|
|
94
|
+
token: Redacted.make(env.OPENROUTER_API_KEY),
|
|
95
|
+
model: "openai/gpt-4o-mini",
|
|
96
|
+
}),
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "xai",
|
|
100
|
+
label: "xAI",
|
|
101
|
+
tier: "canary",
|
|
102
|
+
note: "OpenAI-compatible xAI chat endpoint",
|
|
103
|
+
vars: [{ name: "XAI_API_KEY" }],
|
|
104
|
+
validate: (env) => validateBearer("https://api.x.ai/v1/models", Redacted.make(env.XAI_API_KEY)),
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "cloudflare-ai-gateway",
|
|
108
|
+
label: "Cloudflare AI Gateway",
|
|
109
|
+
tier: "canary",
|
|
110
|
+
note: "Cloudflare Unified/OpenAI-compatible gateway; supports provider/model ids like workers-ai/@cf/...",
|
|
111
|
+
vars: [
|
|
112
|
+
{ name: "CLOUDFLARE_ACCOUNT_ID", label: "Cloudflare account ID", secret: false },
|
|
113
|
+
{
|
|
114
|
+
name: "CLOUDFLARE_GATEWAY_ID",
|
|
115
|
+
label: "Cloudflare AI Gateway ID (defaults to default)",
|
|
116
|
+
optional: true,
|
|
117
|
+
secret: false,
|
|
118
|
+
},
|
|
119
|
+
{ name: "CLOUDFLARE_API_TOKEN", label: "Cloudflare AI Gateway token" },
|
|
120
|
+
],
|
|
121
|
+
validate: (env) =>
|
|
122
|
+
validateChat({
|
|
123
|
+
url: `${Cloudflare.aiGatewayBaseURL({
|
|
124
|
+
accountId: env.CLOUDFLARE_ACCOUNT_ID,
|
|
125
|
+
gatewayId: env.CLOUDFLARE_GATEWAY_ID || undefined,
|
|
126
|
+
})}/chat/completions`,
|
|
127
|
+
token: Redacted.make(envValue(env, Cloudflare.aiGatewayAuthEnvVars)),
|
|
128
|
+
tokenHeader: "cf-aig-authorization",
|
|
129
|
+
model: "workers-ai/@cf/meta/llama-3.1-8b-instruct",
|
|
130
|
+
}),
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
id: "cloudflare-workers-ai",
|
|
134
|
+
label: "Cloudflare Workers AI",
|
|
135
|
+
tier: "canary",
|
|
136
|
+
note: "Direct Workers AI OpenAI-compatible endpoint; supports model ids like @cf/meta/...",
|
|
137
|
+
vars: [
|
|
138
|
+
{ name: "CLOUDFLARE_ACCOUNT_ID", label: "Cloudflare account ID", secret: false },
|
|
139
|
+
{ name: "CLOUDFLARE_API_KEY", label: "Cloudflare Workers AI API token" },
|
|
140
|
+
],
|
|
141
|
+
validate: (env) =>
|
|
142
|
+
validateChat({
|
|
143
|
+
url: `${Cloudflare.workersAIBaseURL({ accountId: env.CLOUDFLARE_ACCOUNT_ID })}/chat/completions`,
|
|
144
|
+
token: Redacted.make(envValue(env, Cloudflare.workersAIAuthEnvVars)),
|
|
145
|
+
model: "@cf/meta/llama-3.1-8b-instruct",
|
|
146
|
+
}),
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: "deepseek",
|
|
150
|
+
label: "DeepSeek",
|
|
151
|
+
tier: "compatible",
|
|
152
|
+
note: "Existing OpenAI-compatible recorded tests",
|
|
153
|
+
vars: [{ name: "DEEPSEEK_API_KEY" }],
|
|
154
|
+
validate: (env) => validateBearer("https://api.deepseek.com/models", Redacted.make(env.DEEPSEEK_API_KEY)),
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: "togetherai",
|
|
158
|
+
label: "TogetherAI",
|
|
159
|
+
tier: "compatible",
|
|
160
|
+
note: "Existing OpenAI-compatible text/tool recorded tests",
|
|
161
|
+
vars: [{ name: "TOGETHER_AI_API_KEY" }],
|
|
162
|
+
validate: (env) => validateBearer("https://api.together.xyz/v1/models", Redacted.make(env.TOGETHER_AI_API_KEY)),
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: "mistral",
|
|
166
|
+
label: "Mistral",
|
|
167
|
+
tier: "optional",
|
|
168
|
+
note: "OpenAI-compatible bridge; native reasoning parity is follow-up work",
|
|
169
|
+
vars: [{ name: "MISTRAL_API_KEY" }],
|
|
170
|
+
validate: (env) => validateBearer("https://api.mistral.ai/v1/models", Redacted.make(env.MISTRAL_API_KEY)),
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: "perplexity",
|
|
174
|
+
label: "Perplexity",
|
|
175
|
+
tier: "optional",
|
|
176
|
+
note: "OpenAI-compatible bridge; citations/search metadata are follow-up work",
|
|
177
|
+
vars: [{ name: "PERPLEXITY_API_KEY" }],
|
|
178
|
+
validate: (env) => validateBearer("https://api.perplexity.ai/models", Redacted.make(env.PERPLEXITY_API_KEY)),
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: "venice",
|
|
182
|
+
label: "Venice",
|
|
183
|
+
tier: "optional",
|
|
184
|
+
note: "OpenAI-compatible bridge",
|
|
185
|
+
vars: [{ name: "VENICE_API_KEY" }],
|
|
186
|
+
validate: (env) => validateBearer("https://api.venice.ai/api/v1/models", Redacted.make(env.VENICE_API_KEY)),
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: "cerebras",
|
|
190
|
+
label: "Cerebras",
|
|
191
|
+
tier: "optional",
|
|
192
|
+
note: "OpenAI-compatible bridge",
|
|
193
|
+
vars: [{ name: "CEREBRAS_API_KEY" }],
|
|
194
|
+
validate: (env) => validateBearer("https://api.cerebras.ai/v1/models", Redacted.make(env.CEREBRAS_API_KEY)),
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: "deepinfra",
|
|
198
|
+
label: "DeepInfra",
|
|
199
|
+
tier: "optional",
|
|
200
|
+
note: "OpenAI-compatible bridge",
|
|
201
|
+
vars: [{ name: "DEEPINFRA_API_KEY" }],
|
|
202
|
+
validate: (env) =>
|
|
203
|
+
validateBearer("https://api.deepinfra.com/v1/openai/models", Redacted.make(env.DEEPINFRA_API_KEY)),
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: "fireworks",
|
|
207
|
+
label: "Fireworks",
|
|
208
|
+
tier: "optional",
|
|
209
|
+
note: "OpenAI-compatible bridge",
|
|
210
|
+
vars: [{ name: "FIREWORKS_API_KEY" }],
|
|
211
|
+
validate: (env) =>
|
|
212
|
+
validateBearer("https://api.fireworks.ai/inference/v1/models", Redacted.make(env.FIREWORKS_API_KEY)),
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: "baseten",
|
|
216
|
+
label: "Baseten",
|
|
217
|
+
tier: "optional",
|
|
218
|
+
note: "OpenAI-compatible bridge",
|
|
219
|
+
vars: [{ name: "BASETEN_API_KEY" }],
|
|
220
|
+
},
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
const args = process.argv.slice(2)
|
|
224
|
+
const hasFlag = (name: string) => args.includes(name)
|
|
225
|
+
const option = (name: string) => {
|
|
226
|
+
const index = args.indexOf(name)
|
|
227
|
+
if (index === -1) return undefined
|
|
228
|
+
return args[index + 1]
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const envPath = path.resolve(process.cwd(), option("--env") ?? ".env.local")
|
|
232
|
+
const checkOnly = hasFlag("--check")
|
|
233
|
+
const providerOption = option("--providers")
|
|
234
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY)
|
|
235
|
+
|
|
236
|
+
const envNames = Array.from(new Set(PROVIDERS.flatMap((provider) => provider.vars.map((item) => item.name))))
|
|
237
|
+
|
|
238
|
+
const providersForOption = (value: string | undefined) => {
|
|
239
|
+
if (!value || value === "recommended")
|
|
240
|
+
return PROVIDERS.filter((provider) => provider.tier === "core" || provider.tier === "canary")
|
|
241
|
+
if (value === "recorded") return PROVIDERS.filter((provider) => provider.tier !== "optional")
|
|
242
|
+
if (value === "all") return PROVIDERS
|
|
243
|
+
const ids = new Set(
|
|
244
|
+
value
|
|
245
|
+
.split(",")
|
|
246
|
+
.map((item) => item.trim())
|
|
247
|
+
.filter(Boolean),
|
|
248
|
+
)
|
|
249
|
+
return PROVIDERS.filter((provider) => ids.has(provider.id))
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const chooseProviders = async () => {
|
|
253
|
+
if (providerOption) return providersForOption(providerOption)
|
|
254
|
+
return providersForOption("recommended")
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const catchMissingFile = (error: PlatformError.PlatformError) => {
|
|
258
|
+
if (error.reason._tag === "NotFound") return Effect.succeed("")
|
|
259
|
+
return Effect.fail(error)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const readEnvFile = Effect.fn("RecordingEnv.readFile")(function* () {
|
|
263
|
+
const fileSystem = yield* FileSystem.FileSystem
|
|
264
|
+
return yield* fileSystem.readFileString(envPath).pipe(Effect.catch(catchMissingFile))
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
const readConfigString = (provider: ConfigProvider.ConfigProvider, name: string) =>
|
|
268
|
+
Config.string(name)
|
|
269
|
+
.parse(provider)
|
|
270
|
+
.pipe(
|
|
271
|
+
Effect.match({
|
|
272
|
+
onFailure: () => undefined,
|
|
273
|
+
onSuccess: (value) => value,
|
|
274
|
+
}),
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
const parseEnv = Effect.fn("RecordingEnv.parseEnv")(function* (contents: string) {
|
|
278
|
+
const provider = ConfigProvider.fromDotEnvContents(contents)
|
|
279
|
+
return Object.fromEntries(
|
|
280
|
+
(yield* Effect.forEach(envNames, (name) =>
|
|
281
|
+
readConfigString(provider, name).pipe(Effect.map((value) => [name, value] as const)),
|
|
282
|
+
)).filter((entry): entry is readonly [string, string] => entry[1] !== undefined),
|
|
283
|
+
)
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
const quote = (value: string) => JSON.stringify(value)
|
|
287
|
+
|
|
288
|
+
const status = (name: string, fileEnv: Env) => {
|
|
289
|
+
if (fileEnv[name]) return "file"
|
|
290
|
+
if (process.env[name]) return "shell"
|
|
291
|
+
return "missing"
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const statusLine = (provider: Provider, fileEnv: Env) =>
|
|
295
|
+
[
|
|
296
|
+
`${provider.label} (${provider.tier})`,
|
|
297
|
+
provider.note,
|
|
298
|
+
...provider.vars.map((item) => {
|
|
299
|
+
const value = status(item.name, fileEnv)
|
|
300
|
+
const suffix = item.optional ? " optional" : ""
|
|
301
|
+
return ` ${value === "missing" ? "missing" : "set"} ${item.name}${suffix}${value === "shell" ? " (shell only)" : ""}`
|
|
302
|
+
}),
|
|
303
|
+
].join("\n")
|
|
304
|
+
|
|
305
|
+
const printStatus = (providers: ReadonlyArray<Provider>, fileEnv: Env) => {
|
|
306
|
+
prompts.note(providers.map((provider) => statusLine(provider, fileEnv)).join("\n\n"), `Recording env: ${envPath}`)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const exitIfCancel = <A>(value: A | symbol): A => {
|
|
310
|
+
if (!prompts.isCancel(value)) return value as A
|
|
311
|
+
prompts.cancel("Cancelled")
|
|
312
|
+
process.exit(130)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const upsertEnv = (contents: string, values: Env) => {
|
|
316
|
+
const names = Object.keys(values)
|
|
317
|
+
const seen = new Set<string>()
|
|
318
|
+
const lines = contents.split(/\r?\n/).map((line) => {
|
|
319
|
+
const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/)
|
|
320
|
+
if (!match || !names.includes(match[1])) return line
|
|
321
|
+
seen.add(match[1])
|
|
322
|
+
return `${match[1]}=${quote(values[match[1]])}`
|
|
323
|
+
})
|
|
324
|
+
const missing = names.filter((name) => !seen.has(name))
|
|
325
|
+
if (missing.length === 0) return lines.join("\n").replace(/\n*$/, "\n")
|
|
326
|
+
const prefix = lines.join("\n").trimEnd()
|
|
327
|
+
const block = [
|
|
328
|
+
"",
|
|
329
|
+
"# Added by bun run setup:recording-env",
|
|
330
|
+
...missing.map((name) => `${name}=${quote(values[name])}`),
|
|
331
|
+
].join("\n")
|
|
332
|
+
return `${prefix}${block}\n`
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const providerRequiredStatus = (provider: Provider, fileEnv: Env) => {
|
|
336
|
+
const required = requiredVars(provider)
|
|
337
|
+
if (required.some((item) => status(item.name, fileEnv) === "missing")) return "missing"
|
|
338
|
+
if (required.some((item) => status(item.name, fileEnv) === "shell")) return "set in shell"
|
|
339
|
+
return "already added"
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const requiredVars = (provider: Provider) => provider.vars.filter((item) => !item.optional)
|
|
343
|
+
|
|
344
|
+
const promptVars = (provider: Provider) => provider.vars.filter((item) => !item.optional || item.secret === false)
|
|
345
|
+
|
|
346
|
+
const processEnv = (): Env =>
|
|
347
|
+
Object.fromEntries(Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined))
|
|
348
|
+
|
|
349
|
+
const envValue = (env: Env, names: ReadonlyArray<string>) => names.map((name) => env[name]).find(Boolean) ?? ""
|
|
350
|
+
|
|
351
|
+
const envWithValues = (fileEnv: Env, values: Env): Env => ({
|
|
352
|
+
...processEnv(),
|
|
353
|
+
...fileEnv,
|
|
354
|
+
...values,
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
const responseError = Effect.fn("RecordingEnv.responseError")(function* (
|
|
358
|
+
response: HttpClientResponse.HttpClientResponse,
|
|
359
|
+
) {
|
|
360
|
+
if (response.status >= 200 && response.status < 300) return undefined
|
|
361
|
+
const body = yield* response.text.pipe(Effect.catch(() => Effect.succeed("")))
|
|
362
|
+
return `${response.status}${body ? `: ${body.slice(0, 180)}` : ""}`
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
const executeRequest = Effect.fn("RecordingEnv.executeRequest")(function* (
|
|
366
|
+
request: HttpClientRequest.HttpClientRequest,
|
|
367
|
+
) {
|
|
368
|
+
const http = yield* HttpClient.HttpClient
|
|
369
|
+
return yield* http.execute(request).pipe(Effect.flatMap(responseError))
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
const validateBearer = (url: string, token: Redacted.Redacted<string>, headers: Record<string, string> = {}) =>
|
|
373
|
+
HttpClientRequest.get(url).pipe(
|
|
374
|
+
HttpClientRequest.setHeaders({ ...headers, authorization: `Bearer ${Redacted.value(token)}` }),
|
|
375
|
+
executeRequest,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
const validateChat = (input: {
|
|
379
|
+
readonly url: string
|
|
380
|
+
readonly token: Redacted.Redacted<string>
|
|
381
|
+
readonly tokenHeader?: string
|
|
382
|
+
readonly model: string
|
|
383
|
+
readonly headers?: Record<string, string>
|
|
384
|
+
}) =>
|
|
385
|
+
ProviderShared.jsonPost({
|
|
386
|
+
url: input.url,
|
|
387
|
+
headers: { ...input.headers, [input.tokenHeader ?? "authorization"]: `Bearer ${Redacted.value(input.token)}` },
|
|
388
|
+
body: ProviderShared.encodeJson({
|
|
389
|
+
model: input.model,
|
|
390
|
+
messages: [{ role: "user", content: "Reply with exactly: ok" }],
|
|
391
|
+
max_tokens: 3,
|
|
392
|
+
temperature: 0,
|
|
393
|
+
}),
|
|
394
|
+
}).pipe(executeRequest)
|
|
395
|
+
|
|
396
|
+
const validateBedrock = (env: Env) =>
|
|
397
|
+
Effect.gen(function* () {
|
|
398
|
+
const request = yield* Effect.promise(() =>
|
|
399
|
+
new AwsV4Signer({
|
|
400
|
+
url: `https://bedrock.${env.BEDROCK_RECORDING_REGION || "us-east-1"}.amazonaws.com/foundation-models`,
|
|
401
|
+
method: "GET",
|
|
402
|
+
service: "bedrock",
|
|
403
|
+
region: env.BEDROCK_RECORDING_REGION || "us-east-1",
|
|
404
|
+
accessKeyId: env.AWS_ACCESS_KEY_ID,
|
|
405
|
+
secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
|
|
406
|
+
sessionToken: env.AWS_SESSION_TOKEN || undefined,
|
|
407
|
+
}).sign(),
|
|
408
|
+
)
|
|
409
|
+
return yield* HttpClientRequest.get(request.url.toString()).pipe(
|
|
410
|
+
HttpClientRequest.setHeaders(Object.fromEntries(request.headers.entries())),
|
|
411
|
+
executeRequest,
|
|
412
|
+
)
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
const validateProvider = Effect.fn("RecordingEnv.validateProvider")(function* (provider: Provider, env: Env) {
|
|
416
|
+
return yield* (provider.validate?.(env) ?? Effect.succeed("no lightweight validator")).pipe(
|
|
417
|
+
Effect.catch((error) => {
|
|
418
|
+
if (error instanceof Error) return Effect.succeed(error.message)
|
|
419
|
+
return Effect.succeed(String(error))
|
|
420
|
+
}),
|
|
421
|
+
)
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
const validateProviders = Effect.fn("RecordingEnv.validateProviders")(function* (
|
|
425
|
+
providers: ReadonlyArray<Provider>,
|
|
426
|
+
env: Env,
|
|
427
|
+
) {
|
|
428
|
+
const spinner = prompts.spinner()
|
|
429
|
+
spinner.start("Validating credentials")
|
|
430
|
+
const results = yield* Effect.forEach(
|
|
431
|
+
providers,
|
|
432
|
+
(provider) => validateProvider(provider, env).pipe(Effect.map((error) => ({ provider, error }))),
|
|
433
|
+
{ concurrency: 4 },
|
|
434
|
+
)
|
|
435
|
+
spinner.stop("Validation complete")
|
|
436
|
+
prompts.note(
|
|
437
|
+
results
|
|
438
|
+
.map(
|
|
439
|
+
(result) =>
|
|
440
|
+
`${result.error ? "failed" : "ok"} ${result.provider.label}${result.error ? ` - ${result.error}` : ""}`,
|
|
441
|
+
)
|
|
442
|
+
.join("\n"),
|
|
443
|
+
"Credential validation",
|
|
444
|
+
)
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
const writeEnvFile = Effect.fn("RecordingEnv.writeFile")(function* (contents: string) {
|
|
448
|
+
const fileSystem = yield* FileSystem.FileSystem
|
|
449
|
+
yield* fileSystem.makeDirectory(path.dirname(envPath), { recursive: true })
|
|
450
|
+
yield* fileSystem.writeFileString(envPath, contents, { mode: 0o600 })
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
const prompt = <A>(run: () => Promise<A | symbol>) => Effect.promise(run).pipe(Effect.map(exitIfCancel))
|
|
454
|
+
|
|
455
|
+
const chooseConfigurableProviders = Effect.fn("RecordingEnv.chooseConfigurableProviders")(function* (
|
|
456
|
+
providers: ReadonlyArray<Provider>,
|
|
457
|
+
fileEnv: Env,
|
|
458
|
+
) {
|
|
459
|
+
const configurable = providers.filter((provider) => requiredVars(provider).length > 0)
|
|
460
|
+
const selected = yield* prompt<ReadonlyArray<string>>(() =>
|
|
461
|
+
prompts.multiselect({
|
|
462
|
+
message: "Select provider credentials to add or override",
|
|
463
|
+
options: configurable.map((provider) => ({
|
|
464
|
+
value: provider.id,
|
|
465
|
+
label: provider.label,
|
|
466
|
+
hint: `${providerRequiredStatus(provider, fileEnv)} - ${requiredVars(provider)
|
|
467
|
+
.map((item) => item.name)
|
|
468
|
+
.join(", ")}`,
|
|
469
|
+
})),
|
|
470
|
+
initialValues: configurable
|
|
471
|
+
.filter((provider) => providerRequiredStatus(provider, fileEnv) === "missing")
|
|
472
|
+
.map((provider) => provider.id),
|
|
473
|
+
}),
|
|
474
|
+
)
|
|
475
|
+
return configurable.filter((provider) => selected.includes(provider.id))
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
const promptEnvVar = (item: Provider["vars"][number]) =>
|
|
479
|
+
prompt<string>(() => {
|
|
480
|
+
const input = {
|
|
481
|
+
message: item.label ?? item.name,
|
|
482
|
+
validate: (input: string | undefined) => {
|
|
483
|
+
if (item.optional) return undefined
|
|
484
|
+
return !input || input.length === 0 ? "Leave blank by pressing Esc/cancel, or paste a value" : undefined
|
|
485
|
+
},
|
|
486
|
+
}
|
|
487
|
+
return item.secret === false ? prompts.text(input) : prompts.password(input)
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
const promptProviderValues = Effect.fn("RecordingEnv.promptProviderValues")(function* (
|
|
491
|
+
providers: ReadonlyArray<Provider>,
|
|
492
|
+
) {
|
|
493
|
+
const values: Env = {}
|
|
494
|
+
for (const provider of providers) {
|
|
495
|
+
prompts.log.info(`${provider.label}: ${provider.note}`)
|
|
496
|
+
for (const item of promptVars(provider)) {
|
|
497
|
+
if (values[item.name]) continue
|
|
498
|
+
const value = yield* promptEnvVar(item)
|
|
499
|
+
if (value !== "") values[item.name] = value
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return values
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
const main = Effect.fn("RecordingEnv.main")(function* () {
|
|
506
|
+
prompts.intro("LLM recording credentials")
|
|
507
|
+
const contents = yield* readEnvFile()
|
|
508
|
+
const fileEnv = yield* parseEnv(contents)
|
|
509
|
+
const providers = yield* Effect.promise(() => chooseProviders())
|
|
510
|
+
printStatus(providers, fileEnv)
|
|
511
|
+
if (checkOnly) {
|
|
512
|
+
prompts.outro("Check complete")
|
|
513
|
+
return
|
|
514
|
+
}
|
|
515
|
+
if (!interactive) {
|
|
516
|
+
prompts.outro("Run this command in a terminal to enter credentials")
|
|
517
|
+
return
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const selectedProviders = yield* chooseConfigurableProviders(providers, fileEnv)
|
|
521
|
+
const values = yield* promptProviderValues(selectedProviders)
|
|
522
|
+
|
|
523
|
+
if (Object.keys(values).length === 0) {
|
|
524
|
+
prompts.outro("No changes")
|
|
525
|
+
return
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (
|
|
529
|
+
interactive &&
|
|
530
|
+
(yield* prompt(() => prompts.confirm({ message: "Validate credentials before saving?", initialValue: true })))
|
|
531
|
+
) {
|
|
532
|
+
yield* validateProviders(selectedProviders, envWithValues(fileEnv, values))
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
yield* writeEnvFile(upsertEnv(contents, values))
|
|
536
|
+
prompts.log.success(
|
|
537
|
+
`Saved ${Object.keys(values).length} value${Object.keys(values).length === 1 ? "" : "s"} to ${envPath}`,
|
|
538
|
+
)
|
|
539
|
+
prompts.outro("Keep .env.local local. Store shared team credentials in a password manager or vault.")
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
await Effect.runPromise(main().pipe(Effect.provide(NodeFileSystem.layer), Effect.provide(FetchHttpClient.layer)))
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Apply an `LLMRequest.cache` policy by injecting `CacheHint`s onto the parts
|
|
2
|
+
// the policy designates. Runs once at compile time, before the per-protocol
|
|
3
|
+
// body builder, so the existing inline-hint lowering path handles the rest.
|
|
4
|
+
//
|
|
5
|
+
// The default `"auto"` shape places one breakpoint at the last tool definition,
|
|
6
|
+
// one at the last system part, and one at the latest user message. This
|
|
7
|
+
// matches what production agent harnesses (LangChain's caching middleware,
|
|
8
|
+
// kern-ai's 10x cost-reduction playbook) converge on for tool-use loops: the
|
|
9
|
+
// latest user message stays put while a single turn explodes into many
|
|
10
|
+
// assistant/tool round-trips, so caching at that boundary lets every
|
|
11
|
+
// intra-turn API call hit the prefix.
|
|
12
|
+
//
|
|
13
|
+
// Manual `cache: CacheHint` placements on individual parts are preserved —
|
|
14
|
+
// this function only fills gaps the caller left empty.
|
|
15
|
+
import { CacheHint, type CachePolicy, type CachePolicyObject } from "./schema/options"
|
|
16
|
+
import { LLMRequest, Message, ToolDefinition, type ContentPart } from "./schema/messages"
|
|
17
|
+
|
|
18
|
+
const AUTO: CachePolicyObject = {
|
|
19
|
+
tools: true,
|
|
20
|
+
system: true,
|
|
21
|
+
messages: "latest-user-message",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const NONE: CachePolicyObject = {}
|
|
25
|
+
|
|
26
|
+
// Resolution rules:
|
|
27
|
+
// - undefined → "auto" — caching is on by default. The math favors it:
|
|
28
|
+
// Anthropic 5m-cache write is 1.25x base, read is 0.1x,
|
|
29
|
+
// so a single reuse within 5 minutes already wins.
|
|
30
|
+
// - "auto" → tools + system + latest user msg.
|
|
31
|
+
// - "none" → no auto placement; manual `CacheHint`s still flow.
|
|
32
|
+
// - object form → exactly what the caller asked for.
|
|
33
|
+
const resolve = (policy: CachePolicy | undefined): CachePolicyObject => {
|
|
34
|
+
if (policy === undefined || policy === "auto") return AUTO
|
|
35
|
+
if (policy === "none") return NONE
|
|
36
|
+
return policy
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Protocols whose wire format ignores inline cache markers (OpenAI's implicit
|
|
40
|
+
// prefix caching, Gemini's implicit + out-of-band CachedContent). Skip the
|
|
41
|
+
// whole policy pass for these — emitting hints would be harmless but pointless.
|
|
42
|
+
const RESPECTS_INLINE_HINTS = new Set(["anthropic-messages", "bedrock-converse"])
|
|
43
|
+
|
|
44
|
+
const makeHint = (ttlSeconds: number | undefined): CacheHint =>
|
|
45
|
+
ttlSeconds !== undefined ? new CacheHint({ type: "ephemeral", ttlSeconds }) : new CacheHint({ type: "ephemeral" })
|
|
46
|
+
|
|
47
|
+
const markLastTool = (tools: ReadonlyArray<ToolDefinition>, hint: CacheHint): ReadonlyArray<ToolDefinition> => {
|
|
48
|
+
if (tools.length === 0) return tools
|
|
49
|
+
const last = tools.length - 1
|
|
50
|
+
if (tools[last]!.cache) return tools
|
|
51
|
+
return tools.map((tool, i) => (i === last ? new ToolDefinition({ ...tool, cache: hint }) : tool))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const markLastSystem = (system: LLMRequest["system"], hint: CacheHint): LLMRequest["system"] => {
|
|
55
|
+
if (system.length === 0) return system
|
|
56
|
+
const last = system.length - 1
|
|
57
|
+
if (system[last]!.cache) return system
|
|
58
|
+
return system.map((part, i) => (i === last ? { ...part, cache: hint } : part))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const lastIndexOfRole = (messages: ReadonlyArray<Message>, role: Message["role"]): number =>
|
|
62
|
+
messages.findLastIndex((m) => m.role === role)
|
|
63
|
+
|
|
64
|
+
// Mark the last text part of `messages[index]`. If no text part exists, mark
|
|
65
|
+
// the last content part regardless of type — that's the breakpoint position
|
|
66
|
+
// in tool-result-only messages too.
|
|
67
|
+
const markMessageAt = (messages: ReadonlyArray<Message>, index: number, hint: CacheHint): ReadonlyArray<Message> => {
|
|
68
|
+
if (index < 0 || index >= messages.length) return messages
|
|
69
|
+
const target = messages[index]!
|
|
70
|
+
if (target.content.length === 0) return messages
|
|
71
|
+
const lastTextIndex = target.content.findLastIndex((part) => part.type === "text")
|
|
72
|
+
const markAt = lastTextIndex >= 0 ? lastTextIndex : target.content.length - 1
|
|
73
|
+
const existing = target.content[markAt]!
|
|
74
|
+
if ("cache" in existing && existing.cache) return messages
|
|
75
|
+
const nextContent = target.content.map((part, i) => (i === markAt ? ({ ...part, cache: hint } as ContentPart) : part))
|
|
76
|
+
const next = new Message({ ...target, content: nextContent })
|
|
77
|
+
// Single pass over `messages`, substituting the one updated entry. Long
|
|
78
|
+
// conversations call this on every request, so avoid `.map()` here — its
|
|
79
|
+
// closure dispatch and identity copies show up in profiling.
|
|
80
|
+
const result = messages.slice()
|
|
81
|
+
result[index] = next
|
|
82
|
+
return result
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const markMessages = (
|
|
86
|
+
messages: ReadonlyArray<Message>,
|
|
87
|
+
strategy: NonNullable<CachePolicyObject["messages"]>,
|
|
88
|
+
hint: CacheHint,
|
|
89
|
+
): ReadonlyArray<Message> => {
|
|
90
|
+
if (messages.length === 0) return messages
|
|
91
|
+
if (strategy === "latest-user-message") return markMessageAt(messages, lastIndexOfRole(messages, "user"), hint)
|
|
92
|
+
if (strategy === "latest-assistant") return markMessageAt(messages, lastIndexOfRole(messages, "assistant"), hint)
|
|
93
|
+
const start = Math.max(0, messages.length - strategy.tail)
|
|
94
|
+
let next = messages
|
|
95
|
+
for (let i = start; i < messages.length; i++) next = markMessageAt(next, i, hint)
|
|
96
|
+
return next
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const applyCachePolicy = (request: LLMRequest): LLMRequest => {
|
|
100
|
+
if (!RESPECTS_INLINE_HINTS.has(request.model.route.id)) return request
|
|
101
|
+
const policy = resolve(request.cache)
|
|
102
|
+
if (!policy.tools && !policy.system && !policy.messages) return request
|
|
103
|
+
|
|
104
|
+
const hint = makeHint(policy.ttlSeconds)
|
|
105
|
+
const tools = policy.tools ? markLastTool(request.tools, hint) : request.tools
|
|
106
|
+
const system = policy.system ? markLastSystem(request.system, hint) : request.system
|
|
107
|
+
const messages = policy.messages ? markMessages(request.messages, policy.messages, hint) : request.messages
|
|
108
|
+
|
|
109
|
+
if (tools === request.tools && system === request.system && messages === request.messages) return request
|
|
110
|
+
return LLMRequest.update(request, { tools, system, messages })
|
|
111
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export { LLMClient } from "./route/client"
|
|
2
|
+
export { Auth } from "./route/auth"
|
|
3
|
+
export { Provider } from "./provider"
|
|
4
|
+
export type {
|
|
5
|
+
RouteModelInput,
|
|
6
|
+
RouteRoutedModelInput,
|
|
7
|
+
Interface as LLMClientShape,
|
|
8
|
+
Service as LLMClientService,
|
|
9
|
+
} from "./route/client"
|
|
10
|
+
export * from "./schema"
|
|
11
|
+
export { Tool, ToolFailure, toDefinitions } from "./tool"
|
|
12
|
+
export { ToolRuntime } from "./tool-runtime"
|
|
13
|
+
export type { DispatchResult as ToolDispatchResult, ToolSettlement } from "./tool-runtime"
|
|
14
|
+
export type {
|
|
15
|
+
AnyExecutableTool,
|
|
16
|
+
AnyTool,
|
|
17
|
+
ExecutableTool,
|
|
18
|
+
ExecutableTools,
|
|
19
|
+
Tool as ToolShape,
|
|
20
|
+
ToolExecute,
|
|
21
|
+
ToolExecuteContext,
|
|
22
|
+
ToolModelOutputInput,
|
|
23
|
+
Tools,
|
|
24
|
+
ToolSchema,
|
|
25
|
+
ToolToModelOutput,
|
|
26
|
+
} from "./tool"
|
|
27
|
+
export * as LLM from "./llm"
|
|
28
|
+
export type {
|
|
29
|
+
Definition as ProviderDefinition,
|
|
30
|
+
ModelFactory as ProviderModelFactory,
|
|
31
|
+
ModelOptions as ProviderModelOptions,
|
|
32
|
+
} from "./provider"
|