1import * as aiplatform from "@distilled.cloud/gcp/aiplatform_v1";
2import { Credentials } from "@distilled.cloud/gcp/Credentials";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import { GcpProjectMissing } from "../Environment.ts";
6import { bindGcpHost } from "../Host.ts";
7import {
8 GenerateContent,
9 type GenerateContentClient,
10 type GenerateContentRequest,
11 type PublisherModel,
12} from "./GenerateContent.ts";
13
14const TAG = "GCP.AIPlatform.GenerateContent";
15
16/**
17 * HTTP implementation of {@link GenerateContent}.
18 *
19 * @layer
20 * @provides GCP.AIPlatform.GenerateContent
21 */
22export const GenerateContentHttp = Layer.effect(
23 GenerateContent,
24 Effect.gen(function* () {
25 const generateContent =
26 yield* aiplatform.generateContentProjectsLocationsPublishersModels;
27 // The project is the caller's own: deploy credentials at plan time,
28 // the metadata server inside Cloud Run / Cloud Functions.
29 const credentials = yield* Credentials;
30 const project = credentials.pipe(
31 Effect.flatMap(({ project }) =>
32 project
33 ? Effect.succeed(project)
34 : Effect.fail(
35 new GcpProjectMissing({
36 message: `${TAG}: no GCP project id in the runtime credentials`,
37 }),
38 ),
39 ),
40 );
41
42 return Effect.fn(function* (target: string | PublisherModel) {
43 const {
44 model,
45 location = "global",
46 publisher = "google",
47 } = typeof target === "string" ? { model: target } : target;
48 const id = `${publisher}/${model}@${location}`;
49
50 yield* bindGcpHost({
51 tag: TAG,
52 resource: { LogicalId: id },
53 // Publisher models have no resource IAM policy; aiplatform.user is
54 // the narrowest predefined role carrying aiplatform.endpoints.predict.
55 iam: [{ role: "roles/aiplatform.user" }],
56 });
57
58 const generate = Effect.fn(`${TAG}(${id})`)(function* (
59 request: GenerateContentRequest,
60 ) {
61 return yield* generateContent({
62 model: `projects/${yield* project}/locations/${location}/publishers/${publisher}/models/${model}`,
63 body: request,
64 });
65 });
66
67 const client: GenerateContentClient = {
68 generate,
69 text: (prompt, options) =>
70 generate({
71 ...options,
72 contents: [{ role: "user", parts: [{ text: prompt }] }],
73 }).pipe(
74 Effect.map((response) =>
75 (response.candidates?.[0]?.content?.parts ?? [])
76 .filter((part) => !part.thought)
77 .map((part) => part.text ?? "")
78 .join(""),
79 ),
80 ),
81 };
82 return client;
83 });
84 }),
85);