feat(ai): add Vertex Mistral route (#54058)

This commit is contained in:
Aiden Cline
2026-10-08 23:51:21 -05:00
committed by GitHub
Unverified
parent d7f34d4662
commit 93a7c8404f
7 changed files with 120 additions and 4 deletions
+1 -1
View File
@@ -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),
@@ -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<MistralChat.MistralBody, "prompt_cache_key">
// 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<Body>().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<MistralChat.ProviderOptionsInput>({ id: modelID }),
configure,
}
}
export const provider = {
id,
configure,
}
export const model: ProviderPackage.Definition<Settings, MistralChat.ProviderOptionsInput>["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)
}
@@ -0,0 +1,2 @@
export { model } from "../google-vertex-mistral.js"
export type { Settings } from "../google-vertex-mistral.js"
+1
View File
@@ -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"
+5 -3
View File
@@ -104,7 +104,10 @@ const HOSTS: Readonly<Record<string, Readonly<Record<string, string>>>> = {
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
}
+1
View File
@@ -81,6 +81,7 @@ const builtins = new Map<string, () => Promise<unknown>>([
["@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")],
+1
View File
@@ -610,6 +610,7 @@ const PROTOCOLS: Readonly<Record<string, Protocol>> = {
"@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,