diff --git a/packages/ai/src/protocols/mistral-chat.ts b/packages/ai/src/protocols/mistral-chat.ts index 44a86f64577..5f380859d2a 100644 --- a/packages/ai/src/protocols/mistral-chat.ts +++ b/packages/ai/src/protocols/mistral-chat.ts @@ -125,7 +125,7 @@ export type ProviderOptionsInput = { readonly [key: string]: unknown } -const MistralBody = Schema.Struct({ +export const MistralBody = Schema.Struct({ model: Schema.String, messages: Schema.Array(MistralMessage), tools: optionalArray(MistralTool), diff --git a/packages/ai/src/providers/google-vertex-mistral.ts b/packages/ai/src/providers/google-vertex-mistral.ts new file mode 100644 index 00000000000..2c9a9808090 --- /dev/null +++ b/packages/ai/src/providers/google-vertex-mistral.ts @@ -0,0 +1,109 @@ +import { Effect, Schema, Struct } from "effect" +import type { ProviderPackage } from "../provider-package.js" +import { MistralChat } from "../protocols/mistral-chat.js" +import { Auth } from "../route/auth.js" +import { Route, type RouteDefaultsInput } from "../route/client.js" +import { Endpoint } from "../route/endpoint.js" +import { Protocol } from "../route/protocol.js" +import { HttpTransport } from "../route/transport/index.js" +import { ProviderConfigurationError, ProviderID, type ModelID } from "../schema/index.js" +import { GoogleVertexShared } from "./google-vertex-shared.js" + +export const id = ProviderID.make("google-vertex") + +export type Config = RouteDefaultsInput & + GoogleVertexShared.OAuthOptions & { + readonly baseURL?: string + readonly location?: string + readonly project?: string + readonly providerOptions?: MistralChat.ProviderOptionsInput + } + +export type Settings = ProviderPackage.Settings & + MistralChat.ProviderOptionsInput & { + readonly accessToken?: string + readonly apiKey?: never + readonly baseURL?: string + readonly location?: string + readonly project?: string + } + +type Body = Omit + +// Mistral on Vertex takes the Mistral chat body, model included, through the publisher's streamRawPredict method. +// Vertex validates the body strictly and rejects prompt_cache_key, which Mistral's own API accepts. +const route = Route.make({ + id: "google-vertex-mistral", + provider: id, + providerMetadataKey: "vertex", + protocol: Protocol.make({ + id: MistralChat.protocol.id, + body: { + schema: Schema.Struct(Struct.omit(MistralChat.MistralBody.fields, ["prompt_cache_key"])), + from: (request) => + MistralChat.protocol.body.from(request).pipe(Effect.map((body) => Struct.omit(body, ["prompt_cache_key"]))), + }, + stream: MistralChat.protocol.stream, + }), + endpoint: Endpoint.path(({ request }) => `/${request.model.id}:streamRawPredict`), + auth: Auth.none, + transport: HttpTransport.sseJson.with().with({ framing: MistralChat.framing }), +}) + +export const routes = [route] + +const configuredRoute = (input: Config) => { + if ("apiKey" in input && input.apiKey !== undefined) + throw new ProviderConfigurationError({ provider: id, message: "Google Vertex Mistral does not support API keys" }) + const { + accessToken: _accessToken, + auth: _auth, + baseURL, + location: inputLocation, + project: inputProject, + ...rest + } = input + // Mistral is served from regional endpoints only; the global endpoint returns 404. + const location = GoogleVertexShared.location(inputLocation, "us-central1") + const project = GoogleVertexShared.project(inputProject) + return route.with({ + ...rest, + endpoint: { + baseURL: + baseURL ?? + `https://${GoogleVertexShared.host(location)}/v1/projects/${GoogleVertexShared.requireProject(project)}/locations/${location}/publishers/mistralai/models`, + }, + auth: GoogleVertexShared.oauth(input, project), + }) +} + +export const configure = (input: Config = {}) => { + const route = configuredRoute(input) + return { + id, + model: (modelID: string | ModelID) => route.model({ id: modelID }), + configure, + } +} + +export const provider = { + id, + configure, +} + +export const model: ProviderPackage.Definition["model"] = ( + modelID, + { accessToken, apiKey, baseURL, body, headers, location, project, ...providerOptions }, +) => { + if (apiKey !== undefined) + throw new ProviderConfigurationError({ provider: id, message: "Google Vertex Mistral does not support API keys" }) + return configure({ + accessToken, + baseURL, + headers, + http: body === undefined ? undefined : { body }, + location, + project, + providerOptions, + }).model(modelID) +} diff --git a/packages/ai/src/providers/google-vertex/mistral.ts b/packages/ai/src/providers/google-vertex/mistral.ts new file mode 100644 index 00000000000..33a1f4e4b05 --- /dev/null +++ b/packages/ai/src/providers/google-vertex/mistral.ts @@ -0,0 +1,2 @@ +export { model } from "../google-vertex-mistral.js" +export type { Settings } from "../google-vertex-mistral.js" diff --git a/packages/ai/src/providers/index.ts b/packages/ai/src/providers/index.ts index bfe3855a315..91e4ce4c9e7 100644 --- a/packages/ai/src/providers/index.ts +++ b/packages/ai/src/providers/index.ts @@ -23,6 +23,7 @@ export * as Google from "./google.js" export * as GoogleVertex from "./google-vertex.js" export * as GoogleVertexChat from "./google-vertex-chat.js" export * as GoogleVertexMessages from "./google-vertex-messages.js" +export * as GoogleVertexMistral from "./google-vertex-mistral.js" export * as GoogleVertexResponses from "./google-vertex-responses.js" export * as Groq from "./groq.js" export * as Meta from "./meta.js" diff --git a/packages/core/src/aisdk-native.ts b/packages/core/src/aisdk-native.ts index 51d1b509c40..c75d720ad1a 100644 --- a/packages/core/src/aisdk-native.ts +++ b/packages/core/src/aisdk-native.ts @@ -104,7 +104,10 @@ const HOSTS: Readonly>>> = { deepseek: { "@ai-sdk/openai-compatible": "@opencode/ai/providers/deepseek" }, digitalocean: { "@ai-sdk/openai-compatible": "@opencode/ai/providers/digitalocean" }, "fireworks-ai": { "@ai-sdk/openai-compatible": "@opencode/ai/providers/fireworks" }, - "google-vertex": { "@ai-sdk/openai-compatible": "@opencode/ai/providers/google-vertex/chat" }, + "google-vertex": { + "@ai-sdk/openai-compatible": "@opencode/ai/providers/google-vertex/chat", + "@ai-sdk/mistral": "@opencode/ai/providers/google-vertex/mistral", + }, "kimi-for-coding": protocols("moonshot"), meta: protocols("meta"), minimax: protocols("minimax"), @@ -149,8 +152,7 @@ function resolve(specifier: string, context: Context & { readonly settings?: Pro if (Provider.isAISDK(specifier) || npm in PACKAGES || npm in (HOSTS[context.providerID] ?? {})) return native(npm, context) if (npm === "@opencode/ai/providers/amazon-bedrock/mantle") return mantle(context.modelID) - if (npm === "@opencode/ai/providers/azure/responses" && azureChat(context)) - return "@opencode/ai/providers/azure/chat" + if (npm === "@opencode/ai/providers/azure/responses" && azureChat(context)) return "@opencode/ai/providers/azure/chat" return NATIVE.has(npm) ? npm : undefined } diff --git a/packages/core/src/provider.ts b/packages/core/src/provider.ts index 18608bd435e..7b1f76a67c2 100644 --- a/packages/core/src/provider.ts +++ b/packages/core/src/provider.ts @@ -81,6 +81,7 @@ const builtins = new Map Promise>([ ["@opencode/ai/providers/google-vertex/chat", () => import("@opencode/ai/providers/google-vertex/chat")], ["@opencode/ai/providers/google-vertex/responses", () => import("@opencode/ai/providers/google-vertex/responses")], ["@opencode/ai/providers/google-vertex/messages", () => import("@opencode/ai/providers/google-vertex/messages")], + ["@opencode/ai/providers/google-vertex/mistral", () => import("@opencode/ai/providers/google-vertex/mistral")], ["@opencode/ai/providers/groq", () => import("@opencode/ai/providers/groq")], ["@opencode/ai/providers/meta/chat", () => import("@opencode/ai/providers/meta/chat")], ["@opencode/ai/providers/meta/messages", () => import("@opencode/ai/providers/meta/messages")], diff --git a/packages/core/src/variant.ts b/packages/core/src/variant.ts index 75731437bd6..98dc3953f9b 100644 --- a/packages/core/src/variant.ts +++ b/packages/core/src/variant.ts @@ -610,6 +610,7 @@ const PROTOCOLS: Readonly> = { "@opencode/ai/providers/anthropic": anthropicMessages, "@opencode/ai/providers/google-vertex/messages": anthropicMessages, + "@opencode/ai/providers/google-vertex/mistral": openaiChat, "@opencode/ai/providers/alibaba/messages": alibabaMessages, "@opencode/ai/providers/meta/messages": anthropicMessages, "@opencode/ai/providers/minimax/messages": minimaxMessages,