@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,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"
@@ -0,0 +1,156 @@
1
+ import { Config, Effect, Redacted } from "effect"
2
+ import { Headers } from "effect/unstable/http"
3
+ import { AuthenticationReason, InvalidRequestReason, LLMError, type LLMRequest } from "../schema"
4
+
5
+ export class MissingCredentialError extends Error {
6
+ readonly _tag = "MissingCredentialError"
7
+
8
+ constructor(readonly source: string) {
9
+ super(`Missing auth credential: ${source}`)
10
+ }
11
+ }
12
+
13
+ export type CredentialError = MissingCredentialError | Config.ConfigError
14
+ export type AuthError = CredentialError | LLMError
15
+ type Secret = string | Redacted.Redacted | Config.Config<string | Redacted.Redacted>
16
+
17
+ export interface AuthInput {
18
+ readonly request: LLMRequest
19
+ readonly method: "POST" | "GET"
20
+ readonly url: string
21
+ readonly body: string
22
+ readonly headers: Headers.Headers
23
+ }
24
+
25
+ export interface Credential {
26
+ readonly load: Effect.Effect<Redacted.Redacted, CredentialError>
27
+ readonly orElse: (that: Credential) => Credential
28
+ readonly bearer: () => Auth
29
+ readonly header: (name: string) => Auth
30
+ readonly pipe: <A>(f: (self: Credential) => A) => A
31
+ }
32
+
33
+ export interface Auth {
34
+ readonly apply: (input: AuthInput) => Effect.Effect<Headers.Headers, AuthError>
35
+ readonly andThen: (that: Auth) => Auth
36
+ readonly orElse: (that: Auth) => Auth
37
+ readonly pipe: <A>(f: (self: Auth) => A) => A
38
+ }
39
+
40
+ export const isAuth = (input: unknown): input is Auth =>
41
+ typeof input === "object" && input !== null && "apply" in input && typeof input.apply === "function"
42
+
43
+ const credential = (load: Effect.Effect<Redacted.Redacted, CredentialError>): Credential => {
44
+ const self: Credential = {
45
+ load,
46
+ orElse: (that) => credential(load.pipe(Effect.catch(() => that.load))),
47
+ bearer: () => fromCredential(self, (secret) => ({ authorization: `Bearer ${secret}` })),
48
+ header: (name) => fromCredential(self, (secret) => ({ [name]: secret })),
49
+ pipe: (f) => f(self),
50
+ }
51
+ return self
52
+ }
53
+
54
+ const auth = (apply: Auth["apply"]): Auth => {
55
+ const self: Auth = {
56
+ apply,
57
+ andThen: (that) =>
58
+ auth((input) => apply(input).pipe(Effect.flatMap((headers) => that.apply({ ...input, headers })))),
59
+ orElse: (that) => auth((input) => apply(input).pipe(Effect.catch(() => that.apply(input)))),
60
+ pipe: (f) => f(self),
61
+ }
62
+ return self
63
+ }
64
+
65
+ const fromCredential = (source: Credential, render: (secret: string) => Headers.Input) =>
66
+ auth((input) =>
67
+ source.load.pipe(Effect.map((secret) => Headers.setAll(input.headers, render(Redacted.value(secret))))),
68
+ )
69
+
70
+ const secretEffect = (secret: string | Redacted.Redacted, source: string) => {
71
+ const redacted = typeof secret === "string" ? Redacted.make(secret) : secret
72
+ if (Redacted.value(redacted) === "") return Effect.fail(new MissingCredentialError(source))
73
+ return Effect.succeed(redacted)
74
+ }
75
+
76
+ const credentialFromSecret = (secret: Secret, source: string) => {
77
+ if (typeof secret === "string" || Redacted.isRedacted(secret)) return credential(secretEffect(secret, source))
78
+ return credential(
79
+ Effect.gen(function* () {
80
+ return yield* secretEffect(yield* secret, source)
81
+ }),
82
+ )
83
+ }
84
+
85
+ export const value = (secret: string, source = "value") => credentialFromSecret(secret, source)
86
+
87
+ export const optional = (secret: Secret | undefined, source = "optional value") =>
88
+ secret === undefined
89
+ ? credential(Effect.fail(new MissingCredentialError(source)))
90
+ : credentialFromSecret(secret, source)
91
+
92
+ export const config = (name: string) => credentialFromSecret(Config.redacted(name), name)
93
+
94
+ export const effect = (load: Effect.Effect<Redacted.Redacted, CredentialError>) => credential(load)
95
+
96
+ export const none = auth((input) => Effect.succeed(input.headers))
97
+
98
+ export const headers = (input: Headers.Input) =>
99
+ auth((inputAuth) => Effect.succeed(Headers.setAll(inputAuth.headers, input)))
100
+
101
+ export const remove = (name: string) => auth((input) => Effect.succeed(Headers.remove(input.headers, name)))
102
+
103
+ export const custom = (apply: (input: AuthInput) => Effect.Effect<Headers.Headers, LLMError>) => auth(apply)
104
+
105
+ export const passthrough = none
106
+
107
+ const credentialInput = (source: Secret | Credential) =>
108
+ typeof source === "string" || Redacted.isRedacted(source) || Config.isConfig(source)
109
+ ? credentialFromSecret(source, "value")
110
+ : source
111
+
112
+ export function bearer(source: Secret | Credential): Auth
113
+ export function bearer(source: Secret | Credential) {
114
+ return credentialInput(source).bearer()
115
+ }
116
+
117
+ export const apiKey = bearer
118
+
119
+ export function header(name: string): (source: Secret | Credential) => Auth
120
+ export function header(name: string, source: Secret | Credential): Auth
121
+ export function header(name: string, source?: Secret | Credential) {
122
+ if (source === undefined) {
123
+ return (next: Secret | Credential) => credentialInput(next).header(name)
124
+ }
125
+ return credentialInput(source).header(name)
126
+ }
127
+
128
+ export function bearerHeader(name: string): (source: Secret | Credential) => Auth
129
+ export function bearerHeader(name: string, source: Secret | Credential): Auth
130
+ export function bearerHeader(name: string, source?: Secret | Credential) {
131
+ const render = (input: Secret | Credential) =>
132
+ fromCredential(credentialInput(input), (secret) => ({ [name]: `Bearer ${secret}` }))
133
+ if (source === undefined) return render
134
+ return render(source)
135
+ }
136
+
137
+ const toLLMError = (error: AuthError): LLMError => {
138
+ if (error instanceof MissingCredentialError || error instanceof Config.ConfigError) {
139
+ return new LLMError({
140
+ module: "Auth",
141
+ method: "apply",
142
+ reason:
143
+ error instanceof MissingCredentialError
144
+ ? new AuthenticationReason({ message: error.message, kind: "missing" })
145
+ : new InvalidRequestReason({ message: `Failed to resolve auth config: ${error.message}` }),
146
+ })
147
+ }
148
+ return error
149
+ }
150
+
151
+ export const toEffect =
152
+ (input: Auth) =>
153
+ (authInput: AuthInput): Effect.Effect<Headers.Headers, LLMError> =>
154
+ input.apply(authInput).pipe(Effect.mapError(toLLMError))
155
+
156
+ export * as Auth from "./auth"