1import * as Neon from "@/Neon";
2import * as Test from "@/Test/Alchemy";
3import * as SDK from "@distilled.cloud/neon";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import * as Schedule from "effect/Schedule";
7import * as Schema from "effect/Schema";
8import * as HttpClient from "effect/http/HttpClient";
9import NativeLanguageModel from "./fixtures/language-model-native.ts";
10import HttpLanguageModel from "./fixtures/language-model-http.ts";
11import { languageModelBranch } from "./fixtures/language-model-resources.ts";
12
13const { test } = Test.make({ providers: Neon.providers() });
14const Text = Schema.Struct({ text: Schema.String });
15
16test.provider.skipIf(
17 process.env.NEON_TEST_AI_PAID !== "1" || !process.env.NEON_TEST_AI_MODEL,
18)(
19 "injected and explicit credentials use the same HTTP Effect AI client for generation, streaming, tools and objects",
20 (stack) =>
21 Effect.gen(function* () {
22 yield* stack.destroy();
23 const deployed = yield* stack.deploy(
24 Effect.gen(function* () {
25 const branch = yield* languageModelBranch;
26 const native = yield* NativeLanguageModel;
27 const http = yield* HttpLanguageModel;
28 return { branch, native, http };
29 }),
30 );
31 yield* Effect.gen(function* () {
32 const client = (yield* HttpClient.HttpClient).pipe(
33 HttpClient.transformResponse(
34 Effect.tap((response) =>
35 response.status < 400
36 ? Effect.void
37 : response.json.pipe(
38 Effect.flatMap(
39 Schema.decodeUnknownEffect(
40 Schema.Struct({ reason: Schema.String }),
41 ),
42 ),
43 Effect.flatMap(({ reason }) =>
44 Effect.log("Neon AI fixture rejected the request", {
45 status: response.status,
46 reason,
47 }),
48 ),
49 ),
50 ),
51 ),
52 HttpClient.filterStatusOk,
53 );
54 for (const fn of [deployed.native, deployed.http]) {
55 const generated = yield* client.get(`${fn.url}/generate`).pipe(
56 Effect.retry({ times: 3, schedule: Schedule.spaced("1 second") }),
57 Effect.flatMap((response) => response.json),
58 Effect.flatMap(Schema.decodeUnknownEffect(Text)),
59 );
60 expect(generated.text.length).toBeGreaterThan(0);
61 const streamed = yield* client.get(`${fn.url}/stream`).pipe(
62 Effect.flatMap((response) => response.json),
63 Effect.flatMap(
64 Schema.decodeUnknownEffect(
65 Schema.Struct({
66 text: Schema.String,
67 finished: Schema.Boolean,
68 }),
69 ),
70 ),
71 );
72 expect(streamed.text.length).toBeGreaterThan(0);
73 expect(streamed.finished).toBe(true);
74 const tool = yield* client.get(`${fn.url}/tool`).pipe(
75 Effect.flatMap((response) => response.json),
76 Effect.flatMap(
77 Schema.decodeUnknownEffect(
78 Schema.Struct({ results: Schema.Array(Schema.Number) }),
79 ),
80 ),
81 );
82 expect(tool.results).toContain(5);
83 const object = yield* client.get(`${fn.url}/object`).pipe(
84 Effect.flatMap((response) => response.json),
85 Effect.flatMap(
86 Schema.decodeUnknownEffect(
87 Schema.Struct({ greeting: Schema.String }),
88 ),
89 ),
90 );
91 expect(object.greeting.length).toBeGreaterThan(0);
92 }
93 const credentials = yield* SDK.listCredentials({
94 project_id: deployed.branch.projectId,
95 branch_id: deployed.branch.branchId,
96 });
97 expect(
98 credentials.credentials.some(
99 (entry) =>
100 !entry.revoked_at && entry.scopes.includes("ai_gateway:invoke"),
101 ),
102 ).toBe(true);
103 }).pipe(Effect.ensuring(stack.destroy().pipe(Effect.orDie)));
104 expect(
105 yield* SDK.getProjectBranch({
106 project_id: deployed.branch.projectId,
107 branch_id: deployed.branch.branchId,
108 }).pipe(
109 Effect.as(false),
110 Effect.catchTag("NotFound", () => Effect.succeed(true)),
111 ),
112 ).toBe(true);
113 yield* stack.destroy();
114 }),
115 {
116 tags: [
117 "provider:neon",
118 "provider:neon:aigateway",
119 "provider:neon:branch",
120 "provider:neon:credential",
121 "provider:neon:function",
122 "provider:neon:languagemodel",
123 "provider:neon:project",
124 "live",
125 ],
126 timeout: 120_000,
127 },
128);