1import { getProjectBranchAiGateway } from "@distilled.cloud/neon";
2import * as Effect from "effect/Effect";
3import * as Namespace from "../Namespace.ts";
4import * as Output from "../Output.ts";
5import { InvalidBranchScope, resolveBranchScope } from "./BranchScope.ts";
6import type { Credential } from "./Credential.ts";
7
8export type AIGatewayProps = (
9 | {
10 /** Branch resource or explicit identity; output references are accepted. */
11 branch:
12 | import("./Branch.ts").Branch
13 | {
14 projectId: string | Output.Output<string>;
15 branchId: string | Output.Output<string>;
16 };
17 project?: never;
18 }
19 | {
20 /** Project resource or identity whose default branch supplies the gateway. */
21 project:
22 | import("./Project.ts").Project
23 | {
24 projectId: string | Output.Output<string>;
25 };
26 branch?: never;
27 }
28) & {
29 /** Optional managed branch credential with ai_gateway:invoke permission. */
30 credential?: Credential;
31};
32
33export interface AIGateway {
34 /** Construct identity, used for deterministic binding names. */
35 readonly FQN: string;
36 /** Local construct identity. */
37 readonly LogicalId: string;
38 /** Declared scope and credential, retained for binding identity. */
39 readonly Props: AIGatewayProps;
40 /** Endpoint's owning project. */
41 readonly projectId: Output.Output<string>;
42 /** Endpoint's owning branch. */
43 readonly branchId: Output.Output<string>;
44 /** Gateway root, before the chat or Responses dialect path. */
45 readonly baseUrl: Output.Output<string>;
46 /** Optional explicit credential; never an account deployment API key. */
47 readonly credential: Credential | undefined;
48}
49
50/**
51 * Discover a branch AI Gateway endpoint without inventing gateway CRUD. The
52 * construct never purchases credits or changes the account plan. Native Neon
53 * Functions use injected credentials; QueryAIGatewayHttp owns a scoped service
54 * credential when none is supplied. Model access and credits remain separate.
55 *
56 * ### Connect an existing branch
57 * **Example:** Gateway configuration
58 * ```typescript
59 * const gateway = yield* Neon.AIGateway("AI", { branch });
60 * // In a Function init Effect:
61 * const ai = yield* Neon.QueryAIGateway(gateway);
62 * const model = ai.model({ model: "gpt-5-mini" });
63 * // Provide model to Effect AI generateText/generateObject/streamText in a handler.
64 * ```
65 *
66 * ### Use output references explicitly
67 * **Example:** Discover a gateway from a project's and branch's outputs
68 * ```typescript
69 * const gateway = yield* Neon.AIGateway("AI", {
70 * branch: { projectId: project.projectId, branchId: branch.branchId },
71 * });
72 * ```
73 *
74 * @resource
75 * @product AI Gateway
76 * @category AI Gateway
77 */
78export const AIGateway = Effect.fn(function* (
79 id: string,
80 props: AIGatewayProps,
81) {
82 if ((props.branch !== undefined) === (props.project !== undefined)) {
83 return yield* Effect.die(
84 new InvalidBranchScope({
85 message: "Specify exactly one of branch or project",
86 }),
87 );
88 }
89 const namespace = yield* Namespace.CurrentChain;
90 const projectId = Output.asOutput(
91 props.branch?.projectId ?? props.project!.projectId,
92 );
93 const branchId = props.branch
94 ? Output.asOutput(props.branch.branchId)
95 : projectId.pipe(
96 Output.mapEffect((projectId) =>
97 resolveBranchScope({ project: { projectId } }).pipe(
98 Effect.map((scope) => scope.branchId),
99 Effect.orDie,
100 ),
101 ),
102 );
103 const baseUrl = Output.all(projectId, branchId).pipe(
104 Output.mapEffect(([project_id, branch_id]) =>
105 getProjectBranchAiGateway({ project_id, branch_id }).pipe(
106 Effect.map((gateway) => gateway.base_url.replace(/\/$/, "")),
107 Effect.orDie,
108 ),
109 ),
110 );
111 return {
112 FQN: [...namespace.toReversed(), id].join("/"),
113 LogicalId: id,
114 Props: props,
115 projectId,
116 branchId,
117 baseUrl,
118 credential: props.credential,
119 } satisfies AIGateway;
120});