@neurocode-ai/llm 1.18.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/AGENTS.md +321 -0
  2. package/DESIGN.md +1114 -0
  3. package/README.md +131 -0
  4. package/example/call-sites.md +591 -0
  5. package/example/tutorial.ts +255 -0
  6. package/package.json +51 -0
  7. package/script/recording-cost-report.ts +250 -0
  8. package/script/setup-recording-env.ts +542 -0
  9. package/src/cache-policy.ts +111 -0
  10. package/src/index.ts +33 -0
  11. package/src/llm.ts +186 -0
  12. package/src/protocols/anthropic-messages.ts +855 -0
  13. package/src/protocols/bedrock-converse.ts +674 -0
  14. package/src/protocols/bedrock-event-stream.ts +87 -0
  15. package/src/protocols/gemini.ts +512 -0
  16. package/src/protocols/index.ts +6 -0
  17. package/src/protocols/openai-chat.ts +506 -0
  18. package/src/protocols/openai-compatible-chat.ts +24 -0
  19. package/src/protocols/openai-responses.ts +1022 -0
  20. package/src/protocols/shared.ts +326 -0
  21. package/src/protocols/utils/bedrock-auth.ts +70 -0
  22. package/src/protocols/utils/bedrock-cache.ts +37 -0
  23. package/src/protocols/utils/bedrock-media.ts +90 -0
  24. package/src/protocols/utils/cache.ts +16 -0
  25. package/src/protocols/utils/gemini-tool-schema.ts +99 -0
  26. package/src/protocols/utils/lifecycle.ts +102 -0
  27. package/src/protocols/utils/openai-options.ts +93 -0
  28. package/src/protocols/utils/tool-schema.ts +86 -0
  29. package/src/protocols/utils/tool-stream.ts +218 -0
  30. package/src/provider-error.ts +43 -0
  31. package/src/provider.ts +36 -0
  32. package/src/providers/amazon-bedrock.ts +43 -0
  33. package/src/providers/anthropic.ts +35 -0
  34. package/src/providers/azure.ts +110 -0
  35. package/src/providers/cloudflare.ts +127 -0
  36. package/src/providers/github-copilot.ts +69 -0
  37. package/src/providers/google.ts +35 -0
  38. package/src/providers/index.ts +11 -0
  39. package/src/providers/openai-compatible-profile.ts +20 -0
  40. package/src/providers/openai-compatible.ts +65 -0
  41. package/src/providers/openai-options.ts +83 -0
  42. package/src/providers/openai.ts +63 -0
  43. package/src/providers/openrouter.ts +98 -0
  44. package/src/providers/xai.ts +56 -0
  45. package/src/route/auth-options.ts +57 -0
  46. package/src/route/auth.ts +156 -0
  47. package/src/route/client.ts +436 -0
  48. package/src/route/endpoint.ts +53 -0
  49. package/src/route/executor.ts +385 -0
  50. package/src/route/framing.ts +27 -0
  51. package/src/route/index.ts +25 -0
  52. package/src/route/protocol.ts +84 -0
  53. package/src/route/transport/http.ts +155 -0
  54. package/src/route/transport/index.ts +33 -0
  55. package/src/route/transport/websocket.ts +280 -0
  56. package/src/schema/errors.ts +207 -0
  57. package/src/schema/events.ts +618 -0
  58. package/src/schema/ids.ts +43 -0
  59. package/src/schema/index.ts +5 -0
  60. package/src/schema/messages.ts +312 -0
  61. package/src/schema/options.ts +276 -0
  62. package/src/tool-runtime.ts +78 -0
  63. package/src/tool.ts +253 -0
  64. package/src/utils/record.ts +3 -0
  65. package/sst-env.d.ts +10 -0
  66. package/test/adapter.test.ts +171 -0
  67. package/test/auth-options.types.ts +168 -0
  68. package/test/auth.test.ts +103 -0
  69. package/test/cache-policy.test.ts +262 -0
  70. package/test/continuation-scenarios.ts +104 -0
  71. package/test/endpoint.test.ts +58 -0
  72. package/test/executor.test.ts +458 -0
  73. package/test/exports.test.ts +76 -0
  74. package/test/fixtures/media/restroom.png +0 -0
  75. package/test/fixtures/recordings/anthropic-messages/accepts-malformed-assistant-tool-order-with-default-patch.json +29 -0
  76. package/test/fixtures/recordings/anthropic-messages/anthropic-opus-4-7-image-tool-result.json +43 -0
  77. package/test/fixtures/recordings/anthropic-messages/claude-opus-4-7-drives-a-tool-loop.json +56 -0
  78. package/test/fixtures/recordings/anthropic-messages/rejects-malformed-assistant-tool-order-without-patch.json +29 -0
  79. package/test/fixtures/recordings/anthropic-messages/streams-text.json +29 -0
  80. package/test/fixtures/recordings/anthropic-messages/streams-tool-call.json +29 -0
  81. package/test/fixtures/recordings/anthropic-messages-cache/writes-then-reads-cache-control-on-identical-second-call.json +48 -0
  82. package/test/fixtures/recordings/bedrock-converse/drives-a-tool-loop.json +55 -0
  83. package/test/fixtures/recordings/bedrock-converse/streams-a-tool-call.json +29 -0
  84. package/test/fixtures/recordings/bedrock-converse/streams-text.json +29 -0
  85. package/test/fixtures/recordings/cloudflare-ai-gateway/cloudflare-ai-gateway-workers-ai-gpt-oss-20b-tools-tool-call.json +32 -0
  86. package/test/fixtures/recordings/cloudflare-ai-gateway/cloudflare-ai-gateway-workers-ai-llama-3-1-8b-text.json +32 -0
  87. package/test/fixtures/recordings/cloudflare-workers-ai/cloudflare-workers-ai-gpt-oss-20b-tools-tool-call.json +32 -0
  88. package/test/fixtures/recordings/cloudflare-workers-ai/cloudflare-workers-ai-llama-3-1-8b-text.json +32 -0
  89. package/test/fixtures/recordings/gemini/gemini-2-5-flash-image.json +32 -0
  90. package/test/fixtures/recordings/gemini/streams-text.json +28 -0
  91. package/test/fixtures/recordings/gemini/streams-tool-call.json +28 -0
  92. package/test/fixtures/recordings/gemini-cache/reports-cachedcontenttokencount-on-identical-second-call.json +46 -0
  93. package/test/fixtures/recordings/openai-chat/continues-after-tool-result.json +28 -0
  94. package/test/fixtures/recordings/openai-chat/drives-a-tool-loop-end-to-end.json +46 -0
  95. package/test/fixtures/recordings/openai-chat/streams-text.json +28 -0
  96. package/test/fixtures/recordings/openai-chat/streams-tool-call.json +28 -0
  97. package/test/fixtures/recordings/openai-compatible-chat/deepseek-streams-text.json +28 -0
  98. package/test/fixtures/recordings/openai-compatible-chat/groq-llama-3-3-70b-drives-a-tool-loop.json +53 -0
  99. package/test/fixtures/recordings/openai-compatible-chat/groq-streams-text.json +28 -0
  100. package/test/fixtures/recordings/openai-compatible-chat/groq-streams-tool-call.json +28 -0
  101. package/test/fixtures/recordings/openai-compatible-chat/openrouter-claude-opus-4-7-drives-a-tool-loop.json +54 -0
  102. package/test/fixtures/recordings/openai-compatible-chat/openrouter-gpt-4o-mini-drives-a-tool-loop.json +53 -0
  103. package/test/fixtures/recordings/openai-compatible-chat/openrouter-gpt-5-5-drives-a-tool-loop.json +54 -0
  104. package/test/fixtures/recordings/openai-compatible-chat/openrouter-streams-text.json +28 -0
  105. package/test/fixtures/recordings/openai-compatible-chat/openrouter-streams-tool-call.json +28 -0
  106. package/test/fixtures/recordings/openai-compatible-chat/togetherai-streams-text.json +28 -0
  107. package/test/fixtures/recordings/openai-compatible-chat/togetherai-streams-tool-call.json +28 -0
  108. package/test/fixtures/recordings/openai-responses/gpt-5-5-drives-a-tool-loop.json +54 -0
  109. package/test/fixtures/recordings/openai-responses/gpt-5-5-streams-text.json +28 -0
  110. package/test/fixtures/recordings/openai-responses/gpt-5-5-streams-tool-call.json +28 -0
  111. package/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-image-tool-result.json +42 -0
  112. package/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning-continuation.json +58 -0
  113. package/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning.json +32 -0
  114. package/test/fixtures/recordings/openai-responses-cache/reports-cached-tokens-on-identical-second-call.json +46 -0
  115. package/test/generate-object.test.ts +184 -0
  116. package/test/lib/effect.ts +50 -0
  117. package/test/lib/http.ts +98 -0
  118. package/test/lib/openai-chunks.ts +27 -0
  119. package/test/lib/sse.ts +17 -0
  120. package/test/lib/tool-runtime.ts +146 -0
  121. package/test/llm.test.ts +199 -0
  122. package/test/prepare.test.ts +178 -0
  123. package/test/provider/anthropic-messages-cache.recorded.test.ts +53 -0
  124. package/test/provider/anthropic-messages.recorded.test.ts +45 -0
  125. package/test/provider/anthropic-messages.test.ts +895 -0
  126. package/test/provider/bedrock-converse-cache.recorded.test.ts +54 -0
  127. package/test/provider/bedrock-converse.test.ts +744 -0
  128. package/test/provider/cloudflare.test.ts +230 -0
  129. package/test/provider/gemini-cache.recorded.test.ts +48 -0
  130. package/test/provider/gemini.test.ts +584 -0
  131. package/test/provider/golden.recorded.test.ts +223 -0
  132. package/test/provider/openai-chat.test.ts +674 -0
  133. package/test/provider/openai-compatible-chat.test.ts +238 -0
  134. package/test/provider/openai-responses-cache.recorded.test.ts +46 -0
  135. package/test/provider/openai-responses.test.ts +1472 -0
  136. package/test/provider/openrouter.test.ts +56 -0
  137. package/test/provider-error.test.ts +30 -0
  138. package/test/provider.types.ts +41 -0
  139. package/test/recorded-golden.ts +97 -0
  140. package/test/recorded-runner.ts +100 -0
  141. package/test/recorded-scenarios.ts +531 -0
  142. package/test/recorded-test.ts +94 -0
  143. package/test/recorded-utils.ts +56 -0
  144. package/test/recorded-websocket.ts +26 -0
  145. package/test/response.test.ts +98 -0
  146. package/test/route.test.ts +43 -0
  147. package/test/schema.test.ts +86 -0
  148. package/test/tool-runtime.test.ts +818 -0
  149. package/test/tool-schema-projection.test.ts +117 -0
  150. package/test/tool-stream.test.ts +99 -0
  151. package/test/tool.types.ts +40 -0
  152. package/tsconfig.json +15 -0
@@ -0,0 +1,110 @@
1
+ import { Auth } from "../route/auth"
2
+ import { type AtLeastOne, type ProviderAuthOption } from "../route/auth-options"
3
+ import type { Route as RouteDef, RouteDefaultsInput } from "../route/client"
4
+ import { ProviderID, type ModelID } from "../schema"
5
+ import * as OpenAIChat from "../protocols/openai-chat"
6
+ import * as OpenAIResponses from "../protocols/openai-responses"
7
+ import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options"
8
+
9
+ export const id = ProviderID.make("azure")
10
+ const routeAuth = Auth.remove("authorization")
11
+
12
+ // Azure needs the customer's resource URL; supply either `resourceName`
13
+ // (helper builds the URL) or `baseURL` directly.
14
+ type AzureURL = AtLeastOne<{ readonly resourceName: string; readonly baseURL: string }>
15
+
16
+ export type ModelOptions = AzureURL &
17
+ RouteDefaultsInput &
18
+ ProviderAuthOption<"optional"> & {
19
+ readonly apiVersion?: string
20
+ readonly queryParams?: Record<string, string>
21
+ readonly useCompletionUrls?: boolean
22
+ readonly providerOptions?: OpenAIProviderOptionsInput
23
+ }
24
+ export type Config = ModelOptions
25
+
26
+ const resourceBaseURL = (resourceName: string) => `https://${resourceName.trim()}.openai.azure.com/openai/v1`
27
+
28
+ const responsesRoute = OpenAIResponses.route.with({
29
+ id: "azure-openai-responses",
30
+ provider: id,
31
+ auth: routeAuth,
32
+ endpoint: {
33
+ query: { "api-version": "v1" },
34
+ },
35
+ })
36
+
37
+ const chatRoute = OpenAIChat.route.with({
38
+ id: "azure-openai-chat",
39
+ provider: id,
40
+ auth: routeAuth,
41
+ endpoint: {
42
+ query: { "api-version": "v1" },
43
+ },
44
+ })
45
+
46
+ export const routes = [responsesRoute, chatRoute]
47
+
48
+ const defaults = (input: Config) => {
49
+ const {
50
+ apiKey: _,
51
+ apiVersion: _apiVersion,
52
+ resourceName: _resourceName,
53
+ useCompletionUrls: _useCompletionUrls,
54
+ baseURL: _baseURL,
55
+ queryParams: _queryParams,
56
+ ...rest
57
+ } = input
58
+ if ("auth" in rest) {
59
+ const { auth: _, ...withoutAuth } = rest
60
+ return withoutAuth
61
+ }
62
+ return rest
63
+ }
64
+
65
+ const auth = (input: Config) => {
66
+ if ("auth" in input && input.auth) return input.auth
67
+ return Auth.remove("authorization").andThen(
68
+ Auth.optional("apiKey" in input ? input.apiKey : undefined, "apiKey")
69
+ .orElse(Auth.config("AZURE_OPENAI_API_KEY"))
70
+ .pipe(Auth.header("api-key")),
71
+ )
72
+ }
73
+
74
+ const configuredRoute = <Body, Prepared>(route: RouteDef<Body, Prepared>, input: Config) =>
75
+ route.with({
76
+ auth: auth(input),
77
+ endpoint: {
78
+ // AtLeastOne guarantees at least one is set; baseURL wins if both are.
79
+ baseURL: input.baseURL ?? resourceBaseURL(input.resourceName!),
80
+ query: {
81
+ ...(input.apiVersion ? { "api-version": input.apiVersion } : {}),
82
+ ...input.queryParams,
83
+ },
84
+ },
85
+ })
86
+
87
+ export const configure = (input: Config) => {
88
+ const configuredResponsesRoute = configuredRoute(responsesRoute, input)
89
+ const configuredChatRoute = configuredRoute(chatRoute, input)
90
+ const modelDefaults = defaults(input)
91
+
92
+ const responses = (modelID: string | ModelID) =>
93
+ configuredResponsesRoute.with(withOpenAIOptions(modelID, modelDefaults)).model({ id: modelID })
94
+
95
+ const chat = (modelID: string | ModelID) =>
96
+ configuredChatRoute.with(withOpenAIOptions(modelID, modelDefaults)).model({ id: modelID })
97
+
98
+ return {
99
+ id,
100
+ model: (modelID: string | ModelID) => (input.useCompletionUrls === true ? chat(modelID) : responses(modelID)),
101
+ responses,
102
+ chat,
103
+ configure,
104
+ }
105
+ }
106
+
107
+ export const provider = {
108
+ id,
109
+ configure,
110
+ }
@@ -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,69 @@
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 (opencode, etc.) must
11
+ // supply `baseURL` explicitly.
12
+ export type ModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
13
+ ProviderAuthOption<"optional"> & {
14
+ readonly baseURL: string
15
+ readonly endpoint?: "chat" | "responses"
16
+ readonly providerOptions?: OpenAIProviderOptionsInput
17
+ }
18
+
19
+ export const shouldUseResponsesApi = (modelID: string | ModelID, endpoint?: ModelOptions["endpoint"]) => {
20
+ if (endpoint) return endpoint === "responses"
21
+ const model = String(modelID)
22
+ const match = /^gpt-(\d+)/.exec(model)
23
+ if (!match) return false
24
+ return Number(match[1]) >= 5 && !model.startsWith("gpt-5-mini")
25
+ }
26
+
27
+ export const routes = [OpenAIResponses.route, OpenAIChat.route]
28
+
29
+ const chatRoute = OpenAIChat.route.with({ provider: id })
30
+ const responsesRoute = OpenAIResponses.route.with({ provider: id })
31
+
32
+ const defaults = (options: ModelOptions) => {
33
+ const { apiKey: _, auth: _auth, baseURL: _baseURL, endpoint: _endpoint, ...rest } = options
34
+ return rest
35
+ }
36
+
37
+ const configuredResponsesRoute = (options: ModelOptions) =>
38
+ responsesRoute.with({
39
+ endpoint: { baseURL: options.baseURL },
40
+ auth: AuthOptions.bearer(options, []),
41
+ })
42
+
43
+ const configuredChatRoute = (options: ModelOptions) =>
44
+ chatRoute.with({
45
+ endpoint: { baseURL: options.baseURL },
46
+ auth: AuthOptions.bearer(options, []),
47
+ })
48
+
49
+ export const configure = (options: ModelOptions) => {
50
+ const responsesRoute = configuredResponsesRoute(options)
51
+ const chatRoute = configuredChatRoute(options)
52
+ const responses = (modelID: string | ModelID) =>
53
+ responsesRoute.with(withOpenAIOptions(modelID, defaults(options))).model({ id: modelID })
54
+ const chat = (modelID: string | ModelID) =>
55
+ chatRoute.with(withOpenAIOptions(modelID, defaults(options))).model({ id: modelID })
56
+ return {
57
+ id,
58
+ model: (modelID: string | ModelID) =>
59
+ shouldUseResponsesApi(modelID, options.endpoint) ? responses(modelID) : chat(modelID),
60
+ responses,
61
+ chat,
62
+ configure,
63
+ }
64
+ }
65
+
66
+ export const provider = {
67
+ id,
68
+ configure,
69
+ }
@@ -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,83 @@
1
+ import type { ProviderOptions, ReasoningEffort, TextVerbosity } from "../schema"
2
+ import { mergeProviderOptions } from "../schema"
3
+ import type { OpenAIResponseIncludable, OpenAIServiceTier } from "../protocols/utils/openai-options"
4
+
5
+ export type { OpenAIResponseIncludable, OpenAIServiceTier } 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
+ readonly serviceTier?: OpenAIServiceTier
19
+ }
20
+
21
+ export type OpenAIProviderOptionsInput = ProviderOptions & {
22
+ readonly openai?: OpenAIOptionsInput
23
+ }
24
+
25
+ const definedEntries = (input: Record<string, unknown>) =>
26
+ Object.entries(input).filter((entry) => entry[1] !== undefined)
27
+
28
+ const openAIProviderOptions = (options: OpenAIOptionsInput | undefined): ProviderOptions | undefined => {
29
+ const openai = Object.fromEntries(
30
+ definedEntries({
31
+ store: options?.store,
32
+ promptCacheKey: options?.promptCacheKey,
33
+ reasoningEffort: options?.reasoningEffort,
34
+ reasoningSummary: options?.reasoningSummary,
35
+ include: options?.include,
36
+ textVerbosity: options?.textVerbosity,
37
+ serviceTier: options?.serviceTier,
38
+ }),
39
+ )
40
+ if (Object.keys(openai).length === 0) return undefined
41
+ return { openai }
42
+ }
43
+
44
+ export const gpt5DefaultOptions = (
45
+ modelID: string,
46
+ options: { readonly textVerbosity?: boolean } = {},
47
+ ): ProviderOptions | undefined => {
48
+ const id = modelID.toLowerCase()
49
+ if (!id.includes("gpt-5") || id.includes("gpt-5-chat") || id.includes("gpt-5-pro")) return undefined
50
+ return openAIProviderOptions({
51
+ reasoningEffort: "medium",
52
+ reasoningSummary: "auto",
53
+ // GPT-5 reasoning models are configured stateless (`store: false`) by
54
+ // `openAIDefaultOptions` below, so the only way a follow-up turn can
55
+ // carry reasoning state is via the encrypted reasoning include. Without
56
+ // this, callers using the default model facade get reasoning summaries
57
+ // they cannot replay statelessly.
58
+ include: ["reasoning.encrypted_content"],
59
+ textVerbosity:
60
+ options.textVerbosity === true && id.includes("gpt-5.") && !id.includes("codex") && !id.includes("-chat")
61
+ ? "low"
62
+ : undefined,
63
+ })
64
+ }
65
+
66
+ export const openAIDefaultOptions = (
67
+ modelID: string,
68
+ options: { readonly textVerbosity?: boolean } = {},
69
+ ): ProviderOptions | undefined =>
70
+ mergeProviderOptions(openAIProviderOptions({ store: false }), gpt5DefaultOptions(modelID, options))
71
+
72
+ export const withOpenAIOptions = <Options extends { readonly providerOptions?: OpenAIProviderOptionsInput }>(
73
+ modelID: string,
74
+ options: Options,
75
+ defaults: { readonly textVerbosity?: boolean } = {},
76
+ ): Omit<Options, "providerOptions"> & { readonly providerOptions?: ProviderOptions } => {
77
+ return {
78
+ ...options,
79
+ providerOptions: mergeProviderOptions(openAIDefaultOptions(modelID, defaults), options.providerOptions),
80
+ }
81
+ }
82
+
83
+ 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