import { getServiceToken as getConvexServiceToken } from "convex/server"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Schema from "effect/Schema"; import * as HttpClient from "effect/http/HttpClient"; import * as HttpClientError from "effect/http/HttpClientError"; import * as HttpClientRequest from "effect/http/HttpClientRequest"; /** * The current team is on the free plan or has the Convex AI gateway disabled. * Upgrade to a paid plan, or contact Convex support if this looks wrong. */ export class AiGatewayDisabled extends Schema.TaggedError()( "AiGatewayDisabled", {}, ) { override get message(): string { return "The Convex AI gateway is disabled. Your team may be on the free plan, or the gateway may have been disabled for your team. Upgrade to a paid plan, or email support@convex.dev if this looks wrong."; } } /** * The current deployment is anonymous, outdated, or self-hosted and cannot use * the Convex AI gateway. Authenticated, project-linked local deployments are * supported. */ export class AiGatewayUnavailable extends Schema.TaggedError()( "AiGatewayUnavailable", {}, ) { override get message(): string { return "The Convex AI gateway is unavailable on this deployment. For local development, log in, link the deployment to a Convex project, and update the Convex CLI and local backend. Anonymous and self-hosted deployments cannot use the gateway; call the model provider directly with your own API key instead."; } } /** * A schema for the possible failures that can occur while obtaining a Convex AI * gateway service token. */ export const AiGatewayError = Schema.Union([ AiGatewayDisabled, AiGatewayUnavailable, ]); /** * The possible failures that can occur while obtaining a Convex AI gateway * service token. */ export type AiGatewayError = typeof AiGatewayError.Type; type GetServiceToken = typeof getConvexServiceToken; type AiGatewayErrorCode = AiGatewayError["_tag"]; const nodeRuntimeErrorCodeFragments: Record = { AiGatewayDisabled: '"code":"AiGatewayDisabled"', AiGatewayUnavailable: '"code":"AiGatewayUnavailable"', }; // The default Convex runtime exposes only the human-readable ErrorMetadata // message. Node actions preserve the short code in a wrapped JSON response. const defaultRuntimeErrorMessagePrefixes: Record = { AiGatewayDisabled: "The Convex AI gateway is not enabled for your team.", AiGatewayUnavailable: '`getServiceToken("ai-gateway")` isn\'t available on this deployment', }; export interface Service { readonly get: ( service: "ai-gateway", ) => Effect.Effect; } export class AiGatewayServiceToken extends Context.Service< AiGatewayServiceToken, Service >()("@confect/server/internal/AiGatewayServiceToken") {} const matchesErrorCode = ( rejection: unknown, code: AiGatewayErrorCode, ): boolean => (typeof rejection === "object" && rejection !== null && "code" in rejection && rejection.code === code) || (rejection instanceof Error && (rejection.message.includes(nodeRuntimeErrorCodeFragments[code]) || rejection.message.includes(defaultRuntimeErrorMessagePrefixes[code]))); const classifyError = (rejection: unknown): AiGatewayError => { if (matchesErrorCode(rejection, "AiGatewayDisabled")) { return new AiGatewayDisabled(); } if (matchesErrorCode(rejection, "AiGatewayUnavailable")) { return new AiGatewayUnavailable(); } throw rejection; }; export const make = (getServiceToken: GetServiceToken): Service => ({ get: (service) => Effect.tryPromise({ try: () => getServiceToken(service), catch: classifyError, }), }); export const layer = Layer.succeed( AiGatewayServiceToken, make(getConvexServiceToken), ); export const transformClientRequest = (serviceToken: Service) => HttpClient.mapRequestEffect((request) => serviceToken.get("ai-gateway").pipe( Effect.map((token) => HttpClientRequest.bearerToken(request, token)), Effect.mapError( (cause) => new HttpClientError.HttpClientError({ reason: new HttpClientError.TransportError({ request, cause, description: cause.message, }), }), ), ), );