vibe/vibe/core/llm/backend/vertex.py
Mathias Gesbert a560a47ce8
v2.2.1 (#403)
Co-authored-by: Quentin Torroba <quentin.torroba@mistral.ai>
Co-authored-by: Vincent Guilloux <vincent.guilloux@mistral.ai>
Co-authored-by: Thomas Kenbeek <thomas.kenbeek@mistral.ai>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
2026-02-19 12:06:03 +01:00

131 lines
4.2 KiB
Python

from __future__ import annotations
import json
import threading
from typing import Any, ClassVar
import google.auth
import google.auth.credentials
from google.auth.transport.requests import Request
from vibe.core.config import ProviderConfig
from vibe.core.llm.backend.anthropic import AnthropicAdapter
from vibe.core.llm.backend.base import PreparedRequest
from vibe.core.types import AvailableTool, LLMMessage, StrToolChoice
def build_vertex_base_url(region: str) -> str:
if region == "global":
return "https://aiplatform.googleapis.com"
return f"https://{region}-aiplatform.googleapis.com"
def build_vertex_endpoint(
region: str, project_id: str, model: str, streaming: bool = False
) -> str:
action = "streamRawPredict" if streaming else "rawPredict"
return (
f"/v1/projects/{project_id}/locations/{region}/"
f"publishers/anthropic/models/{model}:{action}"
)
class VertexCredentials:
def __init__(self) -> None:
self._credentials: google.auth.credentials.Credentials | None = None
self._lock = threading.Lock()
@property
def access_token(self) -> str:
with self._lock:
creds = self._credentials
if creds is None:
creds, _ = google.auth.default(
scopes=["https://www.googleapis.com/auth/cloud-platform"]
)
self._credentials = creds
if not creds.valid:
creds.refresh(Request())
if creds.token is None:
raise RuntimeError(
"Vertex AI credential refresh did not produce a token"
)
return creds.token
class VertexAnthropicAdapter(AnthropicAdapter):
"""Vertex AI adapter — inherits all streaming/parsing from AnthropicAdapter."""
endpoint: ClassVar[str] = ""
BETA_FEATURES: ClassVar[str] = ""
def __init__(self) -> None:
super().__init__()
self.credentials = VertexCredentials()
def prepare_request( # noqa: PLR0913
self,
*,
model_name: str,
messages: list[LLMMessage],
temperature: float,
tools: list[AvailableTool] | None,
max_tokens: int | None,
tool_choice: StrToolChoice | AvailableTool | None,
enable_streaming: bool,
provider: ProviderConfig,
api_key: str | None = None,
thinking: str = "off",
) -> PreparedRequest:
project_id = provider.project_id
region = provider.region
if not project_id:
raise ValueError("project_id is required in provider config for Vertex AI")
if not region:
raise ValueError("region is required in provider config for Vertex AI")
system_prompt, converted_messages = self._mapper.prepare_messages(messages)
converted_tools = self._mapper.prepare_tools(tools)
converted_tool_choice = self._mapper.prepare_tool_choice(tool_choice)
payload: dict[str, Any] = {
"anthropic_version": "vertex-2023-10-16",
"messages": converted_messages,
}
self._apply_thinking_config(
payload,
model_name=model_name,
messages=converted_messages,
temperature=temperature,
max_tokens=max_tokens,
thinking=thinking,
)
if system_blocks := self._build_system_blocks(system_prompt):
payload["system"] = system_blocks
if converted_tools:
payload["tools"] = converted_tools
if converted_tool_choice:
payload["tool_choice"] = converted_tool_choice
if enable_streaming:
payload["stream"] = True
self._add_cache_control_to_last_user_message(converted_messages)
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.credentials.access_token}",
"anthropic-beta": self.BETA_FEATURES,
}
endpoint = build_vertex_endpoint(
region, project_id, model_name, streaming=enable_streaming
)
base_url = build_vertex_base_url(region)
body = json.dumps(payload).encode("utf-8")
return PreparedRequest(endpoint, headers, body, base_url=base_url)