1import * as translate from "@distilled.cloud/gcp/translate_v3";
2import * as Effect from "effect/Effect";
3import { Unowned } from "../../AdoptPolicy.ts";
4import { isResolved } from "../../Diff.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import { GcpEnvironment } from "../Environment.ts";
8import type { Providers } from "../Providers.ts";
9import {
10 DEFAULT_LOCATION,
11 ResourceNotResolved,
12 expandParent,
13 locationParent,
14 normalizeLocation,
15 parseResourceName,
16 replaceOnIdentity,
17 resourceNameOf,
18 retryTransient,
19 waitUntilGone,
20 toRestrictedDisplayName,
21} from "./internal.ts";
22import { waitForOperation } from "./operations.ts";
23
24export type ModelProps = {
25 /**
26 * Server-assigned model id (the `{model}` segment of
27 * `projects/{project}/locations/{location}/models/{model}`, e.g.
28 * `NM…`) of an existing model to manage. Omit to train a new model.
29 * Immutable — changing it replaces the model.
30 */
31 modelId?: string;
32 /**
33 * Location of the model (`us-central1`, `europe-west1`, …). Custom
34 * AutoML Translation models are regional. Immutable — changing it
35 * replaces the model.
36 * @default "us-central1"
37 */
38 location?: string;
39 /**
40 * Training dataset resource name
41 * `projects/{project}/locations/{location}/datasets/{dataset}` or the
42 * dataset id (combined with `location`). Immutable — changing it
43 * replaces the model.
44 */
45 dataset: string;
46 /**
47 * User-facing display name. There is no update API — changing display name
48 * replaces the model.
49 */
50 displayName?: string;
51};
52
53export type Model = Resource<
54 "GCP.Translate.Model",
55 ModelProps,
56 {
57 /** Full resource name `projects/{project}/locations/{location}/models/{model}`. */
58 name: string;
59 /** Model id (last path segment). */
60 modelId: string;
61 /** Project id. */
62 project: string;
63 /** Location id. */
64 location: string;
65 /** Training dataset resource name. */
66 dataset: string | undefined;
67 /** Display name. */
68 displayName: string | undefined;
69 /** BCP-47 source language copied from the dataset. */
70 sourceLanguageCode: string | undefined;
71 /** BCP-47 target language copied from the dataset. */
72 targetLanguageCode: string | undefined;
73 /** Number of sentence pairs used to train the model. */
74 trainExampleCount: number | undefined;
75 /** Number of sentence pairs used to validate the model. */
76 validateExampleCount: number | undefined;
77 /** Number of sentence pairs used to test the model. */
78 testExampleCount: number | undefined;
79 /** RFC3339 creation timestamp (also when training started). */
80 createTime: string | undefined;
81 /** RFC3339 last-update timestamp. */
82 updateTime: string | undefined;
83 },
84 never,
85 Providers
86>;
87
88/**
89 * A trained Cloud Translation AutoML model.
90 *
91 * Model ids are assigned by Google and models have no labels, so Alchemy
92 * tracks a model only through the name recorded in state; `read` reports a
93 * model it finds without prior state as unowned (adopt it with `--adopt`).
94 * Models are trained from a Translation dataset in the same location. Create
95 * and delete are long-running operations. Dataset, location, and display name
96 * are immutable — there is no patch RPC. Training can take hours; tests
97 * skipIf-gate behind `FAST`.
98 *
99 * ### Creating a Model
100 * **Example:** Train from a dataset
101 * ```typescript
102 * const model = yield* GCP.Translate.Model("EnEs", {
103 * dataset: dataset.name,
104 * displayName: "enes",
105 * });
106 * ```
107 *
108 * **Example:** Explicit location
109 * ```typescript
110 * const model = yield* GCP.Translate.Model("EnEs", {
111 * location: "us-central1",
112 * dataset: dataset.name,
113 * });
114 * ```
115 *
116 * @resource
117 * @category Translate
118 */
119export const Model = Resource<Model>("GCP.Translate.Model");
120
121const resourceName = (project: string, location: string, modelId: string) =>
122 resourceNameOf(locationParent(project, location), "models", modelId);
123
124const datasetNameOf = (project: string, location: string, dataset: string) =>
125 expandParent(dataset, project, location, "datasets");
126
127const toAttrs = (model: translate.Model, project: string) => {
128 const name = model.name ?? "";
129 const parsed = parseResourceName(name, "models");
130 return {
131 name,
132 modelId: parsed.id,
133 project: parsed.project || project,
134 location: parsed.location,
135 dataset: model.dataset,
136 displayName: model.displayName,
137 sourceLanguageCode: model.sourceLanguageCode,
138 targetLanguageCode: model.targetLanguageCode,
139 trainExampleCount: model.trainExampleCount,
140 validateExampleCount: model.validateExampleCount,
141 testExampleCount: model.testExampleCount,
142 createTime: model.createTime,
143 updateTime: model.updateTime,
144 };
145};
146
147const getByName = (name: string) =>
148 name.length === 0
149 ? Effect.succeed(undefined)
150 : translate
151 .getProjectsLocationsModels({ name })
152 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
153
154export const ModelProvider = () =>
155 Provider.succeed(Model, {
156 stables: ["name", "modelId", "project", "location", "createTime"],
157
158 diff: Effect.fn(function* ({ news, olds, output }) {
159 if (!isResolved(news)) return undefined;
160 const previousDataset = olds?.dataset ?? output?.dataset;
161 const extra =
162 (previousDataset !== undefined &&
163 news.dataset !== previousDataset &&
164 !news.dataset.endsWith(`/${previousDataset}`) &&
165 !(previousDataset ?? "").endsWith(`/${news.dataset}`)) ||
166 (news.displayName !== undefined &&
167 (olds?.displayName ?? output?.displayName) !== undefined &&
168 news.displayName !== (olds?.displayName ?? output?.displayName));
169 return replaceOnIdentity({
170 previousId: olds?.modelId ?? output?.modelId,
171 nextId: news.modelId,
172 previousLocation: olds?.location ?? output?.location,
173 nextLocation: news.location ?? olds?.location ?? output?.location,
174 extra,
175 });
176 }),
177
178 read: Effect.fn(function* ({ olds, output }) {
179 const env = yield* GcpEnvironment.current;
180 const location = normalizeLocation(olds?.location ?? output?.location);
181 const modelId = olds?.modelId ?? output?.modelId;
182 const name =
183 output?.name ??
184 (modelId ? resourceName(env.project, location, modelId) : "");
185 const existing = yield* getByName(name);
186 if (existing === undefined) return undefined;
187 const attrs = toAttrs(existing, env.project);
188 // No labels field: without prior state it may not be ours.
189 return output !== undefined ? attrs : Unowned(attrs);
190 }),
191
192 reconcile: Effect.fn(function* ({ id, news, output }) {
193 const env = yield* GcpEnvironment.current;
194 const location = normalizeLocation(
195 news.location ?? output?.location ?? DEFAULT_LOCATION,
196 );
197 const parent = locationParent(env.project, location);
198 const dataset = datasetNameOf(env.project, location, news.dataset);
199 const displayName = toRestrictedDisplayName(news.displayName ?? id);
200 const hinted =
201 output?.name ??
202 (news.modelId ? resourceName(env.project, location, news.modelId) : "");
203
204 let current = yield* getByName(hinted);
205
206 if (current === undefined) {
207 const operation = yield* retryTransient(
208 translate.createProjectsLocationsModels({
209 parent,
210 body: { displayName, dataset },
211 }),
212 );
213 // Training a custom model takes hours.
214 const done = yield* waitForOperation(operation, { budget: "12 hours" });
215 const createdName = done.response?.name;
216 current = yield* getByName(
217 typeof createdName === "string" ? createdName : "",
218 );
219 }
220
221 if (current === undefined) {
222 return yield* new ResourceNotResolved({ name: hinted || parent });
223 }
224
225 return toAttrs(current, env.project);
226 }),
227
228 delete: Effect.fn(function* ({ output }) {
229 if (!output.name) return;
230 const operation = yield* retryTransient(
231 translate.deleteProjectsLocationsModels({
232 name: output.name,
233 }),
234 ).pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
235 if (operation !== undefined && "done" in operation) {
236 yield* waitForOperation(operation, { notFoundOk: true });
237 }
238 yield* waitUntilGone(getByName(output.name), output.name);
239 }),
240 });