effect-vercel 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lucas Duailibe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,166 @@
1
+ # effect-vercel
2
+
3
+ [Effect](https://effect.website) (v4) integrations for Vercel. Each one is its own entry point:
4
+
5
+ | Import | What |
6
+ | ----------------------------- | ------------------------------------------------------------------------------- |
7
+ | `effect-vercel/ai-gateway` | [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) for Effect AI |
8
+ | `effect-vercel/oidc` | The OIDC token Vercel issues to a deployment |
9
+ | `effect-vercel/runtime-cache` | [Runtime Cache](https://vercel.com/docs/runtime-cache) for Effect `Persistence` |
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ pnpm add effect-vercel effect
15
+ pnpm add @effect/ai-anthropic # only for effect-vercel/ai-gateway
16
+ pnpm add @vercel/functions # only for effect-vercel/runtime-cache
17
+ ```
18
+
19
+ ## AI Gateway
20
+
21
+ The gateway serves the Anthropic Messages API and routes to any model it lists, named
22
+ `provider/model`. `AiGateway` points Effect AI's Anthropic provider at the gateway, handles
23
+ authentication per request, and smooths over the small differences between the gateway's
24
+ dialect and what the provider's schemas expect.
25
+
26
+ ### Usage
27
+
28
+ ```ts
29
+ import { AiGateway } from "effect-vercel/ai-gateway"
30
+ import { AnthropicLanguageModel } from "@effect/ai-anthropic"
31
+ import { Effect, Layer } from "effect"
32
+ import { LanguageModel } from "effect/unstable/ai"
33
+ import { FetchHttpClient } from "effect/unstable/http"
34
+
35
+ const Model = AnthropicLanguageModel.layer({ model: "google/gemini-2.5-flash" }).pipe(
36
+ Layer.provide(AiGateway.layer),
37
+ Layer.provide(FetchHttpClient.layer),
38
+ )
39
+
40
+ const program = LanguageModel.generateText({ prompt: "Say hi" }).pipe(
41
+ Effect.map((r) => r.text),
42
+ Effect.provide(Model),
43
+ )
44
+ ```
45
+
46
+ `AiGateway.layer` provides `AnthropicClient` and requires an `HttpClient`. It authenticates
47
+ with `AI_GATEWAY_API_KEY` if set, else the Vercel OIDC token, the same order as Vercel's own
48
+ SDK. So it works locally with an API key and on Vercel with no setup.
49
+
50
+ Any Effect AI feature that works with the Anthropic provider (tools, structured output,
51
+ streaming, thinking) works through the gateway, for every model the gateway offers.
52
+
53
+ ### Credentials
54
+
55
+ To pick the source yourself, use `AiGateway.layerWithoutCredentials` and provide an
56
+ `AiGatewayCredentials` layer:
57
+
58
+ ```ts
59
+ AnthropicLanguageModel.layer({ model: "google/gemini-2.5-flash" }).pipe(
60
+ Layer.provide(AiGateway.layerWithoutCredentials),
61
+ Layer.provide([AiGatewayCredentials.layerFromApiKey(key), FetchHttpClient.layer]),
62
+ )
63
+ ```
64
+
65
+ | Layer | Source |
66
+ | ------------------------------------------- | --------------------------------------------------------- |
67
+ | `AiGatewayCredentials.layer` | The default: `layerFromEnv`, then `layerFromVercelOidc`. |
68
+ | `AiGatewayCredentials.layerFromEnv` | `AI_GATEWAY_API_KEY`. Local dev, CI, anywhere off Vercel. |
69
+ | `AiGatewayCredentials.layerFromVercelOidc` | The Vercel OIDC token (see [OIDC](#oidc)). |
70
+ | `AiGatewayCredentials.layerFromApiKey(key)` | A fixed key. Tests, custom wiring. |
71
+
72
+ The credential is resolved on every request, so a rotating token is always current.
73
+ Each request carries `x-api-key` and `ai-gateway-auth-method` (`api-key` or `oidc`). A
74
+ missing credential fails the request with an `AiError` wrapping an `AiGatewayCredentialsError` that
75
+ names the source it tried and how to fix it.
76
+
77
+ Environment variables are read through Effect's `Config`, so a `ConfigProvider` can redirect
78
+ them.
79
+
80
+ ### Dialect fixes
81
+
82
+ Two adjustments are applied to traffic with the gateway:
83
+
84
+ - Requests: `"cache_control": null` is removed from content blocks. The Effect provider emits it,
85
+ Anthropic accepts it, the gateway rejects it.
86
+ - Responses: keys Anthropic always sends but the gateway omits are filled in with Anthropic's
87
+ "nothing to report" values, so the provider's schemas decode. These are the cache and
88
+ service-tier usage fields, `signature` on thinking blocks from non-Anthropic models, and
89
+ `type`/`request_id` on error envelopes (unknown `error.type` values map to `api_error`).
90
+
91
+ Streaming responses pass through untouched.
92
+
93
+ ## OIDC
94
+
95
+ Vercel issues each deployment an OIDC token. Use it for the AI Gateway, or exchange it for
96
+ cloud credentials (AWS `AssumeRoleWithWebIdentity`, GCP workload identity federation).
97
+
98
+ ```ts
99
+ import { VercelOidc } from "effect-vercel/oidc"
100
+ import { Effect, Redacted } from "effect"
101
+
102
+ const program = Effect.gen(function* () {
103
+ const token = yield* VercelOidc.token
104
+ // Redacted.value(token) is the web identity token
105
+ })
106
+ ```
107
+
108
+ - `VercelOidc.token` reads the `x-vercel-oidc-token` header of the current request
109
+ (Functions), else `VERCEL_OIDC_TOKEN` (builds, `vercel env pull`). It fails with a
110
+ `VercelOidcError` when neither is set.
111
+ - `VercelOidc.find` is the same but returns `Option.none()` instead of failing, for fallback
112
+ chains.
113
+
114
+ Both read the token on every run, since it rotates per request. They do not refresh an
115
+ expired local token; re-run `vercel env pull`.
116
+
117
+ ## Runtime Cache
118
+
119
+ `VercelRuntimeCache.layer` provides Effect's `Persistence`, stored in the Vercel Runtime Cache.
120
+ Use it with `PersistedCache` to share results across Function instances in a region.
121
+
122
+ ```ts
123
+ import { VercelRuntimeCache } from "effect-vercel/runtime-cache"
124
+ import { Effect, Schema } from "effect"
125
+ import { Persistable, PersistedCache } from "effect/unstable/persistence"
126
+
127
+ class GetUser extends Persistable.Class<{ payload: { id: number } }>()("GetUser", {
128
+ primaryKey: (req) => `GetUser:${req.id}`,
129
+ success: User,
130
+ error: Schema.Never,
131
+ }) {}
132
+
133
+ const program = Effect.gen(function* () {
134
+ const users = yield* PersistedCache.make(fetchUser, {
135
+ storeId: "users",
136
+ timeToLive: () => "1 hour",
137
+ })
138
+ return yield* users.get(new GetUser({ id: 1 }))
139
+ }).pipe(Effect.scoped, Effect.provide(VercelRuntimeCache.layer))
140
+ ```
141
+
142
+ - Off Vercel, `@vercel/functions` falls back to an in-memory cache, so the layer works in
143
+ local dev and tests.
144
+ - Entries are keyed by `[storeId, primaryKey]`, hashed with SHA-256. Only the hash reaches
145
+ Vercel. `getCache`'s default hash is 32-bit and can collide, so
146
+ `VercelRuntimeCache.makeBacking(cache)` expects a cache with a stronger `keyHashFunction` if
147
+ you build your own.
148
+ - Each entry is tagged `effect-persistence:<sha256 of storeId>`. `clear` expires that tag.
149
+ - TTLs round up to whole seconds.
150
+ - The Runtime Cache swallows its own network errors and timeouts, which then read as misses
151
+ and silently skipped writes, removes and clears. `getCache` has no option to surface them.
152
+
153
+ ## Development
154
+
155
+ ```sh
156
+ pnpm install
157
+ pnpm test
158
+ pnpm check # tsc
159
+ pnpm lint # oxlint
160
+ pnpm fmt # oxfmt
161
+ pnpm build # tsdown -> dist/
162
+ ```
163
+
164
+ ## License
165
+
166
+ MIT
@@ -0,0 +1,70 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
+ import { Config, Data, Effect, Option, Redacted } from "effect";
3
+ //#region src/VercelOidc.ts
4
+ /**
5
+ * The OIDC token Vercel issues to a deployment.
6
+ *
7
+ * In Functions the token arrives on each request as the `x-vercel-oidc-token`
8
+ * header and rotates, so it is read on every run of `token`/`find`, never
9
+ * cached. In builds and after `vercel env pull` it is `VERCEL_OIDC_TOKEN`.
10
+ * Use it to authenticate to the AI Gateway, or exchange it for cloud
11
+ * credentials (AWS `AssumeRoleWithWebIdentity`, GCP workload identity
12
+ * federation).
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { VercelOidc } from "effect-vercel/oidc"
17
+ * import { Effect, Redacted } from "effect"
18
+ *
19
+ * const program = Effect.gen(function* () {
20
+ * const token = yield* VercelOidc.token
21
+ * // pass Redacted.value(token) as the web identity token
22
+ * })
23
+ * ```
24
+ */
25
+ var VercelOidc_exports = /* @__PURE__ */ __exportAll({
26
+ TOKEN_ENV: () => TOKEN_ENV,
27
+ VercelOidcError: () => VercelOidcError,
28
+ find: () => find,
29
+ hints: () => hints,
30
+ token: () => token
31
+ });
32
+ /** The environment variable read when no request carries a token. */
33
+ const TOKEN_ENV = "VERCEL_OIDC_TOKEN";
34
+ /** No usable OIDC token. */
35
+ var VercelOidcError = class extends Data.TaggedError("VercelOidcError") {};
36
+ /** How to make a token available. */
37
+ const hints = ["Run on Vercel, where each request carries an OIDC token.", `Or run \`vercel env pull\` locally to populate ${TOKEN_ENV}.`];
38
+ /**
39
+ * Vercel exposes the current request's context on a well-known global symbol.
40
+ * In Functions, the `x-vercel-oidc-token` header of the request lives there.
41
+ * Mirrors `@vercel/oidc`'s `getVercelOidcTokenSync`, minus the dev-time
42
+ * refresh that package also offers.
43
+ */
44
+ const REQUEST_CONTEXT = Symbol.for("@vercel/request-context");
45
+ const fromRequestContext = Effect.sync(() => {
46
+ const holder = globalThis;
47
+ return Option.fromNullishOr(holder[REQUEST_CONTEXT]?.get?.()?.headers?.["x-vercel-oidc-token"]).pipe(Option.map(Redacted.make));
48
+ });
49
+ const fromEnv = Config.option(Config.redacted(TOKEN_ENV)).pipe(Effect.catchTag("ConfigError", (cause) => new VercelOidcError({
50
+ message: `Failed to read ${TOKEN_ENV}.`,
51
+ hints,
52
+ cause
53
+ })));
54
+ /**
55
+ * The token if one is available: the request header, else `VERCEL_OIDC_TOKEN`.
56
+ * `None` means neither is set, so a caller can fall back to something else.
57
+ */
58
+ const find = Effect.flatMap(fromRequestContext, (found) => Option.isSome(found) ? Effect.succeed(found) : fromEnv);
59
+ /** Like `find`, but fails when no token is available. */
60
+ const token = Effect.flatMap(find, Option.match({
61
+ onNone: () => Effect.fail(new VercelOidcError({
62
+ message: "No Vercel OIDC token is available.",
63
+ hints
64
+ })),
65
+ onSome: Effect.succeed
66
+ }));
67
+ //#endregion
68
+ export { find as n, hints as r, VercelOidc_exports as t };
69
+
70
+ //# sourceMappingURL=VercelOidc--1oU5DP2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VercelOidc--1oU5DP2.js","names":[],"sources":["../src/VercelOidc.ts"],"sourcesContent":["/**\n * The OIDC token Vercel issues to a deployment.\n *\n * In Functions the token arrives on each request as the `x-vercel-oidc-token`\n * header and rotates, so it is read on every run of `token`/`find`, never\n * cached. In builds and after `vercel env pull` it is `VERCEL_OIDC_TOKEN`.\n * Use it to authenticate to the AI Gateway, or exchange it for cloud\n * credentials (AWS `AssumeRoleWithWebIdentity`, GCP workload identity\n * federation).\n *\n * @example\n * ```ts\n * import { VercelOidc } from \"effect-vercel/oidc\"\n * import { Effect, Redacted } from \"effect\"\n *\n * const program = Effect.gen(function* () {\n * const token = yield* VercelOidc.token\n * // pass Redacted.value(token) as the web identity token\n * })\n * ```\n */\n\nimport { Config, Data, Effect, Option, Redacted } from \"effect\"\n\n/** The environment variable read when no request carries a token. */\nexport const TOKEN_ENV = \"VERCEL_OIDC_TOKEN\"\n\n/** No usable OIDC token. */\nexport class VercelOidcError extends Data.TaggedError(\"VercelOidcError\")<{\n readonly message: string\n readonly hints: ReadonlyArray<string>\n readonly cause?: unknown\n}> {}\n\n/** How to make a token available. */\nexport const hints: ReadonlyArray<string> = [\n \"Run on Vercel, where each request carries an OIDC token.\",\n `Or run \\`vercel env pull\\` locally to populate ${TOKEN_ENV}.`,\n]\n\n/**\n * Vercel exposes the current request's context on a well-known global symbol.\n * In Functions, the `x-vercel-oidc-token` header of the request lives there.\n * Mirrors `@vercel/oidc`'s `getVercelOidcTokenSync`, minus the dev-time\n * refresh that package also offers.\n */\nconst REQUEST_CONTEXT = Symbol.for(\"@vercel/request-context\")\n\ntype RequestContext = { readonly headers?: Record<string, string | undefined> }\n\nconst fromRequestContext = Effect.sync(() => {\n const holder = globalThis as typeof globalThis & {\n [REQUEST_CONTEXT]?: { get?: () => RequestContext | undefined }\n }\n return Option.fromNullishOr(\n holder[REQUEST_CONTEXT]?.get?.()?.headers?.[\"x-vercel-oidc-token\"],\n ).pipe(Option.map(Redacted.make))\n})\n\nconst fromEnv = Config.option(Config.redacted(TOKEN_ENV)).pipe(\n Effect.catchTag(\n \"ConfigError\",\n (cause) => new VercelOidcError({ message: `Failed to read ${TOKEN_ENV}.`, hints, cause }),\n ),\n)\n\n/**\n * The token if one is available: the request header, else `VERCEL_OIDC_TOKEN`.\n * `None` means neither is set, so a caller can fall back to something else.\n */\nexport const find: Effect.Effect<\n Option.Option<Redacted.Redacted<string>>,\n VercelOidcError\n> = Effect.flatMap(fromRequestContext, (found) =>\n Option.isSome(found) ? Effect.succeed(found) : fromEnv,\n)\n\n/** Like `find`, but fails when no token is available. */\nexport const token: Effect.Effect<Redacted.Redacted<string>, VercelOidcError> = Effect.flatMap(\n find,\n Option.match({\n onNone: () =>\n Effect.fail(new VercelOidcError({ message: \"No Vercel OIDC token is available.\", hints })),\n onSome: Effect.succeed,\n }),\n)\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,MAAa,YAAY;;AAGzB,IAAa,kBAAb,cAAqC,KAAK,YAAY,iBAAiB,CAAC,CAIrE,CAAC;;AAGJ,MAAa,QAA+B,CAC1C,4DACA,kDAAkD,UAAU,EAC9D;;;;;;;AAQA,MAAM,kBAAkB,OAAO,IAAI,yBAAyB;AAI5D,MAAM,qBAAqB,OAAO,WAAW;CAC3C,MAAM,SAAS;CAGf,OAAO,OAAO,cACZ,OAAO,gBAAgB,EAAE,MAAM,CAAC,EAAE,UAAU,sBAC9C,CAAC,CAAC,KAAK,OAAO,IAAI,SAAS,IAAI,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,OAAO,OAAO,OAAO,SAAS,SAAS,CAAC,CAAC,CAAC,KACxD,OAAO,SACL,gBACC,UAAU,IAAI,gBAAgB;CAAE,SAAS,kBAAkB,UAAU;CAAI;CAAO;AAAM,CAAC,CAC1F,CACF;;;;;AAMA,MAAa,OAGT,OAAO,QAAQ,qBAAqB,UACtC,OAAO,OAAO,KAAK,IAAI,OAAO,QAAQ,KAAK,IAAI,OACjD;;AAGA,MAAa,QAAmE,OAAO,QACrF,MACA,OAAO,MAAM;CACX,cACE,OAAO,KAAK,IAAI,gBAAgB;EAAE,SAAS;EAAsC;CAAM,CAAC,CAAC;CAC3F,QAAQ,OAAO;AACjB,CAAC,CACH"}
@@ -0,0 +1,87 @@
1
+ import { AnthropicClient } from "@effect/ai-anthropic";
2
+ import { Context, Effect, Layer, Redacted } from "effect";
3
+ import { HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
4
+ declare namespace AiGatewayCredentials_d_exports {
5
+ export { API_KEY_ENV, AiGatewayCredentials, AiGatewayCredentialsError, Resolved, Source, formatHeaders, layer$1 as layer, layerFromApiKey, layerFromEnv, layerFromVercelOidc };
6
+ }
7
+ /** The environment variable `layerFromEnv` reads. */
8
+ declare const API_KEY_ENV = "AI_GATEWAY_API_KEY";
9
+ /** Which layer produced or failed to produce a credential. */
10
+ type Source = "api-key" | "env" | "vercel-oidc" | "default";
11
+ /**
12
+ * A credential ready to put on a request. `method` is what the gateway
13
+ * records as the auth method; it matches the values Vercel's own SDK sends.
14
+ */
15
+ interface Resolved {
16
+ readonly method: "api-key" | "oidc";
17
+ readonly token: Redacted.Redacted<string>;
18
+ }
19
+ declare const AiGatewayCredentialsError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
20
+ readonly _tag: "AiGatewayCredentialsError";
21
+ } & Readonly<A>;
22
+ /** No credential could be produced for a gateway request. */
23
+ declare class AiGatewayCredentialsError extends AiGatewayCredentialsError_base<{
24
+ readonly message: string;
25
+ readonly source: Source;
26
+ readonly hints: ReadonlyArray<string>;
27
+ readonly cause?: unknown;
28
+ }> {}
29
+ declare const AiGatewayCredentials_base: Context.ServiceClass<AiGatewayCredentials, "effect-vercel/ai-gateway/AiGatewayCredentials", Effect.Effect<Resolved, AiGatewayCredentialsError, never>>;
30
+ declare class AiGatewayCredentials extends AiGatewayCredentials_base {}
31
+ /** The request headers that carry `credentials`. */
32
+ declare const formatHeaders: (credentials: Resolved) => Record<string, string>;
33
+ /** A fixed API key. */
34
+ declare const layerFromApiKey: (apiKey: string | Redacted.Redacted<string>) => Layer.Layer<AiGatewayCredentials>;
35
+ /** `AI_GATEWAY_API_KEY`: local dev, CI, or anywhere off Vercel. */
36
+ declare const layerFromEnv: Layer.Layer<AiGatewayCredentials>;
37
+ /** The Vercel OIDC token (see `VercelOidc.find`). Not refreshed when it expires locally. */
38
+ declare const layerFromVercelOidc: Layer.Layer<AiGatewayCredentials>;
39
+ /**
40
+ * The default: `layerFromEnv`, then `layerFromVercelOidc`. Same order as
41
+ * Vercel's own SDK, so it works locally with an API key and on Vercel without
42
+ * one.
43
+ */
44
+ declare const layer$1: Layer.Layer<AiGatewayCredentials>;
45
+ declare namespace AiGateway_d_exports {
46
+ export { AI_GATEWAY_URL, dropNullCacheControl, fillMissingResponseKeys, layer, layerWithoutCredentials, make, patchResponseBody };
47
+ }
48
+ /** Base URL of the Vercel AI Gateway. */
49
+ declare const AI_GATEWAY_URL = "https://ai-gateway.vercel.sh";
50
+ /**
51
+ * Builds the Anthropic client against the gateway, authenticating each
52
+ * request with `credentials`. A credential failure fails that request as an
53
+ * `HttpClientError`, which Effect AI surfaces as a network `AiError`.
54
+ */
55
+ declare const make: (credentials: typeof AiGatewayCredentials.Service) => Effect.Effect<AnthropicClient.Service, never, HttpClient.HttpClient>;
56
+ /**
57
+ * `AnthropicClient` backed by the gateway, authenticating with the
58
+ * `AiGatewayCredentials` you provide (see that module's `layerFrom*` layers).
59
+ * Needs an `HttpClient` too.
60
+ */
61
+ declare const layerWithoutCredentials: Layer.Layer<AnthropicClient.AnthropicClient, never, AiGatewayCredentials | HttpClient.HttpClient>;
62
+ /**
63
+ * `layerWithoutCredentials` with the default `AiGatewayCredentials.layer`:
64
+ * `AI_GATEWAY_API_KEY`, else the Vercel OIDC token. Needs an `HttpClient`.
65
+ * Building never fails; without a usable credential, the requests fail.
66
+ */
67
+ declare const layer: Layer.Layer<AnthropicClient.AnthropicClient, never, HttpClient.HttpClient>;
68
+ /**
69
+ * The Effect provider writes `"cache_control": null` on every content block
70
+ * it has no cache setting for. Anthropic accepts that; the gateway rejects
71
+ * the request with `messages.0.content: Invalid input`. Remove the null keys.
72
+ */
73
+ declare function dropNullCacheControl(request: HttpClientRequest.HttpClientRequest): HttpClientRequest.HttpClientRequest;
74
+ /**
75
+ * The gateway's Anthropic-shaped responses leave out keys that Anthropic
76
+ * always sends and the Effect provider's schema requires: the cache and
77
+ * service-tier usage fields, the `signature` of a thinking block from a
78
+ * non-Anthropic model, and `type`/`request_id` on error envelopes, whose
79
+ * `error.type` may also be a gateway-specific value. Fill them with the values
80
+ * Anthropic uses when there is nothing to report. Streaming responses are not
81
+ * JSON and pass through untouched.
82
+ */
83
+ declare function fillMissingResponseKeys(response: HttpClientResponse.HttpClientResponse): Effect.Effect<HttpClientResponse.HttpClientResponse, HttpClientError.HttpClientError>;
84
+ declare function patchResponseBody(body: Record<string, unknown>): Record<string, unknown>;
85
+ //#endregion
86
+ export { AiGateway_d_exports as AiGateway, AiGatewayCredentials_d_exports as AiGatewayCredentials };
87
+ //# sourceMappingURL=ai-gateway.d.ts.map
@@ -0,0 +1,255 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
+ import { n as find, r as hints$1 } from "./VercelOidc--1oU5DP2.js";
3
+ import { AnthropicClient } from "@effect/ai-anthropic";
4
+ import { Config, Context, Data, Effect, Layer, Option, Redacted } from "effect";
5
+ import { HttpBody, HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
6
+ //#region src/AiGatewayCredentials.ts
7
+ /**
8
+ * How gateway requests authenticate.
9
+ *
10
+ * `AiGatewayCredentials` holds an effect that produces the credential for one
11
+ * request. It runs per request rather than once at startup: on Vercel the OIDC
12
+ * token arrives on the request context and rotates, so it must not be cached.
13
+ * `layer` is the default; pick a `layerFrom*` layer to use one source only.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { AiGatewayCredentials } from "effect-vercel/ai-gateway"
18
+ *
19
+ * AiGatewayCredentials.layer // AI_GATEWAY_API_KEY, else the Vercel OIDC token
20
+ * AiGatewayCredentials.layerFromEnv // AI_GATEWAY_API_KEY only
21
+ * AiGatewayCredentials.layerFromVercelOidc // the Vercel OIDC token only
22
+ * AiGatewayCredentials.layerFromApiKey("…") // a fixed value
23
+ * ```
24
+ */
25
+ var AiGatewayCredentials_exports = /* @__PURE__ */ __exportAll({
26
+ API_KEY_ENV: () => API_KEY_ENV,
27
+ AiGatewayCredentials: () => AiGatewayCredentials,
28
+ AiGatewayCredentialsError: () => AiGatewayCredentialsError,
29
+ formatHeaders: () => formatHeaders,
30
+ layer: () => layer$1,
31
+ layerFromApiKey: () => layerFromApiKey,
32
+ layerFromEnv: () => layerFromEnv,
33
+ layerFromVercelOidc: () => layerFromVercelOidc
34
+ });
35
+ /** The environment variable `layerFromEnv` reads. */
36
+ const API_KEY_ENV = "AI_GATEWAY_API_KEY";
37
+ /** No credential could be produced for a gateway request. */
38
+ var AiGatewayCredentialsError = class extends Data.TaggedError("AiGatewayCredentialsError") {};
39
+ var AiGatewayCredentials = class extends Context.Service()("effect-vercel/ai-gateway/AiGatewayCredentials") {};
40
+ /** The request headers that carry `credentials`. */
41
+ const formatHeaders = (credentials) => ({
42
+ "x-api-key": Redacted.value(credentials.token),
43
+ "ai-gateway-auth-method": credentials.method
44
+ });
45
+ const hints = {
46
+ "api-key": [],
47
+ env: [`Set ${API_KEY_ENV} to an AI Gateway API key.`],
48
+ "vercel-oidc": hints$1,
49
+ default: [`Set ${API_KEY_ENV} to an AI Gateway API key.`, "Or run on Vercel / `vercel env pull` so an OIDC token is available."]
50
+ };
51
+ const apiKeyLookup = Config.option(Config.redacted(API_KEY_ENV)).pipe(Effect.map(Option.map((token) => ({
52
+ method: "api-key",
53
+ token
54
+ }))), Effect.catchTag("ConfigError", (cause) => new AiGatewayCredentialsError({
55
+ message: `Failed to read ${API_KEY_ENV}.`,
56
+ source: "env",
57
+ hints: hints.env,
58
+ cause
59
+ })));
60
+ const vercelOidcLookup = find.pipe(Effect.map(Option.map((token) => ({
61
+ method: "oidc",
62
+ token
63
+ }))), Effect.mapError((cause) => new AiGatewayCredentialsError({
64
+ message: cause.message,
65
+ source: "vercel-oidc",
66
+ hints: cause.hints,
67
+ cause
68
+ })));
69
+ const firstOf = (lookups) => Effect.gen(function* () {
70
+ for (const lookup of lookups) {
71
+ const found = yield* lookup;
72
+ if (Option.isSome(found)) return found;
73
+ }
74
+ return Option.none();
75
+ });
76
+ const toLayer = (lookup, source, message) => Layer.succeed(AiGatewayCredentials)(Effect.flatMap(lookup, (found) => Option.isSome(found) ? Effect.succeed(found.value) : Effect.fail(new AiGatewayCredentialsError({
77
+ message,
78
+ source,
79
+ hints: hints[source]
80
+ }))));
81
+ /** A fixed API key. */
82
+ const layerFromApiKey = (apiKey) => Layer.succeed(AiGatewayCredentials)(Effect.succeed({
83
+ method: "api-key",
84
+ token: Redacted.isRedacted(apiKey) ? apiKey : Redacted.make(apiKey)
85
+ }));
86
+ /** `AI_GATEWAY_API_KEY`: local dev, CI, or anywhere off Vercel. */
87
+ const layerFromEnv = toLayer(apiKeyLookup, "env", `${API_KEY_ENV} is not set.`);
88
+ /** The Vercel OIDC token (see `VercelOidc.find`). Not refreshed when it expires locally. */
89
+ const layerFromVercelOidc = toLayer(vercelOidcLookup, "vercel-oidc", "No Vercel OIDC token is available.");
90
+ /**
91
+ * The default: `layerFromEnv`, then `layerFromVercelOidc`. Same order as
92
+ * Vercel's own SDK, so it works locally with an API key and on Vercel without
93
+ * one.
94
+ */
95
+ const layer$1 = toLayer(firstOf([apiKeyLookup, vercelOidcLookup]), "default", "No AI Gateway credential found.");
96
+ //#endregion
97
+ //#region src/AiGateway.ts
98
+ /**
99
+ * Vercel AI Gateway as an Anthropic Messages API client for Effect AI.
100
+ *
101
+ * The gateway serves the Anthropic Messages API at its root URL and accepts
102
+ * any model it lists, named `provider/model` (for example
103
+ * `anthropic/claude-sonnet-4.5` or `google/gemini-2.5-flash`). Effect AI's
104
+ * Anthropic provider only needs the base URL and the credential swapped, plus
105
+ * two small dialect fixes (see `dropNullCacheControl` and
106
+ * `fillMissingResponseKeys`), so this module is the one place the gateway is
107
+ * configured.
108
+ *
109
+ * Structured output (`output_config.format`) works for every provider behind
110
+ * the gateway, so the Anthropic provider's default of native structured
111
+ * output holds for models it does not recognise.
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * import { AiGateway } from "effect-vercel/ai-gateway"
116
+ * import { AnthropicLanguageModel } from "@effect/ai-anthropic"
117
+ * import { Layer } from "effect"
118
+ * import { FetchHttpClient } from "effect/unstable/http"
119
+ *
120
+ * const Model = AnthropicLanguageModel.layer({ model: "anthropic/claude-sonnet-4.5" }).pipe(
121
+ * Layer.provide(AiGateway.layer),
122
+ * Layer.provide(FetchHttpClient.layer),
123
+ * )
124
+ * ```
125
+ */
126
+ var AiGateway_exports = /* @__PURE__ */ __exportAll({
127
+ AI_GATEWAY_URL: () => AI_GATEWAY_URL,
128
+ dropNullCacheControl: () => dropNullCacheControl,
129
+ fillMissingResponseKeys: () => fillMissingResponseKeys,
130
+ layer: () => layer,
131
+ layerWithoutCredentials: () => layerWithoutCredentials,
132
+ make: () => make,
133
+ patchResponseBody: () => patchResponseBody
134
+ });
135
+ /** Base URL of the Vercel AI Gateway. */
136
+ const AI_GATEWAY_URL = "https://ai-gateway.vercel.sh";
137
+ /**
138
+ * Builds the Anthropic client against the gateway, authenticating each
139
+ * request with `credentials`. A credential failure fails that request as an
140
+ * `HttpClientError`, which Effect AI surfaces as a network `AiError`.
141
+ */
142
+ const make = (credentials) => AnthropicClient.make({
143
+ apiUrl: AI_GATEWAY_URL,
144
+ transformClient: (client) => client.pipe(HttpClient.mapRequestEffect(authenticate(credentials)), HttpClient.mapRequest(dropNullCacheControl), HttpClient.transformResponse(Effect.flatMap(fillMissingResponseKeys)))
145
+ });
146
+ /**
147
+ * `AnthropicClient` backed by the gateway, authenticating with the
148
+ * `AiGatewayCredentials` you provide (see that module's `layerFrom*` layers).
149
+ * Needs an `HttpClient` too.
150
+ */
151
+ const layerWithoutCredentials = Layer.effect(AnthropicClient.AnthropicClient)(Effect.flatMap(Effect.service(AiGatewayCredentials), make));
152
+ /**
153
+ * `layerWithoutCredentials` with the default `AiGatewayCredentials.layer`:
154
+ * `AI_GATEWAY_API_KEY`, else the Vercel OIDC token. Needs an `HttpClient`.
155
+ * Building never fails; without a usable credential, the requests fail.
156
+ */
157
+ const layer = layerWithoutCredentials.pipe(Layer.provide(layer$1));
158
+ function authenticate(credentials) {
159
+ return (request) => credentials.pipe(Effect.map((resolved) => HttpClientRequest.setHeaders(request, formatHeaders(resolved))), Effect.mapError((error) => new HttpClientError.HttpClientError({ reason: new HttpClientError.TransportError({
160
+ request,
161
+ cause: error,
162
+ description: "AI Gateway credential unavailable"
163
+ }) })));
164
+ }
165
+ /**
166
+ * The Effect provider writes `"cache_control": null` on every content block
167
+ * it has no cache setting for. Anthropic accepts that; the gateway rejects
168
+ * the request with `messages.0.content: Invalid input`. Remove the null keys.
169
+ */
170
+ function dropNullCacheControl(request) {
171
+ const body = request.body;
172
+ if (body._tag !== "Uint8Array" || !isJson(body.contentType)) return request;
173
+ const json = JSON.parse(new TextDecoder().decode(body.body));
174
+ return HttpClientRequest.setBody(request, HttpBody.jsonUnsafe(withoutNullCacheControl(json), body.contentType));
175
+ }
176
+ function withoutNullCacheControl(value) {
177
+ if (Array.isArray(value)) return value.map(withoutNullCacheControl);
178
+ if (value === null || typeof value !== "object") return value;
179
+ const result = {};
180
+ for (const [key, entry] of Object.entries(value)) {
181
+ if (key === "cache_control" && entry === null) continue;
182
+ result[key] = withoutNullCacheControl(entry);
183
+ }
184
+ return result;
185
+ }
186
+ /**
187
+ * The gateway's Anthropic-shaped responses leave out keys that Anthropic
188
+ * always sends and the Effect provider's schema requires: the cache and
189
+ * service-tier usage fields, the `signature` of a thinking block from a
190
+ * non-Anthropic model, and `type`/`request_id` on error envelopes, whose
191
+ * `error.type` may also be a gateway-specific value. Fill them with the values
192
+ * Anthropic uses when there is nothing to report. Streaming responses are not
193
+ * JSON and pass through untouched.
194
+ */
195
+ function fillMissingResponseKeys(response) {
196
+ if (!isJson(response.headers["content-type"] ?? "")) return Effect.succeed(response);
197
+ return Effect.map(response.text, (text) => {
198
+ const body = JSON.parse(text);
199
+ return HttpClientResponse.fromWeb(response.request, new Response(JSON.stringify(patchResponseBody(body)), {
200
+ status: response.status,
201
+ headers: response.headers
202
+ }));
203
+ });
204
+ }
205
+ function patchResponseBody(body) {
206
+ if (body.type === "message") {
207
+ body.usage = {
208
+ cache_creation: null,
209
+ cache_creation_input_tokens: null,
210
+ cache_read_input_tokens: null,
211
+ inference_geo: null,
212
+ service_tier: null,
213
+ ...body.usage ?? {}
214
+ };
215
+ if (Array.isArray(body.content)) body.content = body.content.map((block) => isRecord(block) && block.type === "thinking" ? {
216
+ signature: "",
217
+ ...block
218
+ } : block);
219
+ return body;
220
+ }
221
+ if (isRecord(body.error)) {
222
+ const error = ANTHROPIC_ERROR_TYPES.has(String(body.error.type)) ? body.error : {
223
+ ...body.error,
224
+ type: "api_error"
225
+ };
226
+ return {
227
+ type: "error",
228
+ request_id: null,
229
+ ...body,
230
+ error
231
+ };
232
+ }
233
+ return body;
234
+ }
235
+ const ANTHROPIC_ERROR_TYPES = /* @__PURE__ */ new Set([
236
+ "invalid_request_error",
237
+ "authentication_error",
238
+ "billing_error",
239
+ "permission_error",
240
+ "not_found_error",
241
+ "rate_limit_error",
242
+ "timeout_error",
243
+ "api_error",
244
+ "overloaded_error"
245
+ ]);
246
+ function isRecord(value) {
247
+ return typeof value === "object" && value !== null && !Array.isArray(value);
248
+ }
249
+ function isJson(contentType) {
250
+ return contentType.startsWith("application/json");
251
+ }
252
+ //#endregion
253
+ export { AiGateway_exports as AiGateway, AiGatewayCredentials_exports as AiGatewayCredentials };
254
+
255
+ //# sourceMappingURL=ai-gateway.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-gateway.js","names":["VercelOidc.hints","layer","AiGatewayCredentials.AiGatewayCredentials","AiGatewayCredentials.layer","AiGatewayCredentials.formatHeaders"],"sources":["../src/AiGatewayCredentials.ts","../src/AiGateway.ts"],"sourcesContent":["/**\n * How gateway requests authenticate.\n *\n * `AiGatewayCredentials` holds an effect that produces the credential for one\n * request. It runs per request rather than once at startup: on Vercel the OIDC\n * token arrives on the request context and rotates, so it must not be cached.\n * `layer` is the default; pick a `layerFrom*` layer to use one source only.\n *\n * @example\n * ```ts\n * import { AiGatewayCredentials } from \"effect-vercel/ai-gateway\"\n *\n * AiGatewayCredentials.layer // AI_GATEWAY_API_KEY, else the Vercel OIDC token\n * AiGatewayCredentials.layerFromEnv // AI_GATEWAY_API_KEY only\n * AiGatewayCredentials.layerFromVercelOidc // the Vercel OIDC token only\n * AiGatewayCredentials.layerFromApiKey(\"…\") // a fixed value\n * ```\n */\n\nimport { Config, Context, Data, Effect, Layer, Option, Redacted } from \"effect\"\nimport * as VercelOidc from \"./VercelOidc.js\"\n\n/** The environment variable `layerFromEnv` reads. */\nexport const API_KEY_ENV = \"AI_GATEWAY_API_KEY\"\n\n/** Which layer produced or failed to produce a credential. */\nexport type Source = \"api-key\" | \"env\" | \"vercel-oidc\" | \"default\"\n\n/**\n * A credential ready to put on a request. `method` is what the gateway\n * records as the auth method; it matches the values Vercel's own SDK sends.\n */\nexport interface Resolved {\n readonly method: \"api-key\" | \"oidc\"\n readonly token: Redacted.Redacted<string>\n}\n\n/** No credential could be produced for a gateway request. */\nexport class AiGatewayCredentialsError extends Data.TaggedError(\"AiGatewayCredentialsError\")<{\n readonly message: string\n readonly source: Source\n readonly hints: ReadonlyArray<string>\n readonly cause?: unknown\n}> {}\n\nexport class AiGatewayCredentials extends Context.Service<\n AiGatewayCredentials,\n Effect.Effect<Resolved, AiGatewayCredentialsError>\n>()(\"effect-vercel/ai-gateway/AiGatewayCredentials\") {}\n\n/** The request headers that carry `credentials`. */\nexport const formatHeaders = (credentials: Resolved): Record<string, string> => ({\n \"x-api-key\": Redacted.value(credentials.token),\n \"ai-gateway-auth-method\": credentials.method,\n})\n\nconst hints: Record<Source, ReadonlyArray<string>> = {\n \"api-key\": [],\n env: [`Set ${API_KEY_ENV} to an AI Gateway API key.`],\n \"vercel-oidc\": VercelOidc.hints,\n default: [\n `Set ${API_KEY_ENV} to an AI Gateway API key.`,\n \"Or run on Vercel / `vercel env pull` so an OIDC token is available.\",\n ],\n}\n\n/**\n * Where a credential may come from. `None` means the place is not configured,\n * so a chain can move on; a failure means it is configured but unusable.\n */\ntype Lookup = Effect.Effect<Option.Option<Resolved>, AiGatewayCredentialsError>\n\nconst apiKeyLookup: Lookup = Config.option(Config.redacted(API_KEY_ENV)).pipe(\n Effect.map(Option.map((token) => ({ method: \"api-key\" as const, token }))),\n Effect.catchTag(\n \"ConfigError\",\n (cause) =>\n new AiGatewayCredentialsError({\n message: `Failed to read ${API_KEY_ENV}.`,\n source: \"env\",\n hints: hints.env,\n cause,\n }),\n ),\n)\n\nconst vercelOidcLookup: Lookup = VercelOidc.find.pipe(\n Effect.map(Option.map((token) => ({ method: \"oidc\" as const, token }))),\n Effect.mapError(\n (cause) =>\n new AiGatewayCredentialsError({\n message: cause.message,\n source: \"vercel-oidc\",\n hints: cause.hints,\n cause,\n }),\n ),\n)\n\nconst firstOf = (lookups: ReadonlyArray<Lookup>): Lookup =>\n Effect.gen(function* () {\n for (const lookup of lookups) {\n const found = yield* lookup\n if (Option.isSome(found)) return found\n }\n return Option.none()\n })\n\nconst toLayer = (lookup: Lookup, source: Source, message: string) =>\n Layer.succeed(AiGatewayCredentials)(\n Effect.flatMap(lookup, (found) =>\n Option.isSome(found)\n ? Effect.succeed(found.value)\n : Effect.fail(new AiGatewayCredentialsError({ message, source, hints: hints[source] })),\n ),\n )\n\n/** A fixed API key. */\nexport const layerFromApiKey = (\n apiKey: string | Redacted.Redacted<string>,\n): Layer.Layer<AiGatewayCredentials> =>\n Layer.succeed(AiGatewayCredentials)(\n Effect.succeed({\n method: \"api-key\",\n token: Redacted.isRedacted(apiKey) ? apiKey : Redacted.make(apiKey),\n }),\n )\n\n/** `AI_GATEWAY_API_KEY`: local dev, CI, or anywhere off Vercel. */\nexport const layerFromEnv: Layer.Layer<AiGatewayCredentials> = toLayer(\n apiKeyLookup,\n \"env\",\n `${API_KEY_ENV} is not set.`,\n)\n\n/** The Vercel OIDC token (see `VercelOidc.find`). Not refreshed when it expires locally. */\nexport const layerFromVercelOidc: Layer.Layer<AiGatewayCredentials> = toLayer(\n vercelOidcLookup,\n \"vercel-oidc\",\n \"No Vercel OIDC token is available.\",\n)\n\n/**\n * The default: `layerFromEnv`, then `layerFromVercelOidc`. Same order as\n * Vercel's own SDK, so it works locally with an API key and on Vercel without\n * one.\n */\nexport const layer: Layer.Layer<AiGatewayCredentials> = toLayer(\n firstOf([apiKeyLookup, vercelOidcLookup]),\n \"default\",\n \"No AI Gateway credential found.\",\n)\n","/**\n * Vercel AI Gateway as an Anthropic Messages API client for Effect AI.\n *\n * The gateway serves the Anthropic Messages API at its root URL and accepts\n * any model it lists, named `provider/model` (for example\n * `anthropic/claude-sonnet-4.5` or `google/gemini-2.5-flash`). Effect AI's\n * Anthropic provider only needs the base URL and the credential swapped, plus\n * two small dialect fixes (see `dropNullCacheControl` and\n * `fillMissingResponseKeys`), so this module is the one place the gateway is\n * configured.\n *\n * Structured output (`output_config.format`) works for every provider behind\n * the gateway, so the Anthropic provider's default of native structured\n * output holds for models it does not recognise.\n *\n * @example\n * ```ts\n * import { AiGateway } from \"effect-vercel/ai-gateway\"\n * import { AnthropicLanguageModel } from \"@effect/ai-anthropic\"\n * import { Layer } from \"effect\"\n * import { FetchHttpClient } from \"effect/unstable/http\"\n *\n * const Model = AnthropicLanguageModel.layer({ model: \"anthropic/claude-sonnet-4.5\" }).pipe(\n * Layer.provide(AiGateway.layer),\n * Layer.provide(FetchHttpClient.layer),\n * )\n * ```\n */\n\nimport { AnthropicClient } from \"@effect/ai-anthropic\"\nimport { Effect, Layer } from \"effect\"\nimport {\n HttpBody,\n HttpClient,\n HttpClientError,\n HttpClientRequest,\n HttpClientResponse,\n} from \"effect/unstable/http\"\nimport * as AiGatewayCredentials from \"./AiGatewayCredentials.js\"\n\n/** Base URL of the Vercel AI Gateway. */\nexport const AI_GATEWAY_URL = \"https://ai-gateway.vercel.sh\"\n\n/**\n * Builds the Anthropic client against the gateway, authenticating each\n * request with `credentials`. A credential failure fails that request as an\n * `HttpClientError`, which Effect AI surfaces as a network `AiError`.\n */\nexport const make = (credentials: typeof AiGatewayCredentials.AiGatewayCredentials.Service) =>\n AnthropicClient.make({\n apiUrl: AI_GATEWAY_URL,\n transformClient: (client) =>\n client.pipe(\n HttpClient.mapRequestEffect(authenticate(credentials)),\n HttpClient.mapRequest(dropNullCacheControl),\n HttpClient.transformResponse(Effect.flatMap(fillMissingResponseKeys)),\n ),\n })\n\n/**\n * `AnthropicClient` backed by the gateway, authenticating with the\n * `AiGatewayCredentials` you provide (see that module's `layerFrom*` layers).\n * Needs an `HttpClient` too.\n */\nexport const layerWithoutCredentials: Layer.Layer<\n AnthropicClient.AnthropicClient,\n never,\n AiGatewayCredentials.AiGatewayCredentials | HttpClient.HttpClient\n> = Layer.effect(AnthropicClient.AnthropicClient)(\n Effect.flatMap(Effect.service(AiGatewayCredentials.AiGatewayCredentials), make),\n)\n\n/**\n * `layerWithoutCredentials` with the default `AiGatewayCredentials.layer`:\n * `AI_GATEWAY_API_KEY`, else the Vercel OIDC token. Needs an `HttpClient`.\n * Building never fails; without a usable credential, the requests fail.\n */\nexport const layer: Layer.Layer<AnthropicClient.AnthropicClient, never, HttpClient.HttpClient> =\n layerWithoutCredentials.pipe(Layer.provide(AiGatewayCredentials.layer))\n\nfunction authenticate(credentials: typeof AiGatewayCredentials.AiGatewayCredentials.Service) {\n return (request: HttpClientRequest.HttpClientRequest) =>\n credentials.pipe(\n Effect.map((resolved) =>\n HttpClientRequest.setHeaders(request, AiGatewayCredentials.formatHeaders(resolved)),\n ),\n Effect.mapError(\n (error) =>\n new HttpClientError.HttpClientError({\n reason: new HttpClientError.TransportError({\n request,\n cause: error,\n description: \"AI Gateway credential unavailable\",\n }),\n }),\n ),\n )\n}\n\n/**\n * The Effect provider writes `\"cache_control\": null` on every content block\n * it has no cache setting for. Anthropic accepts that; the gateway rejects\n * the request with `messages.0.content: Invalid input`. Remove the null keys.\n */\nexport function dropNullCacheControl(\n request: HttpClientRequest.HttpClientRequest,\n): HttpClientRequest.HttpClientRequest {\n const body = request.body\n if (body._tag !== \"Uint8Array\" || !isJson(body.contentType)) {\n return request\n }\n const json: unknown = JSON.parse(new TextDecoder().decode(body.body))\n return HttpClientRequest.setBody(\n request,\n HttpBody.jsonUnsafe(withoutNullCacheControl(json), body.contentType),\n )\n}\n\nfunction withoutNullCacheControl(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(withoutNullCacheControl)\n if (value === null || typeof value !== \"object\") return value\n const result: Record<string, unknown> = {}\n for (const [key, entry] of Object.entries(value)) {\n if (key === \"cache_control\" && entry === null) continue\n result[key] = withoutNullCacheControl(entry)\n }\n return result\n}\n\n/**\n * The gateway's Anthropic-shaped responses leave out keys that Anthropic\n * always sends and the Effect provider's schema requires: the cache and\n * service-tier usage fields, the `signature` of a thinking block from a\n * non-Anthropic model, and `type`/`request_id` on error envelopes, whose\n * `error.type` may also be a gateway-specific value. Fill them with the values\n * Anthropic uses when there is nothing to report. Streaming responses are not\n * JSON and pass through untouched.\n */\nexport function fillMissingResponseKeys(\n response: HttpClientResponse.HttpClientResponse,\n): Effect.Effect<HttpClientResponse.HttpClientResponse, HttpClientError.HttpClientError> {\n if (!isJson(response.headers[\"content-type\"] ?? \"\")) {\n return Effect.succeed(response)\n }\n return Effect.map(response.text, (text) => {\n const body = JSON.parse(text) as Record<string, unknown>\n return HttpClientResponse.fromWeb(\n response.request,\n new Response(JSON.stringify(patchResponseBody(body)), {\n status: response.status,\n headers: response.headers,\n }),\n )\n })\n}\n\nexport function patchResponseBody(body: Record<string, unknown>): Record<string, unknown> {\n if (body.type === \"message\") {\n const usage = (body.usage ?? {}) as Record<string, unknown>\n body.usage = {\n cache_creation: null,\n cache_creation_input_tokens: null,\n cache_read_input_tokens: null,\n inference_geo: null,\n service_tier: null,\n ...usage,\n }\n if (Array.isArray(body.content)) {\n body.content = body.content.map((block: unknown) =>\n isRecord(block) && block.type === \"thinking\" ? { signature: \"\", ...block } : block,\n )\n }\n return body\n }\n if (isRecord(body.error)) {\n const error = ANTHROPIC_ERROR_TYPES.has(String(body.error.type))\n ? body.error\n : { ...body.error, type: \"api_error\" }\n return { type: \"error\", request_id: null, ...body, error }\n }\n return body\n}\n\nconst ANTHROPIC_ERROR_TYPES = new Set([\n \"invalid_request_error\",\n \"authentication_error\",\n \"billing_error\",\n \"permission_error\",\n \"not_found_error\",\n \"rate_limit_error\",\n \"timeout_error\",\n \"api_error\",\n \"overloaded_error\",\n])\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n}\n\nfunction isJson(contentType: string): boolean {\n return contentType.startsWith(\"application/json\")\n}\n"],"mappings":"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"}