@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,127 @@
|
|
|
1
|
+
import type { Config, Redacted } from "effect"
|
|
2
|
+
import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat"
|
|
3
|
+
import { Auth } from "../route/auth"
|
|
4
|
+
import { AuthOptions, type AtLeastOne, type ProviderAuthOption } from "../route/auth-options"
|
|
5
|
+
import type { RouteDefaultsInput } from "../route/client"
|
|
6
|
+
import { ProviderID, type ModelID } from "../schema"
|
|
7
|
+
|
|
8
|
+
export const aiGatewayID = ProviderID.make("cloudflare-ai-gateway")
|
|
9
|
+
export const workersAIID = ProviderID.make("cloudflare-workers-ai")
|
|
10
|
+
export const aiGatewayAuthEnvVars = ["CLOUDFLARE_API_TOKEN", "CF_AIG_TOKEN"] as const
|
|
11
|
+
export const workersAIAuthEnvVars = ["CLOUDFLARE_API_KEY", "CLOUDFLARE_WORKERS_AI_TOKEN"] as const
|
|
12
|
+
|
|
13
|
+
type CloudflareSecret = string | Redacted.Redacted | Config.Config<string | Redacted.Redacted>
|
|
14
|
+
|
|
15
|
+
type GatewayURL = AtLeastOne<{
|
|
16
|
+
readonly accountId: string
|
|
17
|
+
readonly baseURL: string
|
|
18
|
+
}> & {
|
|
19
|
+
readonly gatewayId?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type AIGatewayOptions = GatewayURL &
|
|
23
|
+
RouteDefaultsInput &
|
|
24
|
+
ProviderAuthOption<"optional"> & {
|
|
25
|
+
/** Cloudflare AI Gateway authentication token. Sent as `cf-aig-authorization`. */
|
|
26
|
+
readonly gatewayApiKey?: CloudflareSecret
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type WorkersAIURL = AtLeastOne<{
|
|
30
|
+
readonly accountId: string
|
|
31
|
+
readonly baseURL: string
|
|
32
|
+
}>
|
|
33
|
+
|
|
34
|
+
export type WorkersAIOptions = WorkersAIURL & RouteDefaultsInput & ProviderAuthOption<"optional">
|
|
35
|
+
|
|
36
|
+
export const aiGatewayBaseURL = (input: GatewayURL) => {
|
|
37
|
+
if (input.baseURL) return input.baseURL
|
|
38
|
+
if (!input.accountId) throw new Error("CloudflareAIGateway.configure requires accountId unless baseURL is supplied")
|
|
39
|
+
return `https://gateway.ai.cloudflare.com/v1/${encodeURIComponent(input.accountId)}/${encodeURIComponent(input.gatewayId?.trim() || "default")}/compat`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const aiGatewayAuth = (input: AIGatewayOptions) => {
|
|
43
|
+
if ("auth" in input && input.auth) return input.auth
|
|
44
|
+
const gateway = Auth.optional(input.gatewayApiKey, "gatewayApiKey")
|
|
45
|
+
.orElse(Auth.config("CLOUDFLARE_API_TOKEN"))
|
|
46
|
+
.orElse(Auth.config("CF_AIG_TOKEN"))
|
|
47
|
+
.pipe(Auth.bearerHeader("cf-aig-authorization"))
|
|
48
|
+
if (!("apiKey" in input) || input.apiKey === undefined) return gateway
|
|
49
|
+
if (input.gatewayApiKey === undefined) return Auth.bearer(input.apiKey)
|
|
50
|
+
return Auth.bearerHeader("cf-aig-authorization", input.gatewayApiKey).andThen(Auth.bearer(input.apiKey))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const workersAIBaseURL = (input: WorkersAIURL) => {
|
|
54
|
+
if (input.baseURL) return input.baseURL
|
|
55
|
+
if (!input.accountId) throw new Error("CloudflareWorkersAI.configure requires accountId unless baseURL is supplied")
|
|
56
|
+
return `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(input.accountId)}/ai/v1`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const workersAIAuth = (input: WorkersAIOptions) => {
|
|
60
|
+
return AuthOptions.bearer(input, workersAIAuthEnvVars)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const aiGatewayRoute = OpenAICompatibleChat.route.with({
|
|
64
|
+
id: "cloudflare-ai-gateway",
|
|
65
|
+
provider: aiGatewayID,
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
export const workersAIRoute = OpenAICompatibleChat.route.with({
|
|
69
|
+
id: "cloudflare-workers-ai",
|
|
70
|
+
provider: workersAIID,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
export const routes = [aiGatewayRoute, workersAIRoute]
|
|
74
|
+
|
|
75
|
+
const aiGatewayDefaults = (options: AIGatewayOptions) => {
|
|
76
|
+
const {
|
|
77
|
+
accountId: _accountId,
|
|
78
|
+
gatewayId: _gatewayId,
|
|
79
|
+
apiKey: _apiKey,
|
|
80
|
+
gatewayApiKey: _gatewayApiKey,
|
|
81
|
+
baseURL: _baseURL,
|
|
82
|
+
auth: _auth,
|
|
83
|
+
...rest
|
|
84
|
+
} = options
|
|
85
|
+
return rest
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const workersAIDefaults = (options: WorkersAIOptions) => {
|
|
89
|
+
const { accountId: _accountId, apiKey: _apiKey, auth: _auth, baseURL: _baseURL, ...rest } = options
|
|
90
|
+
return rest
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const configureAIGateway = (options: AIGatewayOptions) => {
|
|
94
|
+
const route = aiGatewayRoute.with({
|
|
95
|
+
...aiGatewayDefaults(options),
|
|
96
|
+
endpoint: { baseURL: aiGatewayBaseURL(options) },
|
|
97
|
+
auth: aiGatewayAuth(options),
|
|
98
|
+
})
|
|
99
|
+
return {
|
|
100
|
+
id: aiGatewayID,
|
|
101
|
+
model: (modelID: string | ModelID) => route.model({ id: modelID }),
|
|
102
|
+
configure: configureAIGateway,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const configureWorkersAI = (options: WorkersAIOptions) => {
|
|
107
|
+
const route = workersAIRoute.with({
|
|
108
|
+
...workersAIDefaults(options),
|
|
109
|
+
endpoint: { baseURL: workersAIBaseURL(options) },
|
|
110
|
+
auth: workersAIAuth(options),
|
|
111
|
+
})
|
|
112
|
+
return {
|
|
113
|
+
id: workersAIID,
|
|
114
|
+
model: (modelID: string | ModelID) => route.model({ id: modelID }),
|
|
115
|
+
configure: configureWorkersAI,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const CloudflareAIGateway = {
|
|
120
|
+
id: aiGatewayID,
|
|
121
|
+
configure: configureAIGateway,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const CloudflareWorkersAI = {
|
|
125
|
+
id: workersAIID,
|
|
126
|
+
configure: configureWorkersAI,
|
|
127
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
|
|
2
|
+
import type { RouteDefaultsInput } from "../route/client"
|
|
3
|
+
import { ProviderID, type ModelID } from "../schema"
|
|
4
|
+
import * as OpenAIChat from "../protocols/openai-chat"
|
|
5
|
+
import * as OpenAIResponses from "../protocols/openai-responses"
|
|
6
|
+
import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options"
|
|
7
|
+
|
|
8
|
+
export const id = ProviderID.make("github-copilot")
|
|
9
|
+
|
|
10
|
+
// GitHub Copilot has no canonical public URL — callers (Codilore, etc.) must
|
|
11
|
+
// supply `baseURL` explicitly.
|
|
12
|
+
export type ModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
|
|
13
|
+
ProviderAuthOption<"optional"> & {
|
|
14
|
+
readonly baseURL: string
|
|
15
|
+
readonly providerOptions?: OpenAIProviderOptionsInput
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const shouldUseResponsesApi = (modelID: string | ModelID) => {
|
|
19
|
+
const model = String(modelID)
|
|
20
|
+
const match = /^gpt-(\d+)/.exec(model)
|
|
21
|
+
if (!match) return false
|
|
22
|
+
return Number(match[1]) >= 5 && !model.startsWith("gpt-5-mini")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const routes = [OpenAIResponses.route, OpenAIChat.route]
|
|
26
|
+
|
|
27
|
+
const chatRoute = OpenAIChat.route.with({ provider: id })
|
|
28
|
+
const responsesRoute = OpenAIResponses.route.with({ provider: id })
|
|
29
|
+
|
|
30
|
+
const defaults = (options: ModelOptions) => {
|
|
31
|
+
const { apiKey: _, auth: _auth, baseURL: _baseURL, ...rest } = options
|
|
32
|
+
return rest
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const configuredResponsesRoute = (options: ModelOptions) =>
|
|
36
|
+
responsesRoute.with({
|
|
37
|
+
endpoint: { baseURL: options.baseURL },
|
|
38
|
+
auth: AuthOptions.bearer(options, []),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const configuredChatRoute = (options: ModelOptions) =>
|
|
42
|
+
chatRoute.with({
|
|
43
|
+
endpoint: { baseURL: options.baseURL },
|
|
44
|
+
auth: AuthOptions.bearer(options, []),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
export const configure = (options: ModelOptions) => {
|
|
48
|
+
const responsesRoute = configuredResponsesRoute(options)
|
|
49
|
+
const chatRoute = configuredChatRoute(options)
|
|
50
|
+
const responses = (modelID: string | ModelID) =>
|
|
51
|
+
responsesRoute.with(withOpenAIOptions(modelID, defaults(options))).model({ id: modelID })
|
|
52
|
+
const chat = (modelID: string | ModelID) =>
|
|
53
|
+
chatRoute.with(withOpenAIOptions(modelID, defaults(options))).model({ id: modelID })
|
|
54
|
+
return {
|
|
55
|
+
id,
|
|
56
|
+
model: (modelID: string | ModelID) => (shouldUseResponsesApi(modelID) ? responses(modelID) : chat(modelID)),
|
|
57
|
+
responses,
|
|
58
|
+
chat,
|
|
59
|
+
configure,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const provider = {
|
|
64
|
+
id,
|
|
65
|
+
configure,
|
|
66
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { RouteDefaultsInput } from "../route/client"
|
|
2
|
+
import { Auth } from "../route/auth"
|
|
3
|
+
import type { ProviderAuthOption } from "../route/auth-options"
|
|
4
|
+
import { ProviderID, type ModelID } from "../schema"
|
|
5
|
+
import * as Gemini from "../protocols/gemini"
|
|
6
|
+
|
|
7
|
+
export const id = ProviderID.make("google")
|
|
8
|
+
|
|
9
|
+
export const routes = [Gemini.route]
|
|
10
|
+
|
|
11
|
+
export type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string }
|
|
12
|
+
|
|
13
|
+
const auth = (options: ProviderAuthOption<"optional">) => {
|
|
14
|
+
if ("auth" in options && options.auth) return options.auth
|
|
15
|
+
return Auth.optional("apiKey" in options ? options.apiKey : undefined, "apiKey")
|
|
16
|
+
.orElse(Auth.config("GOOGLE_GENERATIVE_AI_API_KEY"))
|
|
17
|
+
.pipe(Auth.header("x-goog-api-key"))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const configuredRoute = (input: Config) => {
|
|
21
|
+
const { apiKey: _, auth: _auth, baseURL, ...rest } = input
|
|
22
|
+
return Gemini.route.with({ ...rest, endpoint: { baseURL }, auth: auth(input) })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const configure = (input: Config = {}) => {
|
|
26
|
+
const route = configuredRoute(input)
|
|
27
|
+
return {
|
|
28
|
+
id,
|
|
29
|
+
model: (modelID: string | ModelID) => route.model({ id: modelID }),
|
|
30
|
+
configure,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const provider = configure()
|
|
35
|
+
export const model = provider.model
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * as Anthropic from "./anthropic"
|
|
2
|
+
export * as AmazonBedrock from "./amazon-bedrock"
|
|
3
|
+
export * as Azure from "./azure"
|
|
4
|
+
export * as Cloudflare from "./cloudflare"
|
|
5
|
+
export { CloudflareAIGateway, CloudflareWorkersAI } from "./cloudflare"
|
|
6
|
+
export * as GitHubCopilot from "./github-copilot"
|
|
7
|
+
export * as Google from "./google"
|
|
8
|
+
export * as OpenAI from "./openai"
|
|
9
|
+
export * as OpenAICompatible from "./openai-compatible"
|
|
10
|
+
export * as OpenRouter from "./openrouter"
|
|
11
|
+
export * as XAI from "./xai"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface OpenAICompatibleProfile {
|
|
2
|
+
readonly provider: string
|
|
3
|
+
readonly baseURL: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export const profiles = {
|
|
7
|
+
baseten: { provider: "baseten", baseURL: "https://inference.baseten.co/v1" },
|
|
8
|
+
cerebras: { provider: "cerebras", baseURL: "https://api.cerebras.ai/v1" },
|
|
9
|
+
deepinfra: { provider: "deepinfra", baseURL: "https://api.deepinfra.com/v1/openai" },
|
|
10
|
+
deepseek: { provider: "deepseek", baseURL: "https://api.deepseek.com/v1" },
|
|
11
|
+
fireworks: { provider: "fireworks", baseURL: "https://api.fireworks.ai/inference/v1" },
|
|
12
|
+
groq: { provider: "groq", baseURL: "https://api.groq.com/openai/v1" },
|
|
13
|
+
openrouter: { provider: "openrouter", baseURL: "https://openrouter.ai/api/v1" },
|
|
14
|
+
togetherai: { provider: "togetherai", baseURL: "https://api.together.xyz/v1" },
|
|
15
|
+
xai: { provider: "xai", baseURL: "https://api.x.ai/v1" },
|
|
16
|
+
} as const satisfies Record<string, OpenAICompatibleProfile>
|
|
17
|
+
|
|
18
|
+
export const byProvider: Record<string, OpenAICompatibleProfile> = Object.fromEntries(
|
|
19
|
+
Object.values(profiles).map((profile) => [profile.provider, profile]),
|
|
20
|
+
)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { ProviderID, type ModelID } from "../schema"
|
|
2
|
+
import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat"
|
|
3
|
+
import type { RouteDefaultsInput } from "../route/client"
|
|
4
|
+
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
|
|
5
|
+
import { profiles, type OpenAICompatibleProfile } from "./openai-compatible-profile"
|
|
6
|
+
|
|
7
|
+
export const id = ProviderID.make("openai-compatible")
|
|
8
|
+
|
|
9
|
+
type GenericModelOptions = RouteDefaultsInput &
|
|
10
|
+
ProviderAuthOption<"optional"> & {
|
|
11
|
+
readonly provider?: string
|
|
12
|
+
readonly baseURL: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type FamilyModelOptions = RouteDefaultsInput &
|
|
16
|
+
ProviderAuthOption<"optional"> & {
|
|
17
|
+
readonly baseURL?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const routes = [OpenAICompatibleChat.route]
|
|
21
|
+
|
|
22
|
+
export const configure = (input: GenericModelOptions) => {
|
|
23
|
+
const provider = input.provider ?? "openai-compatible"
|
|
24
|
+
const { provider: _, baseURL, apiKey: _apiKey, auth: _auth, ...rest } = input
|
|
25
|
+
const route = OpenAICompatibleChat.route.with({
|
|
26
|
+
...rest,
|
|
27
|
+
provider,
|
|
28
|
+
endpoint: { baseURL },
|
|
29
|
+
auth: AuthOptions.bearer(input, []),
|
|
30
|
+
})
|
|
31
|
+
return {
|
|
32
|
+
id: ProviderID.make(provider),
|
|
33
|
+
model: (modelID: string | ModelID) => route.model({ id: modelID, provider: ProviderID.make(provider) }),
|
|
34
|
+
configure,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const define = (profile: OpenAICompatibleProfile) => {
|
|
39
|
+
const configureProfile = (input: FamilyModelOptions = {}) => {
|
|
40
|
+
const facade = configure({
|
|
41
|
+
...input,
|
|
42
|
+
baseURL: input.baseURL ?? profile.baseURL,
|
|
43
|
+
provider: profile.provider,
|
|
44
|
+
})
|
|
45
|
+
return {
|
|
46
|
+
id: ProviderID.make(profile.provider),
|
|
47
|
+
model: facade.model,
|
|
48
|
+
configure: configureProfile,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return configureProfile()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const provider = {
|
|
55
|
+
id,
|
|
56
|
+
configure,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const baseten = define(profiles.baseten)
|
|
60
|
+
export const cerebras = define(profiles.cerebras)
|
|
61
|
+
export const deepinfra = define(profiles.deepinfra)
|
|
62
|
+
export const deepseek = define(profiles.deepseek)
|
|
63
|
+
export const fireworks = define(profiles.fireworks)
|
|
64
|
+
export const groq = define(profiles.groq)
|
|
65
|
+
export const togetherai = define(profiles.togetherai)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { ProviderOptions, ReasoningEffort, TextVerbosity } from "../schema"
|
|
2
|
+
import { mergeProviderOptions } from "../schema"
|
|
3
|
+
import type { OpenAIResponseIncludable } from "../protocols/utils/openai-options"
|
|
4
|
+
|
|
5
|
+
export type { OpenAIResponseIncludable } from "../protocols/utils/openai-options"
|
|
6
|
+
|
|
7
|
+
export interface OpenAIOptionsInput {
|
|
8
|
+
readonly [key: string]: unknown
|
|
9
|
+
readonly store?: boolean
|
|
10
|
+
readonly promptCacheKey?: string
|
|
11
|
+
readonly reasoningEffort?: ReasoningEffort
|
|
12
|
+
readonly reasoningSummary?: "auto"
|
|
13
|
+
// OpenAI Responses `include` wire field. Mirrors the official SDK's
|
|
14
|
+
// `ResponseIncludable[]` union exactly so AI SDK callers and direct
|
|
15
|
+
// native-SDK callers share one shape and no translation is required.
|
|
16
|
+
readonly include?: ReadonlyArray<OpenAIResponseIncludable>
|
|
17
|
+
readonly textVerbosity?: TextVerbosity
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type OpenAIProviderOptionsInput = ProviderOptions & {
|
|
21
|
+
readonly openai?: OpenAIOptionsInput
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const definedEntries = (input: Record<string, unknown>) =>
|
|
25
|
+
Object.entries(input).filter((entry) => entry[1] !== undefined)
|
|
26
|
+
|
|
27
|
+
const openAIProviderOptions = (options: OpenAIOptionsInput | undefined): ProviderOptions | undefined => {
|
|
28
|
+
const openai = Object.fromEntries(
|
|
29
|
+
definedEntries({
|
|
30
|
+
store: options?.store,
|
|
31
|
+
promptCacheKey: options?.promptCacheKey,
|
|
32
|
+
reasoningEffort: options?.reasoningEffort,
|
|
33
|
+
reasoningSummary: options?.reasoningSummary,
|
|
34
|
+
include: options?.include,
|
|
35
|
+
textVerbosity: options?.textVerbosity,
|
|
36
|
+
}),
|
|
37
|
+
)
|
|
38
|
+
if (Object.keys(openai).length === 0) return undefined
|
|
39
|
+
return { openai }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const gpt5DefaultOptions = (
|
|
43
|
+
modelID: string,
|
|
44
|
+
options: { readonly textVerbosity?: boolean } = {},
|
|
45
|
+
): ProviderOptions | undefined => {
|
|
46
|
+
const id = modelID.toLowerCase()
|
|
47
|
+
if (!id.includes("gpt-5") || id.includes("gpt-5-chat") || id.includes("gpt-5-pro")) return undefined
|
|
48
|
+
return openAIProviderOptions({
|
|
49
|
+
reasoningEffort: "medium",
|
|
50
|
+
reasoningSummary: "auto",
|
|
51
|
+
// GPT-5 reasoning models are configured stateless (`store: false`) by
|
|
52
|
+
// `openAIDefaultOptions` below, so the only way a follow-up turn can
|
|
53
|
+
// carry reasoning state is via the encrypted reasoning include. Without
|
|
54
|
+
// this, callers using the default model facade get reasoning summaries
|
|
55
|
+
// they cannot replay statelessly.
|
|
56
|
+
include: ["reasoning.encrypted_content"],
|
|
57
|
+
textVerbosity:
|
|
58
|
+
options.textVerbosity === true && id.includes("gpt-5.") && !id.includes("codex") && !id.includes("-chat")
|
|
59
|
+
? "low"
|
|
60
|
+
: undefined,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const openAIDefaultOptions = (
|
|
65
|
+
modelID: string,
|
|
66
|
+
options: { readonly textVerbosity?: boolean } = {},
|
|
67
|
+
): ProviderOptions | undefined =>
|
|
68
|
+
mergeProviderOptions(openAIProviderOptions({ store: false }), gpt5DefaultOptions(modelID, options))
|
|
69
|
+
|
|
70
|
+
export const withOpenAIOptions = <Options extends { readonly providerOptions?: OpenAIProviderOptionsInput }>(
|
|
71
|
+
modelID: string,
|
|
72
|
+
options: Options,
|
|
73
|
+
defaults: { readonly textVerbosity?: boolean } = {},
|
|
74
|
+
): Omit<Options, "providerOptions"> & { readonly providerOptions?: ProviderOptions } => {
|
|
75
|
+
return {
|
|
76
|
+
...options,
|
|
77
|
+
providerOptions: mergeProviderOptions(openAIDefaultOptions(modelID, defaults), options.providerOptions),
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export * as OpenAIProviderOptions from "./openai-options"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
|
|
2
|
+
import type { Route, RouteDefaultsInput } from "../route/client"
|
|
3
|
+
import { ProviderID, type ModelID } from "../schema"
|
|
4
|
+
import * as OpenAIChat from "../protocols/openai-chat"
|
|
5
|
+
import * as OpenAIResponses from "../protocols/openai-responses"
|
|
6
|
+
import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options"
|
|
7
|
+
|
|
8
|
+
export type { OpenAIOptionsInput, OpenAIResponseIncludable } from "./openai-options"
|
|
9
|
+
|
|
10
|
+
export const id = ProviderID.make("openai")
|
|
11
|
+
|
|
12
|
+
export const routes = [OpenAIResponses.route, OpenAIResponses.webSocketRoute, OpenAIChat.route]
|
|
13
|
+
|
|
14
|
+
// This provider facade wraps the lower-level Responses and Chat model factories
|
|
15
|
+
// with OpenAI-specific conveniences: typed options, API-key sugar, env fallback,
|
|
16
|
+
// and default option normalization.
|
|
17
|
+
export type Config = RouteDefaultsInput &
|
|
18
|
+
ProviderAuthOption<"optional"> & {
|
|
19
|
+
readonly baseURL?: string
|
|
20
|
+
readonly queryParams?: Record<string, string>
|
|
21
|
+
readonly providerOptions?: OpenAIProviderOptionsInput
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const auth = (options: ProviderAuthOption<"optional">) => AuthOptions.bearer(options, "OPENAI_API_KEY")
|
|
25
|
+
|
|
26
|
+
const defaults = (input: Config) => {
|
|
27
|
+
const { apiKey: _, auth: _auth, baseURL: _baseURL, queryParams: _queryParams, ...rest } = input
|
|
28
|
+
return rest
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const configuredRoute = <Body, Prepared>(route: Route<Body, Prepared>, input: Config) =>
|
|
32
|
+
route.with({
|
|
33
|
+
auth: auth(input),
|
|
34
|
+
endpoint: { baseURL: input.baseURL, query: input.queryParams },
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
export const configure = (input: Config = {}) => {
|
|
38
|
+
const responsesRoute = configuredRoute(OpenAIResponses.route, input)
|
|
39
|
+
const responsesWebSocketRoute = configuredRoute(OpenAIResponses.webSocketRoute, input)
|
|
40
|
+
const chatRoute = configuredRoute(OpenAIChat.route, input)
|
|
41
|
+
const modelDefaults = defaults(input)
|
|
42
|
+
const responses = (id: string | ModelID) =>
|
|
43
|
+
responsesRoute.with(withOpenAIOptions(id, modelDefaults, { textVerbosity: true })).model({ id })
|
|
44
|
+
const responsesWebSocket = (id: string | ModelID) =>
|
|
45
|
+
responsesWebSocketRoute.with(withOpenAIOptions(id, modelDefaults, { textVerbosity: true })).model({ id })
|
|
46
|
+
const chat = (id: string | ModelID) => chatRoute.with(withOpenAIOptions(id, modelDefaults)).model({ id })
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
id,
|
|
50
|
+
model: responses,
|
|
51
|
+
responses,
|
|
52
|
+
responsesWebSocket,
|
|
53
|
+
chat,
|
|
54
|
+
configure,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const provider = configure()
|
|
59
|
+
|
|
60
|
+
export const model = provider.model
|
|
61
|
+
export const responses = provider.responses
|
|
62
|
+
export const responsesWebSocket = provider.responsesWebSocket
|
|
63
|
+
export const chat = provider.chat
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect"
|
|
2
|
+
import { Route, type RouteDefaultsInput } from "../route/client"
|
|
3
|
+
import { Endpoint } from "../route/endpoint"
|
|
4
|
+
import { Framing } from "../route/framing"
|
|
5
|
+
import { Protocol } from "../route/protocol"
|
|
6
|
+
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
|
|
7
|
+
import { ProviderID, type ModelID, type ProviderOptions } from "../schema"
|
|
8
|
+
import * as OpenAICompatibleProfiles from "./openai-compatible-profile"
|
|
9
|
+
import * as OpenAIChat from "../protocols/openai-chat"
|
|
10
|
+
import { isRecord } from "../protocols/shared"
|
|
11
|
+
|
|
12
|
+
export const profile = OpenAICompatibleProfiles.profiles.openrouter
|
|
13
|
+
export const id = ProviderID.make(profile.provider)
|
|
14
|
+
const ADAPTER = "openrouter"
|
|
15
|
+
|
|
16
|
+
export interface OpenRouterOptions {
|
|
17
|
+
readonly [key: string]: unknown
|
|
18
|
+
readonly usage?: boolean | Record<string, unknown>
|
|
19
|
+
readonly reasoning?: Record<string, unknown>
|
|
20
|
+
readonly promptCacheKey?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type OpenRouterProviderOptionsInput = ProviderOptions & {
|
|
24
|
+
readonly openrouter?: OpenRouterOptions
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type ModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
|
|
28
|
+
ProviderAuthOption<"optional"> & {
|
|
29
|
+
readonly baseURL?: string
|
|
30
|
+
readonly providerOptions?: OpenRouterProviderOptionsInput
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const OpenRouterBody = Schema.StructWithRest(Schema.Struct(OpenAIChat.bodyFields), [
|
|
34
|
+
Schema.Record(Schema.String, Schema.Any),
|
|
35
|
+
])
|
|
36
|
+
export type OpenRouterBody = Schema.Schema.Type<typeof OpenRouterBody>
|
|
37
|
+
|
|
38
|
+
export const protocol = Protocol.make({
|
|
39
|
+
id: "openrouter-chat",
|
|
40
|
+
body: {
|
|
41
|
+
schema: OpenRouterBody,
|
|
42
|
+
from: (request) =>
|
|
43
|
+
OpenAIChat.protocol.body.from(request).pipe(
|
|
44
|
+
Effect.map(
|
|
45
|
+
(body) =>
|
|
46
|
+
({
|
|
47
|
+
...body,
|
|
48
|
+
...bodyOptions(request.providerOptions?.openrouter),
|
|
49
|
+
}) as OpenRouterBody,
|
|
50
|
+
),
|
|
51
|
+
),
|
|
52
|
+
},
|
|
53
|
+
stream: OpenAIChat.protocol.stream,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const bodyOptions = (input: unknown) => {
|
|
57
|
+
const openrouter = isRecord(input) ? input : {}
|
|
58
|
+
return {
|
|
59
|
+
...(openrouter.usage === true
|
|
60
|
+
? { usage: { include: true } }
|
|
61
|
+
: isRecord(openrouter.usage)
|
|
62
|
+
? { usage: openrouter.usage }
|
|
63
|
+
: {}),
|
|
64
|
+
...(isRecord(openrouter.reasoning) ? { reasoning: openrouter.reasoning } : {}),
|
|
65
|
+
...(typeof openrouter.promptCacheKey === "string" ? { prompt_cache_key: openrouter.promptCacheKey } : {}),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const route = Route.make({
|
|
70
|
+
id: ADAPTER,
|
|
71
|
+
provider: profile.provider,
|
|
72
|
+
protocol,
|
|
73
|
+
endpoint: Endpoint.path("/chat/completions", { baseURL: profile.baseURL }),
|
|
74
|
+
framing: Framing.sse,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
export const routes = [route]
|
|
78
|
+
|
|
79
|
+
const configuredRoute = (input: ModelOptions) => {
|
|
80
|
+
const { apiKey: _, auth: _auth, baseURL, ...rest } = input
|
|
81
|
+
return route.with({
|
|
82
|
+
...rest,
|
|
83
|
+
endpoint: { baseURL: baseURL ?? profile.baseURL },
|
|
84
|
+
auth: AuthOptions.bearer(input, "OPENROUTER_API_KEY"),
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const configure = (input: ModelOptions = {}) => {
|
|
89
|
+
const route = configuredRoute(input)
|
|
90
|
+
return {
|
|
91
|
+
id,
|
|
92
|
+
model: (modelID: string | ModelID) => route.model({ id: modelID }),
|
|
93
|
+
configure,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const provider = configure()
|
|
98
|
+
export const model = provider.model
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
|
|
2
|
+
import type { RouteDefaultsInput } from "../route/client"
|
|
3
|
+
import { ProviderID, type ModelID } from "../schema"
|
|
4
|
+
import * as OpenAICompatibleProfiles from "./openai-compatible-profile"
|
|
5
|
+
import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat"
|
|
6
|
+
import * as OpenAIResponses from "../protocols/openai-responses"
|
|
7
|
+
|
|
8
|
+
export const id = ProviderID.make("xai")
|
|
9
|
+
|
|
10
|
+
export type ModelOptions = RouteDefaultsInput &
|
|
11
|
+
ProviderAuthOption<"optional"> & {
|
|
12
|
+
readonly baseURL?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const routes = [OpenAIResponses.route, OpenAICompatibleChat.route]
|
|
16
|
+
|
|
17
|
+
const auth = (options: ProviderAuthOption<"optional">) => AuthOptions.bearer(options, "XAI_API_KEY")
|
|
18
|
+
|
|
19
|
+
const configuredResponsesRoute = (input: ModelOptions) => {
|
|
20
|
+
const { apiKey: _, auth: _auth, baseURL, ...rest } = input
|
|
21
|
+
return OpenAIResponses.route.with({
|
|
22
|
+
...rest,
|
|
23
|
+
provider: id,
|
|
24
|
+
endpoint: { baseURL: baseURL ?? OpenAICompatibleProfiles.profiles.xai.baseURL },
|
|
25
|
+
auth: auth(input),
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const configuredChatRoute = (input: ModelOptions) => {
|
|
30
|
+
const { apiKey: _, auth: _auth, baseURL, ...rest } = input
|
|
31
|
+
return OpenAICompatibleChat.route.with({
|
|
32
|
+
...rest,
|
|
33
|
+
provider: id,
|
|
34
|
+
endpoint: { baseURL: baseURL ?? OpenAICompatibleProfiles.profiles.xai.baseURL },
|
|
35
|
+
auth: auth(input),
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const configure = (input: ModelOptions = {}) => {
|
|
40
|
+
const responsesRoute = configuredResponsesRoute(input)
|
|
41
|
+
const chatRoute = configuredChatRoute(input)
|
|
42
|
+
const responses = (modelID: string | ModelID) => responsesRoute.model({ id: modelID })
|
|
43
|
+
const chat = (modelID: string | ModelID) => chatRoute.model({ id: modelID })
|
|
44
|
+
return {
|
|
45
|
+
id,
|
|
46
|
+
model: responses,
|
|
47
|
+
responses,
|
|
48
|
+
chat,
|
|
49
|
+
configure,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const provider = configure()
|
|
54
|
+
export const model = provider.model
|
|
55
|
+
export const responses = provider.responses
|
|
56
|
+
export const chat = provider.chat
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Config, Redacted } from "effect"
|
|
2
|
+
import { Auth } from "./auth"
|
|
3
|
+
|
|
4
|
+
export type ApiKeyMode = "optional" | "required"
|
|
5
|
+
|
|
6
|
+
export type AuthOverride = {
|
|
7
|
+
readonly auth: Auth
|
|
8
|
+
readonly apiKey?: never
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type OptionalApiKeyAuth = {
|
|
12
|
+
readonly apiKey?: string | Redacted.Redacted<string> | Config.Config<string | Redacted.Redacted<string>>
|
|
13
|
+
readonly auth?: never
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type RequiredApiKeyAuth = {
|
|
17
|
+
readonly apiKey: string | Redacted.Redacted<string> | Config.Config<string | Redacted.Redacted<string>>
|
|
18
|
+
readonly auth?: never
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ProviderAuthOption<Mode extends ApiKeyMode> =
|
|
22
|
+
| AuthOverride
|
|
23
|
+
| (Mode extends "optional" ? OptionalApiKeyAuth : RequiredApiKeyAuth)
|
|
24
|
+
|
|
25
|
+
export type ModelOptions<Base, Mode extends ApiKeyMode> = Omit<Base, "apiKey" | "auth"> & ProviderAuthOption<Mode>
|
|
26
|
+
|
|
27
|
+
export type ModelArgs<Base, Mode extends ApiKeyMode> = Mode extends "optional"
|
|
28
|
+
? readonly [options?: ModelOptions<Base, Mode>]
|
|
29
|
+
: readonly [options: ModelOptions<Base, Mode>]
|
|
30
|
+
|
|
31
|
+
export type ModelFactory<Base, Mode extends ApiKeyMode, Model> = (id: string, ...args: ModelArgs<Base, Mode>) => Model
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Require at least one of the keys in `T`. Use for option shapes where any
|
|
35
|
+
* subset of fields is acceptable but at least one must be present (e.g. Azure
|
|
36
|
+
* accepts `resourceName` or `baseURL`).
|
|
37
|
+
*/
|
|
38
|
+
export type AtLeastOne<T> = {
|
|
39
|
+
[K in keyof T]: Required<Pick<T, K>> & Partial<Omit<T, K>>
|
|
40
|
+
}[keyof T]
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Standard bearer-auth resolution for providers: honor an explicit `auth`
|
|
44
|
+
* override, otherwise resolve `apiKey` (option > config var) and apply it as
|
|
45
|
+
* a bearer token.
|
|
46
|
+
*/
|
|
47
|
+
export const bearer = (options: ProviderAuthOption<"optional">, envVar: string | ReadonlyArray<string>): Auth => {
|
|
48
|
+
if ("auth" in options && options.auth) return options.auth
|
|
49
|
+
return (Array.isArray(envVar) ? envVar : [envVar])
|
|
50
|
+
.reduce(
|
|
51
|
+
(auth, name) => auth.orElse(Auth.config(name)),
|
|
52
|
+
Auth.optional("apiKey" in options ? options.apiKey : undefined, "apiKey"),
|
|
53
|
+
)
|
|
54
|
+
.bearer()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export * as AuthOptions from "./auth-options"
|