@opencode-ai/ai 0.0.0-bootstrap.0 → 0.0.0-next-15618

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 (151) hide show
  1. package/README.md +176 -1
  2. package/dist/cache-policy.d.ts +2 -0
  3. package/dist/cache-policy.js +108 -0
  4. package/dist/index.d.ts +14 -0
  5. package/dist/index.js +9 -0
  6. package/dist/llm.d.ts +223 -0
  7. package/dist/llm.js +81 -0
  8. package/dist/protocols/anthropic-messages.d.ts +503 -0
  9. package/dist/protocols/anthropic-messages.js +745 -0
  10. package/dist/protocols/bedrock-converse.d.ts +476 -0
  11. package/dist/protocols/bedrock-converse.js +526 -0
  12. package/dist/protocols/bedrock-event-stream.d.ts +9 -0
  13. package/dist/protocols/bedrock-event-stream.js +64 -0
  14. package/dist/protocols/gemini.d.ts +220 -0
  15. package/dist/protocols/gemini.js +420 -0
  16. package/dist/protocols/google-vertex-anthropic.d.ts +494 -0
  17. package/dist/protocols/google-vertex-anthropic.js +33 -0
  18. package/dist/protocols/google-vertex-gemini.d.ts +59 -0
  19. package/dist/protocols/google-vertex-gemini.js +18 -0
  20. package/dist/protocols/index.d.ts +9 -0
  21. package/dist/protocols/index.js +9 -0
  22. package/dist/protocols/openai-chat.d.ts +391 -0
  23. package/dist/protocols/openai-chat.js +423 -0
  24. package/dist/protocols/openai-compatible-chat.d.ts +73 -0
  25. package/dist/protocols/openai-compatible-chat.js +20 -0
  26. package/dist/protocols/openai-compatible-responses.d.ts +85 -0
  27. package/dist/protocols/openai-compatible-responses.js +18 -0
  28. package/dist/protocols/openai-responses.d.ts +613 -0
  29. package/dist/protocols/openai-responses.js +828 -0
  30. package/dist/protocols/shared.d.ts +192 -0
  31. package/dist/protocols/shared.js +236 -0
  32. package/dist/protocols/utils/bedrock-auth.d.ts +18 -0
  33. package/dist/protocols/utils/bedrock-auth.js +41 -0
  34. package/dist/protocols/utils/bedrock-cache.d.ts +15 -0
  35. package/dist/protocols/utils/bedrock-cache.js +29 -0
  36. package/dist/protocols/utils/bedrock-media.d.ts +49 -0
  37. package/dist/protocols/utils/bedrock-media.js +68 -0
  38. package/dist/protocols/utils/cache.d.ts +6 -0
  39. package/dist/protocols/utils/cache.js +8 -0
  40. package/dist/protocols/utils/gemini-tool-schema.d.ts +2 -0
  41. package/dist/protocols/utils/gemini-tool-schema.js +83 -0
  42. package/dist/protocols/utils/lifecycle.d.ts +19 -0
  43. package/dist/protocols/utils/lifecycle.js +65 -0
  44. package/dist/protocols/utils/openai-options.d.ts +22 -0
  45. package/dist/protocols/utils/openai-options.js +64 -0
  46. package/dist/protocols/utils/tool-schema.d.ts +7 -0
  47. package/dist/protocols/utils/tool-schema.js +79 -0
  48. package/dist/protocols/utils/tool-stream.d.ts +155 -0
  49. package/dist/protocols/utils/tool-stream.js +136 -0
  50. package/dist/protocols.d.ts +1 -0
  51. package/dist/protocols.js +1 -0
  52. package/dist/provider-error.d.ts +13 -0
  53. package/dist/provider-error.js +122 -0
  54. package/dist/provider-package.d.ts +13 -0
  55. package/dist/provider-package.js +1 -0
  56. package/dist/provider.d.ts +23 -0
  57. package/dist/provider.js +2 -0
  58. package/dist/providers/amazon-bedrock.d.ts +150 -0
  59. package/dist/providers/amazon-bedrock.js +41 -0
  60. package/dist/providers/anthropic-compatible.d.ts +183 -0
  61. package/dist/providers/anthropic-compatible.js +44 -0
  62. package/dist/providers/anthropic.d.ts +181 -0
  63. package/dist/providers/anthropic.js +39 -0
  64. package/dist/providers/azure/chat.d.ts +2 -0
  65. package/dist/providers/azure/chat.js +1 -0
  66. package/dist/providers/azure/responses.d.ts +2 -0
  67. package/dist/providers/azure/responses.js +1 -0
  68. package/dist/providers/azure.d.ts +183 -0
  69. package/dist/providers/azure.js +89 -0
  70. package/dist/providers/cloudflare.d.ts +232 -0
  71. package/dist/providers/cloudflare.js +87 -0
  72. package/dist/providers/github-copilot.d.ts +167 -0
  73. package/dist/providers/github-copilot.js +47 -0
  74. package/dist/providers/google-vertex/anthropic.d.ts +2 -0
  75. package/dist/providers/google-vertex/anthropic.js +1 -0
  76. package/dist/providers/google-vertex-anthropic.d.ts +181 -0
  77. package/dist/providers/google-vertex-anthropic.js +46 -0
  78. package/dist/providers/google-vertex-shared.d.ts +22 -0
  79. package/dist/providers/google-vertex-shared.js +61 -0
  80. package/dist/providers/google-vertex.d.ts +93 -0
  81. package/dist/providers/google-vertex.js +49 -0
  82. package/dist/providers/google.d.ts +85 -0
  83. package/dist/providers/google.js +33 -0
  84. package/dist/providers/index.d.ts +15 -0
  85. package/dist/providers/index.js +15 -0
  86. package/dist/providers/openai/chat.d.ts +2 -0
  87. package/dist/providers/openai/chat.js +1 -0
  88. package/dist/providers/openai/responses.d.ts +2 -0
  89. package/dist/providers/openai/responses.js +1 -0
  90. package/dist/providers/openai-compatible/responses.d.ts +1 -0
  91. package/dist/providers/openai-compatible/responses.js +1 -0
  92. package/dist/providers/openai-compatible-profile.d.ts +43 -0
  93. package/dist/providers/openai-compatible-profile.js +12 -0
  94. package/dist/providers/openai-compatible-responses.d.ts +107 -0
  95. package/dist/providers/openai-compatible-responses.js +33 -0
  96. package/dist/providers/openai-compatible.d.ts +130 -0
  97. package/dist/providers/openai-compatible.js +55 -0
  98. package/dist/providers/openai-options.d.ts +30 -0
  99. package/dist/providers/openai-options.js +42 -0
  100. package/dist/providers/openai.d.ts +264 -0
  101. package/dist/providers/openai.js +62 -0
  102. package/dist/providers/openrouter.d.ts +321 -0
  103. package/dist/providers/openrouter.js +65 -0
  104. package/dist/providers/xai.d.ts +169 -0
  105. package/dist/providers/xai.js +43 -0
  106. package/dist/providers.d.ts +1 -0
  107. package/dist/providers.js +1 -0
  108. package/dist/route/auth-options.d.ts +34 -0
  109. package/dist/route/auth-options.js +14 -0
  110. package/dist/route/auth.d.ts +49 -0
  111. package/dist/route/auth.js +89 -0
  112. package/dist/route/client.d.ts +330 -0
  113. package/dist/route/client.js +211 -0
  114. package/dist/route/endpoint.d.ts +28 -0
  115. package/dist/route/endpoint.js +21 -0
  116. package/dist/route/executor.d.ts +12 -0
  117. package/dist/route/executor.js +229 -0
  118. package/dist/route/framing.d.ts +23 -0
  119. package/dist/route/framing.js +4 -0
  120. package/dist/route/index.d.ts +16 -0
  121. package/dist/route/index.js +9 -0
  122. package/dist/route/protocol.d.ts +76 -0
  123. package/dist/route/protocol.js +17 -0
  124. package/dist/route/transport/http.d.ts +33 -0
  125. package/dist/route/transport/http.js +98 -0
  126. package/dist/route/transport/index.d.ts +27 -0
  127. package/dist/route/transport/index.js +4 -0
  128. package/dist/route/transport/websocket.d.ts +56 -0
  129. package/dist/route/transport/websocket.js +181 -0
  130. package/dist/route.d.ts +1 -0
  131. package/dist/route.js +1 -0
  132. package/dist/schema/errors.d.ts +153 -0
  133. package/dist/schema/errors.js +150 -0
  134. package/dist/schema/events.d.ts +4292 -0
  135. package/dist/schema/events.js +482 -0
  136. package/dist/schema/ids.d.ts +30 -0
  137. package/dist/schema/ids.js +18 -0
  138. package/dist/schema/index.d.ts +5 -0
  139. package/dist/schema/index.js +5 -0
  140. package/dist/schema/messages.d.ts +385 -0
  141. package/dist/schema/messages.js +246 -0
  142. package/dist/schema/options.d.ts +143 -0
  143. package/dist/schema/options.js +195 -0
  144. package/dist/tool-runtime.d.ts +15 -0
  145. package/dist/tool-runtime.js +35 -0
  146. package/dist/tool.d.ts +134 -0
  147. package/dist/tool.js +66 -0
  148. package/dist/utils/record.d.ts +2 -0
  149. package/dist/utils/record.js +2 -0
  150. package/package.json +36 -9
  151. package/index.js +0 -1
@@ -0,0 +1,211 @@
1
+ import { Cause, Context, Effect, Layer, Schema, Stream } from "effect";
2
+ import * as Option from "effect/Option";
3
+ import { Auth } from "./auth";
4
+ import { Endpoint } from "./endpoint";
5
+ import { RequestExecutor } from "./executor";
6
+ import { Framing } from "./framing";
7
+ import { HttpTransport } from "./transport";
8
+ import { WebSocketExecutor } from "./transport";
9
+ import { applyCachePolicy } from "../cache-policy";
10
+ import * as ProviderShared from "../protocols/shared";
11
+ import { GenerationOptions, HttpOptions, LLMRequest, LLMResponse, Model, ModelLimits, LLMError as LLMErrorClass, LLMEvent, PreparedRequest, ProviderID, mergeGenerationOptions, mergeHttpOptions, mergeProviderOptions, } from "../schema";
12
+ const makeRouteModel = (route, mapped) => {
13
+ const provider = route.provider ?? ("provider" in mapped ? mapped.provider : undefined);
14
+ if (!provider)
15
+ throw new Error(`Route.model(${route.id}) requires a provider`);
16
+ if (!endpointBaseURL(route.endpoint))
17
+ throw new Error(`Route.model(${route.id}) requires an endpoint baseURL — configure it on the route first`);
18
+ return Model.make({
19
+ ...mapped,
20
+ provider,
21
+ route,
22
+ });
23
+ };
24
+ const mergeRouteDefaults = (base, patch) => {
25
+ const headers = mergeHeaders(base?.headers, patch.headers);
26
+ return {
27
+ ...base,
28
+ ...patch,
29
+ headers,
30
+ limits: patch.limits === undefined ? base?.limits : ModelLimits.make(patch.limits),
31
+ generation: mergeGenerationOptions(generationOptions(base?.generation), generationOptions(patch.generation)),
32
+ providerOptions: mergeProviderOptions(base?.providerOptions, patch.providerOptions),
33
+ http: mergeHttpOptions(base?.http, httpOptions(patch.http), headers === undefined ? undefined : new HttpOptions({ headers })),
34
+ };
35
+ };
36
+ const endpointBaseURL = (endpoint) => typeof endpoint.baseURL === "string" ? endpoint.baseURL : undefined;
37
+ const mergeHeaders = (...items) => {
38
+ const entries = items.flatMap((item) => item === undefined ? [] : Object.entries(item).filter((entry) => entry[1] !== undefined));
39
+ if (entries.length === 0)
40
+ return undefined;
41
+ return Object.fromEntries(entries);
42
+ };
43
+ export const generationOptions = (input) => input === undefined ? undefined : GenerationOptions.make(input);
44
+ export const httpOptions = (input) => {
45
+ if (input === undefined)
46
+ return input;
47
+ return HttpOptions.make(input);
48
+ };
49
+ export class Service extends Context.Service()("@opencode/LLMClient") {
50
+ }
51
+ const resolveRequestOptions = (request) => {
52
+ const routeDefaults = request.model.route.defaults;
53
+ const modelDefaults = request.model.defaults;
54
+ const generation = mergeGenerationOptions(routeDefaults.generation, modelDefaults?.generation, request.generation);
55
+ return LLMRequest.update(request, {
56
+ generation: generation ?? new GenerationOptions({}),
57
+ providerOptions: mergeProviderOptions(routeDefaults.providerOptions, modelDefaults?.providerOptions, request.providerOptions),
58
+ http: mergeHttpOptions(routeDefaults.http, modelDefaults?.http, request.http),
59
+ });
60
+ };
61
+ const streamError = (route, message, cause) => {
62
+ const failed = cause.reasons.find(Cause.isFailReason)?.error;
63
+ if (failed instanceof LLMErrorClass)
64
+ return failed;
65
+ return ProviderShared.eventError(route, message, Cause.pretty(cause));
66
+ };
67
+ const requireTerminalEvent = (route) => (events) => Stream.suspend(() => {
68
+ let terminal = false;
69
+ return events.pipe(Stream.mapEffect((event) => {
70
+ if (terminal)
71
+ return Effect.fail(ProviderShared.eventError(route, `Provider emitted ${event.type} after the terminal event`));
72
+ if (LLMEvent.is.finish(event) || LLMEvent.is.providerError(event))
73
+ terminal = true;
74
+ return Effect.succeed(event);
75
+ }), Stream.onEnd(Effect.suspend(() => terminal
76
+ ? Effect.void
77
+ : Effect.fail(ProviderShared.eventError(route, "Provider stream ended without a terminal finish event")))));
78
+ });
79
+ function makeFromTransport(input) {
80
+ const protocol = input.protocol;
81
+ const encodeBody = Schema.encodeSync(Schema.fromJsonString(protocol.body.schema));
82
+ const decodeEventEffect = Schema.decodeUnknownEffect(protocol.stream.event);
83
+ const decodeEvent = (route) => (frame) => decodeEventEffect(frame).pipe(Effect.mapError(() => ProviderShared.eventError(input.id, `Invalid ${route} stream event`, typeof frame === "string" ? frame : ProviderShared.encodeJson(frame))));
84
+ const build = (routeInput) => {
85
+ const route = {
86
+ id: routeInput.id,
87
+ provider: routeInput.provider === undefined ? undefined : ProviderID.make(routeInput.provider),
88
+ providerMetadataKey: routeInput.providerMetadataKey,
89
+ protocol: protocol.id,
90
+ endpoint: routeInput.endpoint,
91
+ auth: routeInput.auth ?? Auth.none,
92
+ transport: routeInput.transport,
93
+ defaults: routeInput.defaults ?? {},
94
+ body: protocol.body,
95
+ with: (patch) => {
96
+ const { id, provider, auth, transport, endpoint, ...defaults } = patch;
97
+ return build({
98
+ ...routeInput,
99
+ id: id ?? routeInput.id,
100
+ provider: provider ?? routeInput.provider,
101
+ auth: auth ?? routeInput.auth,
102
+ endpoint: endpoint ? Endpoint.merge(routeInput.endpoint, endpoint) : routeInput.endpoint,
103
+ transport: transport ?? routeInput.transport,
104
+ defaults: mergeRouteDefaults(route.defaults, defaults),
105
+ });
106
+ },
107
+ model: (input) => makeRouteModel(route, input),
108
+ prepareTransport: (body, request) => routeInput.transport.prepare({
109
+ body,
110
+ request,
111
+ endpoint: routeInput.endpoint,
112
+ auth: routeInput.auth ?? Auth.none,
113
+ encodeBody,
114
+ headers: routeInput.headers,
115
+ }),
116
+ streamPrepared: (prepared, request, runtime) => {
117
+ const route = `${request.model.provider}/${request.model.route.id}`;
118
+ const events = routeInput.transport
119
+ .frames(prepared, request, runtime)
120
+ .pipe(Stream.mapEffect(decodeEvent(route)), protocol.stream.terminal ? Stream.takeUntil(protocol.stream.terminal) : (stream) => stream);
121
+ return events.pipe(Stream.mapAccumEffect(() => protocol.stream.initial(request), protocol.stream.step, protocol.stream.onHalt ? { onHalt: protocol.stream.onHalt } : undefined), Stream.catchCause((cause) => Stream.fail(streamError(route, `Failed to read ${route} stream`, cause))), requireTerminalEvent(route));
122
+ },
123
+ };
124
+ return route;
125
+ };
126
+ return build({ ...input, defaults: mergeRouteDefaults(undefined, input.defaults ?? {}) });
127
+ }
128
+ export function make(input) {
129
+ if ("transport" in input)
130
+ return makeFromTransport(input);
131
+ const protocol = input.protocol;
132
+ return makeFromTransport({
133
+ id: input.id,
134
+ provider: input.provider,
135
+ providerMetadataKey: input.providerMetadataKey,
136
+ protocol,
137
+ endpoint: input.endpoint,
138
+ auth: input.auth,
139
+ headers: input.headers,
140
+ transport: HttpTransport.httpJson({ framing: input.framing }),
141
+ defaults: input.defaults,
142
+ });
143
+ }
144
+ // `compile` is the important boundary: it turns a common `LLMRequest` into a
145
+ // validated provider body plus transport-private prepared data, but does not
146
+ // execute transport.
147
+ const compile = Effect.fn("LLM.compile")(function* (request) {
148
+ const resolved = applyCachePolicy(resolveRequestOptions(request));
149
+ const route = resolved.model.route;
150
+ const body = yield* route.body
151
+ .from(resolved)
152
+ .pipe(Effect.flatMap(ProviderShared.validateWith(Schema.decodeUnknownEffect(route.body.schema))));
153
+ const prepared = yield* route.prepareTransport(body, resolved);
154
+ return {
155
+ request: resolved,
156
+ route,
157
+ body,
158
+ prepared,
159
+ };
160
+ });
161
+ const prepareWith = Effect.fn("LLMClient.prepare")(function* (request) {
162
+ const compiled = yield* compile(request);
163
+ return new PreparedRequest({
164
+ id: compiled.request.id ?? "request",
165
+ route: compiled.route.id,
166
+ protocol: compiled.route.protocol,
167
+ model: compiled.request.model,
168
+ body: compiled.body,
169
+ metadata: { transport: compiled.route.transport.id },
170
+ });
171
+ });
172
+ const streamRequestWith = (runtime) => (request) => Stream.unwrap(Effect.gen(function* () {
173
+ const compiled = yield* compile(request);
174
+ return compiled.route.streamPrepared(compiled.prepared, compiled.request, runtime);
175
+ }));
176
+ const generateWith = (stream) => Effect.fn("LLM.generate")(function* (request) {
177
+ const state = yield* stream(request).pipe(Stream.runFold(LLMResponse.empty, LLMResponse.reduce));
178
+ const response = LLMResponse.complete(state);
179
+ if (response)
180
+ return response;
181
+ return yield* ProviderShared.eventError(`${request.model.provider}/${request.model.route.id}`, "Provider stream ended without a terminal finish event");
182
+ });
183
+ export const prepare = (request) => prepareWith(request);
184
+ export function stream(request) {
185
+ return Stream.unwrap(Effect.gen(function* () {
186
+ return (yield* Service).stream(request);
187
+ }));
188
+ }
189
+ export function generate(request) {
190
+ return Effect.gen(function* () {
191
+ return yield* (yield* Service).generate(request);
192
+ });
193
+ }
194
+ export const streamRequest = (request) => Stream.unwrap(Effect.gen(function* () {
195
+ return (yield* Service).stream(request);
196
+ }));
197
+ export const layer = Layer.effect(Service, Effect.gen(function* () {
198
+ const stream = streamRequestWith({
199
+ http: yield* RequestExecutor.Service,
200
+ webSocket: Option.getOrUndefined(yield* Effect.serviceOption(WebSocketExecutor.Service)),
201
+ });
202
+ return Service.of({ prepare: prepareWith, stream, generate: generateWith(stream) });
203
+ }));
204
+ export const Route = { make };
205
+ export const LLMClient = {
206
+ Service,
207
+ layer,
208
+ prepare,
209
+ stream,
210
+ generate,
211
+ };
@@ -0,0 +1,28 @@
1
+ import type { LLMRequest } from "../schema";
2
+ export interface EndpointInput<Body> {
3
+ readonly request: LLMRequest;
4
+ readonly body: Body;
5
+ }
6
+ export type EndpointPart<Body> = string | ((input: EndpointInput<Body>) => string);
7
+ /**
8
+ * Declarative URL construction for one route.
9
+ *
10
+ * `Endpoint` carries URL construction for one route. Routes with a canonical
11
+ * host put `baseURL` here; provider helpers can override it by configuring the
12
+ * route before selecting a model.
13
+ *
14
+ * `path` may be a string or a function of `EndpointInput`, for routes whose
15
+ * URL embeds the model id, region, or another body field (e.g. Bedrock,
16
+ * Gemini).
17
+ */
18
+ export interface Definition<Body> {
19
+ readonly baseURL?: string;
20
+ readonly path: EndpointPart<Body>;
21
+ readonly query?: Record<string, string>;
22
+ }
23
+ export type EndpointPatch<Body> = Partial<Definition<Body>>;
24
+ /** Construct an `Endpoint` from a path string or path function. */
25
+ export declare const path: <Body>(value: EndpointPart<Body>, options?: Omit<Definition<Body>, "path">) => Definition<Body>;
26
+ export declare const merge: <Body>(base: Definition<Body>, patch: EndpointPatch<Body>) => Definition<Body>;
27
+ export declare const render: <Body>(endpoint: Definition<Body>, input: EndpointInput<Body>) => URL;
28
+ export * as Endpoint from "./endpoint";
@@ -0,0 +1,21 @@
1
+ import * as ProviderShared from "../protocols/shared";
2
+ /** Construct an `Endpoint` from a path string or path function. */
3
+ export const path = (value, options = {}) => ({
4
+ ...options,
5
+ path: value,
6
+ });
7
+ export const merge = (base, patch) => ({
8
+ ...base,
9
+ ...patch,
10
+ baseURL: patch.baseURL ?? base.baseURL,
11
+ path: patch.path ?? base.path,
12
+ query: patch.query === undefined ? base.query : { ...base.query, ...patch.query },
13
+ });
14
+ const renderPart = (part, input) => typeof part === "function" ? part(input) : part;
15
+ export const render = (endpoint, input) => {
16
+ const url = new URL(`${ProviderShared.trimBaseUrl(endpoint.baseURL ?? "")}${renderPart(endpoint.path, input)}`);
17
+ for (const [key, value] of Object.entries(endpoint.query ?? {}))
18
+ url.searchParams.set(key, value);
19
+ return url;
20
+ };
21
+ export * as Endpoint from "./endpoint";
@@ -0,0 +1,12 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
3
+ import { LLMError } from "../schema";
4
+ export interface Interface {
5
+ readonly execute: (request: HttpClientRequest.HttpClientRequest) => Effect.Effect<HttpClientResponse.HttpClientResponse, LLMError>;
6
+ }
7
+ declare const Service_base: Context.ServiceClass<Service, "@opencode/LLM/RequestExecutor", Interface>;
8
+ export declare class Service extends Service_base {
9
+ }
10
+ export declare const layer: Layer.Layer<Service, never, HttpClient.HttpClient>;
11
+ export declare const fetchLayer: Layer.Layer<Service, never, never>;
12
+ export * as RequestExecutor from "./executor";
@@ -0,0 +1,229 @@
1
+ import { Cause, Context, Effect, Layer } from "effect";
2
+ import { FetchHttpClient, Headers, HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse, } from "effect/unstable/http";
3
+ import { HttpContext, HttpRateLimitDetails, HttpRequestDetails, HttpResponseDetails, LLMError, TransportReason, } from "../schema";
4
+ import { classifyProviderFailure } from "../provider-error";
5
+ export class Service extends Context.Service()("@opencode/LLM/RequestExecutor") {
6
+ }
7
+ const BODY_LIMIT = 16_384;
8
+ const REDACTED = "<redacted>";
9
+ // One source of truth for what counts as a sensitive name across headers,
10
+ // URL query keys, and field names embedded inside request/response bodies.
11
+ //
12
+ // `SENSITIVE_NAME` is used as both a substring matcher (for free-form header
13
+ // names like `Authorization` / `X-API-Key`) and as the body-field alternation
14
+ // list. `SHORT_QUERY_NAME` covers anchored short keys like `?key=…` / `?sig=…`
15
+ // that are too generic to redact substring-style without false positives.
16
+ const SENSITIVE_NAME_SOURCE = "authorization|api[-_]?key|access[-_]?token|refresh[-_]?token|id[-_]?token|token|secret|credential|signature|x-amz-signature";
17
+ const SENSITIVE_NAME = new RegExp(SENSITIVE_NAME_SOURCE, "i");
18
+ const SHORT_QUERY_NAME = /^(key|sig)$/i;
19
+ const SENSITIVE_BODY_FIELD = new RegExp(`(?:${SENSITIVE_NAME_SOURCE}|key)`, "i");
20
+ const REDACT_JSON_FIELD = new RegExp(`("(?:${SENSITIVE_BODY_FIELD.source})"\\s*:\\s*)"[^"]*"`, "gi");
21
+ const REDACT_QUERY_FIELD = new RegExp(`((?:${SENSITIVE_BODY_FIELD.source})=)[^&\\s"]+`, "gi");
22
+ const isSensitiveHeaderName = (name) => SENSITIVE_NAME.test(name);
23
+ const isSensitiveQueryName = (name) => isSensitiveHeaderName(name) || SHORT_QUERY_NAME.test(name);
24
+ const redactHeaders = (headers, redactedNames) => Object.fromEntries(Object.entries(Headers.redact(headers, [...redactedNames, SENSITIVE_NAME])).map(([name, value]) => [
25
+ name,
26
+ String(value),
27
+ ]));
28
+ const redactUrl = (value) => {
29
+ if (!URL.canParse(value))
30
+ return REDACTED;
31
+ const url = new URL(value);
32
+ url.searchParams.forEach((_, key) => {
33
+ if (isSensitiveQueryName(key))
34
+ url.searchParams.set(key, REDACTED);
35
+ });
36
+ return url.toString();
37
+ };
38
+ const normalizedHeaders = (headers) => Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]));
39
+ const requestId = (headers) => {
40
+ return (headers["x-request-id"] ??
41
+ headers["request-id"] ??
42
+ headers["x-amzn-requestid"] ??
43
+ headers["x-amz-request-id"] ??
44
+ headers["x-goog-request-id"] ??
45
+ headers["cf-ray"]);
46
+ };
47
+ const retryAfterMs = (headers) => {
48
+ const millis = Number(headers["retry-after-ms"]);
49
+ if (Number.isFinite(millis))
50
+ return Math.max(0, millis);
51
+ const value = headers["retry-after"];
52
+ if (!value)
53
+ return undefined;
54
+ const seconds = Number(value);
55
+ if (Number.isFinite(seconds))
56
+ return Math.max(0, seconds * 1000);
57
+ const date = Date.parse(value);
58
+ if (!Number.isNaN(date))
59
+ return Math.max(0, date - Date.now());
60
+ return undefined;
61
+ };
62
+ const addRateLimitValue = (target, key, value) => {
63
+ if (key.length > 0)
64
+ target[key] = value;
65
+ };
66
+ const rateLimitDetails = (headers, retryAfter) => {
67
+ const limit = {};
68
+ const remaining = {};
69
+ const reset = {};
70
+ Object.entries(headers).forEach(([name, value]) => {
71
+ const openaiLimit = /^x-ratelimit-limit-(.+)$/.exec(name)?.[1];
72
+ if (openaiLimit)
73
+ return addRateLimitValue(limit, openaiLimit, value);
74
+ const openaiRemaining = /^x-ratelimit-remaining-(.+)$/.exec(name)?.[1];
75
+ if (openaiRemaining)
76
+ return addRateLimitValue(remaining, openaiRemaining, value);
77
+ const openaiReset = /^x-ratelimit-reset-(.+)$/.exec(name)?.[1];
78
+ if (openaiReset)
79
+ return addRateLimitValue(reset, openaiReset, value);
80
+ const anthropic = /^anthropic-ratelimit-(.+)-(limit|remaining|reset)$/.exec(name);
81
+ if (!anthropic)
82
+ return;
83
+ if (anthropic[2] === "limit")
84
+ return addRateLimitValue(limit, anthropic[1], value);
85
+ if (anthropic[2] === "remaining")
86
+ return addRateLimitValue(remaining, anthropic[1], value);
87
+ return addRateLimitValue(reset, anthropic[1], value);
88
+ });
89
+ if (retryAfter === undefined &&
90
+ Object.keys(limit).length === 0 &&
91
+ Object.keys(remaining).length === 0 &&
92
+ Object.keys(reset).length === 0)
93
+ return undefined;
94
+ return new HttpRateLimitDetails({
95
+ retryAfterMs: retryAfter,
96
+ limit: Object.keys(limit).length === 0 ? undefined : limit,
97
+ remaining: Object.keys(remaining).length === 0 ? undefined : remaining,
98
+ reset: Object.keys(reset).length === 0 ? undefined : reset,
99
+ });
100
+ };
101
+ const requestDetails = (request, redactedNames) => new HttpRequestDetails({
102
+ method: request.method,
103
+ url: redactUrl(request.url),
104
+ headers: redactHeaders(request.headers, redactedNames),
105
+ });
106
+ const responseDetails = (response, redactedNames) => new HttpResponseDetails({
107
+ status: response.status,
108
+ headers: redactHeaders(response.headers, redactedNames),
109
+ });
110
+ const secretValues = (request) => {
111
+ const values = new Set();
112
+ const add = (value) => {
113
+ if (value.length < 4)
114
+ return;
115
+ values.add(value);
116
+ values.add(encodeURIComponent(value));
117
+ };
118
+ Object.entries(request.headers).forEach(([name, value]) => {
119
+ if (!isSensitiveHeaderName(name))
120
+ return;
121
+ add(value);
122
+ const bearer = /^Bearer\s+(.+)$/i.exec(value)?.[1];
123
+ if (bearer)
124
+ add(bearer);
125
+ });
126
+ if (!URL.canParse(request.url))
127
+ return values;
128
+ new URL(request.url).searchParams.forEach((value, key) => {
129
+ if (isSensitiveQueryName(key))
130
+ add(value);
131
+ });
132
+ return values;
133
+ };
134
+ // Two passes: structural (redact `"name": "value"` and `name=value` patterns
135
+ // for any field name that looks sensitive) plus literal (replace any actual
136
+ // secret values we sent in the request, in case the response echoes one back).
137
+ const redactBody = (body, request) => Array.from(secretValues(request)).reduce((text, secret) => text.split(secret).join(REDACTED), body.replace(REDACT_JSON_FIELD, `$1"${REDACTED}"`).replace(REDACT_QUERY_FIELD, `$1${REDACTED}`));
138
+ const responseBody = (body, request) => {
139
+ if (body === undefined)
140
+ return {};
141
+ const redacted = redactBody(body, request);
142
+ if (redacted.length <= BODY_LIMIT)
143
+ return { body: redacted };
144
+ return { body: redacted.slice(0, BODY_LIMIT), bodyTruncated: true };
145
+ };
146
+ const providerMessage = (status, body) => {
147
+ if (body.body && body.body.length <= 500)
148
+ return `Provider request failed with HTTP ${status}: ${body.body}`;
149
+ return `Provider request failed with HTTP ${status}`;
150
+ };
151
+ const responseHttp = (input) => new HttpContext({
152
+ request: requestDetails(input.request, input.redactedNames),
153
+ response: responseDetails(input.response, input.redactedNames),
154
+ ...input.body,
155
+ requestId: input.requestId,
156
+ rateLimit: input.rateLimit,
157
+ });
158
+ const statusError = (request, redactedNames) => (response) => Effect.gen(function* () {
159
+ if (response.status < 400)
160
+ return response;
161
+ const body = yield* response.text.pipe(Effect.catch(() => Effect.void));
162
+ const headers = normalizedHeaders(response.headers);
163
+ const retryAfter = retryAfterMs(headers);
164
+ const rateLimit = rateLimitDetails(headers, retryAfter);
165
+ const details = responseBody(body, request);
166
+ return yield* new LLMError({
167
+ module: "RequestExecutor",
168
+ method: "execute",
169
+ reason: classifyProviderFailure({
170
+ status: response.status,
171
+ message: providerMessage(response.status, details),
172
+ retryAfterMs: retryAfter,
173
+ rateLimit,
174
+ http: responseHttp({
175
+ request,
176
+ response,
177
+ redactedNames,
178
+ body: details,
179
+ requestId: requestId(headers),
180
+ rateLimit,
181
+ }),
182
+ }),
183
+ });
184
+ });
185
+ const toHttpError = (redactedNames) => (error) => {
186
+ const transportError = (input) => new LLMError({
187
+ module: "RequestExecutor",
188
+ method: "execute",
189
+ reason: new TransportReason({
190
+ message: input.message,
191
+ kind: input.kind,
192
+ url: input.request ? redactUrl(input.request.url) : undefined,
193
+ http: input.request ? new HttpContext({ request: requestDetails(input.request, redactedNames) }) : undefined,
194
+ }),
195
+ });
196
+ if (Cause.isTimeoutError(error)) {
197
+ return transportError({ message: error.message, kind: "Timeout" });
198
+ }
199
+ if (!HttpClientError.isHttpClientError(error)) {
200
+ return transportError({ message: "HTTP transport failed" });
201
+ }
202
+ const request = "request" in error ? error.request : undefined;
203
+ if (error.reason._tag === "TransportError") {
204
+ return transportError({
205
+ message: error.reason.description ?? "HTTP transport failed",
206
+ kind: error.reason._tag,
207
+ request,
208
+ });
209
+ }
210
+ return transportError({
211
+ message: `HTTP transport failed: ${error.reason._tag}`,
212
+ kind: error.reason._tag,
213
+ request,
214
+ });
215
+ };
216
+ export const layer = Layer.effect(Service, Effect.gen(function* () {
217
+ const http = yield* HttpClient.HttpClient;
218
+ const executeOnce = (request) => Effect.gen(function* () {
219
+ const redactedNames = yield* Headers.CurrentRedactedNames;
220
+ return yield* http
221
+ .execute(request)
222
+ .pipe(Effect.mapError(toHttpError(redactedNames)), Effect.flatMap(statusError(request, redactedNames)));
223
+ });
224
+ return Service.of({
225
+ execute: executeOnce,
226
+ });
227
+ }));
228
+ export const fetchLayer = layer.pipe(Layer.provide(FetchHttpClient.layer));
229
+ export * as RequestExecutor from "./executor";
@@ -0,0 +1,23 @@
1
+ import type { Stream } from "effect";
2
+ import type { LLMError } from "../schema";
3
+ /**
4
+ * Decode a streaming HTTP response body into provider-protocol frames.
5
+ *
6
+ * `Framing` is the byte-stream-shaped seam between transport and protocol:
7
+ *
8
+ * - SSE (`Framing.sse`) — UTF-8 decode the body, run the SSE channel decoder,
9
+ * drop empty / `[DONE]` keep-alives. Each emitted frame is the JSON `data:`
10
+ * payload of one event.
11
+ * - AWS event stream — length-prefixed binary frames with CRC checksums.
12
+ * Each emitted frame is one parsed binary event record.
13
+ *
14
+ * The frame type is opaque to this layer; the protocol's `decode` step turns
15
+ * a frame into a typed chunk.
16
+ */
17
+ export interface Definition<Frame> {
18
+ readonly id: string;
19
+ readonly frame: (bytes: Stream.Stream<Uint8Array, LLMError>) => Stream.Stream<Frame, LLMError>;
20
+ }
21
+ /** Server-Sent Events framing. Used by every JSON-streaming HTTP provider. */
22
+ export declare const sse: Definition<string>;
23
+ export * as Framing from "./framing";
@@ -0,0 +1,4 @@
1
+ import * as ProviderShared from "../protocols/shared";
2
+ /** Server-Sent Events framing. Used by every JSON-streaming HTTP provider. */
3
+ export const sse = { id: "sse", frame: ProviderShared.sseFraming };
4
+ export * as Framing from "./framing";
@@ -0,0 +1,16 @@
1
+ export { Route, LLMClient } from "./client";
2
+ export type { Route as RouteShape, RouteModelInput, RouteRoutedModelInput, RouteDefaults, RouteDefaultsInput, AnyRoute, Interface as LLMClientShape, Service as LLMClientService, } from "./client";
3
+ export * from "./executor";
4
+ export { Auth } from "./auth";
5
+ export { AuthOptions } from "./auth-options";
6
+ export { Endpoint } from "./endpoint";
7
+ export { Framing } from "./framing";
8
+ export { Protocol } from "./protocol";
9
+ export { HttpTransport, WebSocketExecutor, WebSocketTransport } from "./transport";
10
+ export * as Transport from "./transport";
11
+ export type { Definition as AuthShape, AuthInput, Credential, CredentialError } from "./auth";
12
+ export type { ApiKeyMode, AuthOverride, ProviderAuthOption } from "./auth-options";
13
+ export type { Definition as EndpointFn, EndpointInput } from "./endpoint";
14
+ export type { Definition as FramingDef } from "./framing";
15
+ export type { Protocol as ProtocolDef } from "./protocol";
16
+ export type { Transport as TransportDef, TransportRuntime } from "./transport";
@@ -0,0 +1,9 @@
1
+ export { Route, LLMClient } from "./client";
2
+ export * from "./executor";
3
+ export { Auth } from "./auth";
4
+ export { AuthOptions } from "./auth-options";
5
+ export { Endpoint } from "./endpoint";
6
+ export { Framing } from "./framing";
7
+ export { Protocol } from "./protocol";
8
+ export { HttpTransport, WebSocketExecutor, WebSocketTransport } from "./transport";
9
+ export * as Transport from "./transport";
@@ -0,0 +1,76 @@
1
+ import { Schema, type Effect } from "effect";
2
+ import type { LLMError, LLMEvent, LLMRequest, ProtocolID } from "../schema";
3
+ /**
4
+ * The semantic API contract of one model server family.
5
+ *
6
+ * A `Protocol` owns the parts of a route that are intrinsic to "what does
7
+ * this API look like": how a common `LLMRequest` becomes a provider-native
8
+ * body, what schema that body must satisfy before it is JSON-encoded, and
9
+ * how the streaming response decodes back into common `LLMEvent`s.
10
+ *
11
+ * Examples:
12
+ *
13
+ * - `OpenAIChat.protocol` — chat completions style
14
+ * - `OpenAIResponses.protocol` — responses API
15
+ * - `AnthropicMessages.protocol` — messages API with content blocks
16
+ * - `Gemini.protocol` — generateContent
17
+ * - `BedrockConverse.protocol` — Converse with binary event-stream framing
18
+ *
19
+ * A `Protocol` is **not** a deployment. It does not know which URL, which
20
+ * headers, or which auth scheme to use. Those are deployment concerns owned
21
+ * by `Route.make(...)` along with the chosen `Endpoint`, `Auth`,
22
+ * and `Framing`. This separation is what lets DeepSeek, TogetherAI, Cerebras,
23
+ * etc. all reuse `OpenAIChat.protocol` without forking 300 lines per provider.
24
+ *
25
+ * The four type parameters reflect the pipeline:
26
+ *
27
+ * - `Body` — provider-native request body candidate. `Route.make(...)`
28
+ * validates and JSON-encodes it with `body.schema`.
29
+ * - `Frame` — one unit of the framed response stream. SSE: a JSON data
30
+ * string. AWS event stream: a parsed binary frame.
31
+ * - `Event` — schema-decoded provider event produced from one frame.
32
+ * - `State` — accumulator threaded through `stream.step` to translate event
33
+ * sequences into `LLMEvent` sequences.
34
+ */
35
+ export interface Protocol<Body, Frame, Event, State> {
36
+ /** Stable id for the wire protocol implementation. */
37
+ readonly id: ProtocolID;
38
+ /** Request side: schema for the provider-native body and how to build it. */
39
+ readonly body: ProtocolBody<Body>;
40
+ /** Response side: streaming state machine. */
41
+ readonly stream: ProtocolStream<Frame, Event, State>;
42
+ }
43
+ export interface ProtocolBody<Body> {
44
+ /** Schema for the validated provider-native body sent as the JSON request. */
45
+ readonly schema: Schema.Codec<Body, unknown>;
46
+ /** Build the provider-native body from a common `LLMRequest`. */
47
+ readonly from: (request: LLMRequest) => Effect.Effect<Body, LLMError>;
48
+ }
49
+ export interface ProtocolStream<Frame, Event, State> {
50
+ /** Schema for one decoded streaming event, decoded from a transport frame. */
51
+ readonly event: Schema.Codec<Event, Frame>;
52
+ /** Initial parser state. Called once per response with the resolved request. */
53
+ readonly initial: (request: LLMRequest) => State;
54
+ /** Translate one event into emitted `LLMEvent`s plus the next state. */
55
+ readonly step: (state: State, event: Event) => Effect.Effect<readonly [State, ReadonlyArray<LLMEvent>], LLMError>;
56
+ /** Optional request-completion signal for transports that do not end naturally. */
57
+ readonly terminal?: (event: Event) => boolean;
58
+ /** Optional flush emitted when the framed stream ends. */
59
+ readonly onHalt?: (state: State) => ReadonlyArray<LLMEvent>;
60
+ }
61
+ /**
62
+ * Construct a `Protocol` from its body and stream pieces:
63
+ *
64
+ * - `body.schema` infers the provider-native request body shape.
65
+ * - `body.from` ties the common `LLMRequest` to the provider body.
66
+ * - `stream.event` infers the decoded streaming event and the wire frame.
67
+ * - `stream.initial`, `stream.step`, and `stream.onHalt` infer the parser state.
68
+ *
69
+ * Provider implementations should usually call `Protocol.make({ ... })`
70
+ * without explicit type arguments; the schemas and parser functions are the
71
+ * source of truth. The constructor remains as the public seam for future
72
+ * cross-cutting concerns such as tracing or instrumentation.
73
+ */
74
+ export declare const make: <Body, Frame, Event, State>(input: Protocol<Body, Frame, Event, State>) => Protocol<Body, Frame, Event, State>;
75
+ export declare const jsonEvent: <const S extends Schema.Top>(schema: S) => Schema.fromJsonString<S>;
76
+ export * as Protocol from "./protocol";
@@ -0,0 +1,17 @@
1
+ import { Schema } from "effect";
2
+ /**
3
+ * Construct a `Protocol` from its body and stream pieces:
4
+ *
5
+ * - `body.schema` infers the provider-native request body shape.
6
+ * - `body.from` ties the common `LLMRequest` to the provider body.
7
+ * - `stream.event` infers the decoded streaming event and the wire frame.
8
+ * - `stream.initial`, `stream.step`, and `stream.onHalt` infer the parser state.
9
+ *
10
+ * Provider implementations should usually call `Protocol.make({ ... })`
11
+ * without explicit type arguments; the schemas and parser functions are the
12
+ * source of truth. The constructor remains as the public seam for future
13
+ * cross-cutting concerns such as tracing or instrumentation.
14
+ */
15
+ export const make = (input) => input;
16
+ export const jsonEvent = (schema) => Schema.fromJsonString(schema);
17
+ export * as Protocol from "./protocol";