From 85f1168d1d7ddff37a04c2f060cb5c29671c4de3 Mon Sep 17 00:00:00 2001 From: Taitranz Date: Sat, 11 Apr 2026 09:37:48 +1000 Subject: [PATCH] feat(ai): add Google Vertex AI provider --- packages/ai/google-vertex/docgen.json | 34 ++ packages/ai/google-vertex/package.json | 63 ++++ .../google-vertex/src/GoogleVertexClient.ts | 265 +++++++++++++++ .../google-vertex/src/GoogleVertexConfig.ts | 56 ++++ .../src/GoogleVertexLanguageModel.ts | 312 ++++++++++++++++++ .../ai/google-vertex/src/GoogleVertexTool.ts | 9 + packages/ai/google-vertex/src/index.ts | 19 ++ packages/ai/google-vertex/tsconfig.build.json | 17 + packages/ai/google-vertex/tsconfig.json | 8 + packages/ai/google-vertex/tsconfig.src.json | 17 + packages/ai/google-vertex/tsconfig.test.json | 14 + packages/ai/google-vertex/vitest.config.ts | 6 + packages/ai/google/src/GoogleLanguageModel.ts | 34 +- pnpm-lock.yaml | 22 ++ tsconfig.base.json | 1 + tsconfig.json | 1 + 16 files changed, 874 insertions(+), 4 deletions(-) create mode 100644 packages/ai/google-vertex/docgen.json create mode 100644 packages/ai/google-vertex/package.json create mode 100644 packages/ai/google-vertex/src/GoogleVertexClient.ts create mode 100644 packages/ai/google-vertex/src/GoogleVertexConfig.ts create mode 100644 packages/ai/google-vertex/src/GoogleVertexLanguageModel.ts create mode 100644 packages/ai/google-vertex/src/GoogleVertexTool.ts create mode 100644 packages/ai/google-vertex/src/index.ts create mode 100644 packages/ai/google-vertex/tsconfig.build.json create mode 100644 packages/ai/google-vertex/tsconfig.json create mode 100644 packages/ai/google-vertex/tsconfig.src.json create mode 100644 packages/ai/google-vertex/tsconfig.test.json create mode 100644 packages/ai/google-vertex/vitest.config.ts diff --git a/packages/ai/google-vertex/docgen.json b/packages/ai/google-vertex/docgen.json new file mode 100644 index 00000000000..3c125f7c08a --- /dev/null +++ b/packages/ai/google-vertex/docgen.json @@ -0,0 +1,34 @@ +{ + "$schema": "../../../node_modules/@effect/docgen/schema.json", + "exclude": [ + "src/internal/**/*.ts" + ], + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/ai/google-vertex/src/", + "examplesCompilerOptions": { + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "moduleResolution": "Bundler", + "module": "ES2022", + "target": "ES2022", + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], + "paths": { + "effect": ["../../../../effect/src/index.js"], + "effect/*": ["../../../../effect/src/*.js"], + "@effect/experimental": ["../../../../experimental/src/index.js"], + "@effect/experimental/*": ["../../../../experimental/src/*.js"], + "@effect/platform": ["../../../../platform/src/index.js"], + "@effect/platform/*": ["../../../../platform/src/*.js"], + "@effect/ai": ["../../../ai/src/index.js"], + "@effect/ai/*": ["../../../ai/src/*.js"], + "@effect/ai-google": ["../../../google/src/index.js"], + "@effect/ai-google/*": ["../../../google/src/*.js"], + "@effect/ai-google-vertex": ["../../../google-vertex/src/index.js"], + "@effect/ai-google-vertex/*": ["../../../google-vertex/src/*.js"] + } + } +} diff --git a/packages/ai/google-vertex/package.json b/packages/ai/google-vertex/package.json new file mode 100644 index 00000000000..9592f66e667 --- /dev/null +++ b/packages/ai/google-vertex/package.json @@ -0,0 +1,63 @@ +{ + "name": "@effect/ai-google-vertex", + "type": "module", + "version": "0.1.0", + "license": "MIT", + "description": "Effect modules for working with Google Vertex AI APIs", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/ai/google-vertex" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "keywords": [ + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "directory": "dist", + "linkDirectory": false + }, + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null + }, + "scripts": { + "codegen": "build-utils prepare-v3", + "build": "pnpm build-esm && pnpm build-annotate && pnpm build-cjs && build-utils pack-v3", + "build-esm": "tsc -b tsconfig.build.json", + "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "build-annotate": "babel build/esm --plugins annotate-pure-calls --out-dir build/esm --source-maps", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "peerDependencies": { + "@effect/ai": "workspace:^", + "@effect/ai-google": "workspace:^", + "@effect/experimental": "workspace:^", + "@effect/platform": "workspace:^", + "effect": "workspace:^" + }, + "devDependencies": { + "@effect/ai": "workspace:^", + "@effect/ai-google": "workspace:^", + "@effect/experimental": "workspace:^", + "@effect/platform": "workspace:^", + "@effect/platform-node": "workspace:^", + "effect": "workspace:^" + } +} diff --git a/packages/ai/google-vertex/src/GoogleVertexClient.ts b/packages/ai/google-vertex/src/GoogleVertexClient.ts new file mode 100644 index 00000000000..977b2feb260 --- /dev/null +++ b/packages/ai/google-vertex/src/GoogleVertexClient.ts @@ -0,0 +1,265 @@ +/** + * @since 1.0.0 + */ +import * as Generated from "@effect/ai-google/Generated" +import * as AiError from "@effect/ai/AiError" +import * as Sse from "@effect/experimental/Sse" +import * as Headers from "@effect/platform/Headers" +import * as HttpBody from "@effect/platform/HttpBody" +import * as HttpClient from "@effect/platform/HttpClient" +import * as HttpClientRequest from "@effect/platform/HttpClientRequest" +import * as UrlParams from "@effect/platform/UrlParams" +import * as Arr from "effect/Array" +import * as Chunk from "effect/Chunk" +import * as Config from "effect/Config" +import type { ConfigError } from "effect/ConfigError" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { identity } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Predicate from "effect/Predicate" +import type * as Redacted from "effect/Redacted" +import * as Schema from "effect/Schema" +import type * as Scope from "effect/Scope" +import * as Stream from "effect/Stream" +import { GoogleVertexConfig } from "./GoogleVertexConfig.js" + +/** + * @since 1.0.0 + * @category Context + */ +export class GoogleVertexClient extends Context.Tag( + "@effect/ai-google-vertex/GoogleVertexClient" +)() {} + +/** + * @since 1.0.0 + * @category Models + */ +export interface Service { + readonly streamRequest: ( + request: HttpClientRequest.HttpClientRequest, + schema: Schema.Schema + ) => Stream.Stream + + readonly generateContent: ( + request: typeof Generated.GenerateContentRequest.Encoded + ) => Effect.Effect + + readonly generateContentStream: ( + request: typeof Generated.GenerateContentRequest.Encoded + ) => Stream.Stream +} + +/** + * @since 1.0.0 + * @category Constructors + */ +export const make = (options: { + /** + * The GCP project ID. + */ + readonly project: string + + /** + * The GCP location / region (e.g. `"us-central1"`). + */ + readonly location: string + + /** + * An OAuth2 access token for authenticating with Vertex AI. + * + * Sent as `Authorization: Bearer `. When omitted, authentication + * must be handled via `transformClient` (e.g. injecting a fresh token on + * each request from ADC or a token-refresh Effect). + */ + readonly accessToken?: Redacted.Redacted | undefined + + /** + * The Vertex AI API version. + * + * Defaults to `"v1"`. + */ + readonly apiVersion?: "v1" | "v1beta1" | undefined + + /** + * Override the base URL for the Vertex AI API. + * + * Defaults to the regional endpoint: + * `https://{location}-aiplatform.googleapis.com` + */ + readonly apiUrl?: string | undefined + + /** + * A method which can be used to transform the underlying `HttpClient` which + * will be used to communicate with the Vertex AI API. + */ + readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined +}): Effect.Effect => + Effect.gen(function*() { + const authHeader = "authorization" + + yield* Effect.locallyScopedWith(Headers.currentRedactedNames, Arr.append(authHeader)) + + const baseUrl = options.apiUrl ?? `https://${options.location}-aiplatform.googleapis.com` + const apiVersion = options.apiVersion ?? "v1" + const pathPrefix = + `/${apiVersion}/projects/${options.project}/locations/${options.location}/publishers/google/models` + + let httpClient = (yield* HttpClient.HttpClient).pipe( + HttpClient.mapRequest((request) => + request.pipe( + HttpClientRequest.prependUrl(baseUrl), + options.accessToken + ? (r) => HttpClientRequest.bearerToken(r, options.accessToken!) + : identity, + HttpClientRequest.acceptJson + ) + ) + ) + + httpClient = options.transformClient ? options.transformClient(httpClient) : httpClient + + const httpClientOk = HttpClient.filterStatusOk(httpClient) + + const streamRequest = ( + request: HttpClientRequest.HttpClientRequest, + schema: Schema.Schema + ): Stream.Stream => { + const decodeEvents = Schema.decode(Schema.ChunkFromSelf(Schema.parseJson(schema))) + return httpClientOk.execute(request).pipe( + Effect.map((r) => r.stream), + Stream.unwrap, + Stream.decodeText(), + Stream.pipeThroughChannel(Sse.makeChannel()), + Stream.mapChunksEffect((chunk) => decodeEvents(Chunk.map(chunk, (event) => event.data))), + Stream.catchTags({ + RequestError: (error) => + AiError.HttpRequestError.fromRequestError({ + module: "GoogleVertexClient", + method: "streamRequest", + error + }), + ResponseError: (error) => + AiError.HttpResponseError.fromResponseError({ + module: "GoogleVertexClient", + method: "streamRequest", + error + }), + ParseError: (error) => + AiError.MalformedOutput.fromParseError({ + module: "GoogleVertexClient", + method: "streamRequest", + error + }) + }) + ) + } + + const decodeResponse = Schema.decodeUnknown(Generated.GenerateContentResponse) + + const generateContent: ( + request: typeof Generated.GenerateContentRequest.Encoded + ) => Effect.Effect = Effect.fnUntraced( + function*(request) { + const config = yield* GoogleVertexConfig.getOrUndefined + const effectiveClient = config?.transformClient + ? HttpClient.filterStatusOk(config.transformClient(httpClient)) + : httpClientOk + const url = `${pathPrefix}/${request.model}:generateContent` + const httpRequest = HttpClientRequest.post(url, { + body: HttpBody.unsafeJson(request) + }) + return yield* effectiveClient.execute(httpRequest).pipe( + Effect.flatMap((r) => r.json), + Effect.flatMap(decodeResponse), + Effect.scoped, + Effect.catchTags({ + RequestError: (error) => + AiError.HttpRequestError.fromRequestError({ + module: "GoogleVertexClient", + method: "generateContent", + error + }), + ResponseError: (error) => + AiError.HttpResponseError.fromResponseError({ + module: "GoogleVertexClient", + method: "generateContent", + error + }), + ParseError: (error) => + AiError.MalformedOutput.fromParseError({ + module: "GoogleVertexClient", + method: "generateContent", + error + }) + }) + ) + } + ) + + const generateContentStream = ( + request: typeof Generated.GenerateContentRequest.Encoded + ): Stream.Stream => { + const url = `${pathPrefix}/${request.model}:streamGenerateContent` + const httpRequest = HttpClientRequest.post(url, { + urlParams: UrlParams.fromInput({ "alt": "sse" }), + body: HttpBody.unsafeJson(request) + }) + return streamRequest(httpRequest, Generated.GenerateContentResponse).pipe( + Stream.takeUntil(hasFinishReason) + ) + } + + return GoogleVertexClient.of({ + streamRequest, + generateContent, + generateContentStream + }) + }) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layer = (options: { + readonly project: string + readonly location: string + readonly accessToken?: Redacted.Redacted | undefined + readonly apiVersion?: "v1" | "v1beta1" | undefined + readonly apiUrl?: string | undefined + readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined +}): Layer.Layer< + GoogleVertexClient, + never, + HttpClient.HttpClient +> => Layer.scoped(GoogleVertexClient, make(options)) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layerConfig = ( + options: { + readonly project: Config.Config + readonly location: Config.Config + readonly accessToken?: Config.Config | undefined + readonly apiVersion?: Config.Config<"v1" | "v1beta1" | undefined> | undefined + readonly apiUrl?: Config.Config | undefined + readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined + } +): Layer.Layer => { + const { transformClient, ...configs } = options + return Config.all(configs).pipe( + Effect.flatMap((configs) => make({ ...configs, transformClient })), + Layer.scoped(GoogleVertexClient) + ) +} + +// ============================================================================= +// Utilities +// ============================================================================= + +const hasFinishReason = (event: Generated.GenerateContentResponse): boolean => + Predicate.isNotUndefined(event.candidates) && + event.candidates.some((candidate) => Predicate.isNotUndefined(candidate.finishReason)) diff --git a/packages/ai/google-vertex/src/GoogleVertexConfig.ts b/packages/ai/google-vertex/src/GoogleVertexConfig.ts new file mode 100644 index 00000000000..34c939308c5 --- /dev/null +++ b/packages/ai/google-vertex/src/GoogleVertexConfig.ts @@ -0,0 +1,56 @@ +/** + * @since 1.0.0 + */ +import type { HttpClient } from "@effect/platform/HttpClient" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { dual } from "effect/Function" + +/** + * @since 1.0.0 + * @category Context + */ +export class GoogleVertexConfig extends Context.Tag("@effect/ai-google-vertex/GoogleVertexConfig")< + GoogleVertexConfig, + GoogleVertexConfig.Service +>() { + /** + * @since 1.0.0 + */ + static readonly getOrUndefined: Effect.Effect = Effect.map( + Effect.context(), + (context) => context.unsafeMap.get(GoogleVertexConfig.key) + ) +} + +/** + * @since 1.0.0 + */ +export declare namespace GoogleVertexConfig { + /** + * @since 1.0.0 + * @category Models + */ + export interface Service { + readonly transformClient?: (client: HttpClient) => HttpClient + } +} + +/** + * @since 1.0.0 + * @category Configuration + */ +export const withClientTransform: { + (transform: (client: HttpClient) => HttpClient): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, transform: (client: HttpClient) => HttpClient): Effect.Effect +} = dual< + (transform: (client: HttpClient) => HttpClient) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, transform: (client: HttpClient) => HttpClient) => Effect.Effect +>( + 2, + (self, transformClient) => + Effect.flatMap( + GoogleVertexConfig.getOrUndefined, + (config) => Effect.provideService(self, GoogleVertexConfig, { ...config, transformClient }) + ) +) diff --git a/packages/ai/google-vertex/src/GoogleVertexLanguageModel.ts b/packages/ai/google-vertex/src/GoogleVertexLanguageModel.ts new file mode 100644 index 00000000000..fad10897cd9 --- /dev/null +++ b/packages/ai/google-vertex/src/GoogleVertexLanguageModel.ts @@ -0,0 +1,312 @@ +/** + * @since 1.0.0 + */ +import type * as Generated from "@effect/ai-google/Generated" +import { + jsonSchemaToOpenApiSchema, + makeResponse, + makeStreamResponse, + prepareMessages, + prepareTools +} from "@effect/ai-google/GoogleLanguageModel" +import * as LanguageModel from "@effect/ai/LanguageModel" +import * as AiModel from "@effect/ai/Model" +import type * as Response from "@effect/ai/Response" +import { addGenAIAnnotations } from "@effect/ai/Telemetry" +import * as Tool from "@effect/ai/Tool" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Predicate from "effect/Predicate" +import * as Stream from "effect/Stream" +import type { Span } from "effect/Tracer" +import type { Simplify } from "effect/Types" +import { GoogleVertexClient } from "./GoogleVertexClient.js" + +/** + * @since 1.0.0 + * @category Models + */ +export type Model = string + +// ============================================================================= +// Configuration +// ============================================================================= + +/** + * @since 1.0.0 + * @category Context + */ +export class Config extends Context.Tag("@effect/ai-google-vertex/GoogleVertexLanguageModel/Config")< + Config, + Config.Service +>() { + /** + * @since 1.0.0 + */ + static readonly getOrUndefined: Effect.Effect = Effect.map( + Effect.context(), + (context) => context.unsafeMap.get(Config.key) + ) +} + +/** + * @since 1.0.0 + */ +export declare namespace Config { + /** + * @since 1.0.0 + * @category Models + */ + export interface Service extends + Simplify< + Partial< + Omit< + typeof Generated.GenerateContentRequest.Encoded, + "contents" | "tools" | "toolConfig" | "systemInstruction" + > + > + > + { + readonly toolConfig: Partial<{ + readonly functionCallingConfig: Omit< + typeof Generated.FunctionCallingConfig.Encoded, + "mode" + > + }> + } +} + +// ============================================================================= +// Provider Options / Metadata +// ============================================================================= + +declare module "@effect/ai/Prompt" { + export interface ReasoningPartOptions { + readonly google?: { + readonly thoughtSignature?: string | undefined + } | undefined + } + + export interface TextPartOptions { + readonly google?: { + readonly thoughtSignature?: string | undefined + } | undefined + } + + export interface ToolCallPartOptions { + readonly google?: { + readonly thoughtSignature?: string | undefined + } | undefined + } +} + +declare module "@effect/ai/Response" { + export interface TextStartPartMetadata { + readonly google?: { + readonly thoughtSignature?: string | undefined + } | undefined + } + + export interface TextDeltaPartMetadata { + readonly google?: { + readonly thoughtSignature?: string | undefined + } | undefined + } + + export interface ReasoningPartMetadata { + readonly google?: { + readonly thoughtSignature?: string | undefined + } | undefined + } + + export interface ReasoningStartPartMetadata { + readonly google?: { + readonly thoughtSignature?: string | undefined + } | undefined + } + + export interface ReasoningDeltaPartMetadata { + readonly google?: { + readonly thoughtSignature?: string | undefined + } | undefined + } + + export interface ToolParamsStartPartMetadata { + readonly google?: { + readonly thoughtSignature?: string | undefined + } | undefined + } + + export interface ToolParamsDeltaPartMetadata { + readonly google?: { + readonly thoughtSignature?: string | undefined + } | undefined + } + + export interface ToolCallPartMetadata { + readonly google?: { + readonly thoughtSignature?: string | undefined + } | undefined + } + + export interface FinishPartMetadata { + readonly google?: { + readonly groundingMetadata?: Generated.GroundingMetadata | undefined + readonly safetyRatings?: ReadonlyArray | undefined + readonly urlContextMetadata?: Generated.UrlContextMetadata | undefined + readonly usageMetadata?: Generated.UsageMetadata | undefined + } | undefined + } +} + +// ============================================================================= +// Language Model +// ============================================================================= + +/** + * @since 1.0.0 + * @category AiModel + */ +export const model = ( + model: (string & {}) | Model, + config?: Omit +): AiModel.Model<"google-vertex", LanguageModel.LanguageModel, GoogleVertexClient> => + AiModel.make("google-vertex", layer({ model, config })) + +/** + * @since 1.0.0 + * @category Constructors + */ +export const make = Effect.fnUntraced(function*(options: { + readonly model: (string & {}) | Model + readonly config?: Omit +}) { + const client = yield* GoogleVertexClient + + const makeRequest = Effect.fnUntraced( + function*(providerOptions: LanguageModel.ProviderOptions) { + const context = yield* Effect.context() + const config = { model: options.model, ...options.config, ...context.unsafeMap.get(Config.key) } + const { messages, system } = yield* prepareMessages(providerOptions, config) + const { toolConfig, tools } = yield* prepareTools(providerOptions, config) + const responseFormat = providerOptions.responseFormat + const responseMimeType = responseFormat.type === "json" ? "application/json" : undefined + const responseSchema = responseFormat.type === "json" + ? jsonSchemaToOpenApiSchema(Tool.getJsonSchemaFromSchemaAst(responseFormat.schema.ast)) + : undefined + const request: typeof Generated.GenerateContentRequest.Encoded = { + ...config, + systemInstruction: system, + contents: messages, + tools, + toolConfig, + generationConfig: { + ...config.generationConfig, + responseMimeType, + responseSchema + } + } + return request + } + ) + + return yield* LanguageModel.make({ + generateText: Effect.fnUntraced( + function*(options) { + const request = yield* makeRequest(options) + annotateRequest(options.span, request) + const rawResponse = yield* client.generateContent(request) + annotateResponse(options.span, rawResponse) + return yield* makeResponse(rawResponse) + } + ), + streamText: Effect.fnUntraced( + function*(options) { + const request = yield* makeRequest(options) + annotateRequest(options.span, request) + return client.generateContentStream(request) + }, + (effect, options) => + effect.pipe( + Effect.flatMap((stream) => makeStreamResponse(stream)), + Stream.unwrap, + Stream.map((response) => { + annotateStreamResponse(options.span, response) + return response + }) + ) + ) + }) +}) + +/** + * @since 1.0.0 + * @category Layers + */ +export const layer = (options: { + readonly model: (string & {}) | Model + readonly config?: Omit +}): Layer.Layer => + Layer.effect(LanguageModel.LanguageModel, make({ model: options.model, config: options.config })) + +// ============================================================================= +// Telemetry +// ============================================================================= + +const annotateRequest = (span: Span, request: typeof Generated.GenerateContentRequest.Encoded): void => { + addGenAIAnnotations(span, { + system: "gcp.vertex_ai", + operation: { name: "chat" }, + request: { + model: request.model, + temperature: request.generationConfig?.temperature, + topP: request.generationConfig?.topP, + maxTokens: request.generationConfig?.maxOutputTokens, + stopSequences: request.generationConfig?.stopSequences ?? [] + } + }) +} + +const annotateResponse = (span: Span, response: typeof Generated.GenerateContentResponse.Type): void => { + const finishReasons: Array = [] + if (Predicate.isNotNullable(response.candidates)) { + for (const candidate of response.candidates) { + if (Predicate.isNotNullable(candidate.finishReason)) { + finishReasons.push(candidate.finishReason) + } + } + } + addGenAIAnnotations(span, { + response: { + model: response.modelVersion, + finishReasons: finishReasons.length > 0 ? finishReasons : undefined + }, + usage: { + inputTokens: response.usageMetadata?.promptTokenCount, + outputTokens: response.usageMetadata?.candidatesTokenCount + } + }) +} + +const annotateStreamResponse = (span: Span, part: Response.StreamPartEncoded): void => { + if (part.type === "response-metadata") { + addGenAIAnnotations(span, { + response: { + id: part.id, + model: part.modelId + } + }) + } + if (part.type === "finish") { + addGenAIAnnotations(span, { + response: { + finishReasons: [part.reason] + }, + usage: { + inputTokens: part.usage.inputTokens, + outputTokens: part.usage.outputTokens + } + }) + } +} diff --git a/packages/ai/google-vertex/src/GoogleVertexTool.ts b/packages/ai/google-vertex/src/GoogleVertexTool.ts new file mode 100644 index 00000000000..d59b2920e20 --- /dev/null +++ b/packages/ai/google-vertex/src/GoogleVertexTool.ts @@ -0,0 +1,9 @@ +/** + * @since 1.0.0 + */ + +/** + * @since 1.0.0 + * @category Tools + */ +export { CodeExecution, GoogleSearch, GoogleSearchRetrieval, UrlContext } from "@effect/ai-google/GoogleTool" diff --git a/packages/ai/google-vertex/src/index.ts b/packages/ai/google-vertex/src/index.ts new file mode 100644 index 00000000000..7b626533c30 --- /dev/null +++ b/packages/ai/google-vertex/src/index.ts @@ -0,0 +1,19 @@ +/** + * @since 1.0.0 + */ +export * as GoogleVertexClient from "./GoogleVertexClient.js" + +/** + * @since 1.0.0 + */ +export * as GoogleVertexConfig from "./GoogleVertexConfig.js" + +/** + * @since 1.0.0 + */ +export * as GoogleVertexLanguageModel from "./GoogleVertexLanguageModel.js" + +/** + * @since 1.0.0 + */ +export * as GoogleVertexTool from "./GoogleVertexTool.js" diff --git a/packages/ai/google-vertex/tsconfig.build.json b/packages/ai/google-vertex/tsconfig.build.json new file mode 100644 index 00000000000..9a18f813a3b --- /dev/null +++ b/packages/ai/google-vertex/tsconfig.build.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.src.json", + "references": [ + { "path": "../ai/tsconfig.build.json" }, + { "path": "../google/tsconfig.build.json" }, + { "path": "../../effect/tsconfig.build.json" }, + { "path": "../../experimental/tsconfig.build.json" }, + { "path": "../../platform/tsconfig.build.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", + "outDir": "build/esm", + "declarationDir": "build/dts", + "stripInternal": true, + "exactOptionalPropertyTypes": false + } +} diff --git a/packages/ai/google-vertex/tsconfig.json b/packages/ai/google-vertex/tsconfig.json new file mode 100644 index 00000000000..f4464966e7d --- /dev/null +++ b/packages/ai/google-vertex/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": [], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "tsconfig.test.json" } + ] +} diff --git a/packages/ai/google-vertex/tsconfig.src.json b/packages/ai/google-vertex/tsconfig.src.json new file mode 100644 index 00000000000..a55b58ba602 --- /dev/null +++ b/packages/ai/google-vertex/tsconfig.src.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../ai/tsconfig.src.json" }, + { "path": "../google/tsconfig.src.json" }, + { "path": "../../effect/tsconfig.src.json" }, + { "path": "../../experimental/tsconfig.src.json" }, + { "path": "../../platform/tsconfig.src.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", + "rootDir": "src", + "outDir": "build/src", + "exactOptionalPropertyTypes": false + } +} diff --git a/packages/ai/google-vertex/tsconfig.test.json b/packages/ai/google-vertex/tsconfig.test.json new file mode 100644 index 00000000000..95f3ae8f876 --- /dev/null +++ b/packages/ai/google-vertex/tsconfig.test.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["test"], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "../../vitest/tsconfig.src.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", + "rootDir": "test", + "noEmit": true, + "exactOptionalPropertyTypes": false + } +} diff --git a/packages/ai/google-vertex/vitest.config.ts b/packages/ai/google-vertex/vitest.config.ts new file mode 100644 index 00000000000..bf29895f858 --- /dev/null +++ b/packages/ai/google-vertex/vitest.config.ts @@ -0,0 +1,6 @@ +import { mergeConfig, type ViteUserConfig } from "vitest/config" +import shared from "../../../vitest.shared.js" + +const config: ViteUserConfig = {} + +export default mergeConfig(shared, config) diff --git a/packages/ai/google/src/GoogleLanguageModel.ts b/packages/ai/google/src/GoogleLanguageModel.ts index 760022de4bd..b176000a742 100644 --- a/packages/ai/google/src/GoogleLanguageModel.ts +++ b/packages/ai/google/src/GoogleLanguageModel.ts @@ -252,7 +252,11 @@ export const layer = (options: { // Prompt Conversion // ============================================================================= -const prepareMessages: ( +/** + * @since 1.0.0 + * @category Prompt Conversion + */ +export const prepareMessages: ( options: LanguageModel.ProviderOptions, config: Config.Service ) => Effect.Effect<{ @@ -418,7 +422,11 @@ const prepareMessages: ( // Response Conversion // ============================================================================= -const makeResponse: (response: Generated.GenerateContentResponse) => Effect.Effect< +/** + * @since 1.0.0 + * @category Response Conversion + */ +export const makeResponse: (response: Generated.GenerateContentResponse) => Effect.Effect< Array, AiError.AiError, IdGenerator.IdGenerator @@ -560,7 +568,11 @@ const makeResponse: (response: Generated.GenerateContentResponse) => Effect.Effe } ) -const makeStreamResponse: ( +/** + * @since 1.0.0 + * @category Response Conversion + */ +export const makeStreamResponse: ( stream: Stream.Stream ) => Effect.Effect< Stream.Stream, @@ -832,7 +844,11 @@ const annotateStreamResponse = (span: Span, part: Response.StreamPartEncoded): v // Tool Calling // ============================================================================= -const prepareTools: (options: LanguageModel.ProviderOptions, config: Config.Service) => Effect.Effect<{ +/** + * @since 1.0.0 + * @category Tool Calling + */ +export const prepareTools: (options: LanguageModel.ProviderOptions, config: Config.Service) => Effect.Effect<{ readonly tools: typeof Generated.Tool.Encoded | undefined readonly toolConfig: typeof Generated.ToolConfig.Encoded | undefined }, AiError.AiError> = Effect.fnUntraced(function*(options, config) { @@ -1007,3 +1023,13 @@ const getToolCalls = Effect.fnUntraced( return parts } ) + +// ============================================================================= +// Re-exports from internal utilities +// ============================================================================= + +/** + * @since 1.0.0 + * @category Utilities + */ +export { jsonSchemaToOpenApiSchema, resolveFinishReason } from "./internal/utilities.js" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5cb9346f9f..7b21ee099c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -261,6 +261,28 @@ importers: version: link:../../effect publishDirectory: dist + packages/ai/google-vertex: + devDependencies: + '@effect/ai': + specifier: workspace:^ + version: link:../ai + '@effect/ai-google': + specifier: workspace:^ + version: link:../google + '@effect/experimental': + specifier: workspace:^ + version: link:../../experimental + '@effect/platform': + specifier: workspace:^ + version: link:../../platform + '@effect/platform-node': + specifier: workspace:^ + version: link:../../platform-node + effect: + specifier: workspace:^ + version: link:../../effect + publishDirectory: dist + packages/ai/openai: dependencies: gpt-tokenizer: diff --git a/tsconfig.base.json b/tsconfig.base.json index 7cc6f2eb2da..b9c4495ee94 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -41,6 +41,7 @@ "effect", "@effect/ai", "@effect/ai-anthropic", + "@effect/ai-google-vertex", "@effect/ai-openai", "@effect/cli", "@effect/cluster", diff --git a/tsconfig.json b/tsconfig.json index a2560eb9b8d..6802cd3a02f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ { "path": "packages/ai/amazon-bedrock" }, { "path": "packages/ai/anthropic" }, { "path": "packages/ai/google" }, + { "path": "packages/ai/google-vertex" }, { "path": "packages/ai/openai" }, { "path": "packages/cli" }, { "path": "packages/cluster" },