Files
tkmind_go/crates/goose/src/providers/formats/gcpvertexai.rs
T

447 lines
15 KiB
Rust

use super::{anthropic, google};
use crate::message::Message;
use crate::model::ModelConfig;
use crate::providers::base::Usage;
use anyhow::{Context, Result};
use mcp_core::tool::Tool;
use serde_json::Value;
use std::fmt;
/// Sensible default values of Google Cloud Platform (GCP) locations for model deployment.
///
/// Each variant corresponds to a specific GCP region where models can be hosted.
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
pub enum GcpLocation {
/// Represents the us-central1 region in Iowa
Iowa,
/// Represents the us-east5 region in Ohio
Ohio,
}
impl fmt::Display for GcpLocation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Iowa => write!(f, "us-central1"),
Self::Ohio => write!(f, "us-east5"),
}
}
}
impl TryFrom<&str> for GcpLocation {
type Error = ModelError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
"us-central1" => Ok(Self::Iowa),
"us-east5" => Ok(Self::Ohio),
_ => Err(ModelError::UnsupportedLocation(s.to_string())),
}
}
}
/// Represents errors that can occur during model operations.
///
/// This enum encompasses various error conditions that might arise when working
/// with GCP Vertex AI models, including unsupported models, invalid requests,
/// and unsupported locations.
#[derive(Debug, thiserror::Error)]
pub enum ModelError {
/// Error when an unsupported Vertex AI model is specified
#[error("Unsupported Vertex AI model: {0}")]
UnsupportedModel(String),
/// Error when the request structure is invalid
#[error("Invalid request structure: {0}")]
InvalidRequest(String),
/// Error when an unsupported GCP location is specified
#[error("Unsupported GCP location: {0}")]
UnsupportedLocation(String),
}
/// Represents available GCP Vertex AI models for Goose.
///
/// This enum encompasses different model families and their versions
/// that are supported in the GCP Vertex AI platform.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GcpVertexAIModel {
/// Claude model family with specific versions
Claude(ClaudeVersion),
/// Gemini model family with specific versions
Gemini(GeminiVersion),
}
/// Represents available versions of the Claude model for Goose.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ClaudeVersion {
/// Claude 3.5 Sonnet initial version
Sonnet35,
/// Claude 3.5 Sonnet version 2
Sonnet35V2,
/// Claude 3.7 Sonnet
Sonnet37,
/// Claude 3.5 Haiku
Haiku35,
/// Claude Sonnet 4
Sonnet4,
/// Generic Claude model for custom or new versions
Generic(String),
}
/// Represents available versions of the Gemini model for Goose.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GeminiVersion {
/// Gemini 1.5 Pro version
Pro15,
/// Gemini 2.0 Flash version
Flash20,
/// Gemini 2.0 Pro Experimental version
Pro20Exp,
/// Gemini 2.5 Pro Experimental version
Pro25Exp,
/// Gemini 2.5 Flash Preview version
Flash25Preview,
/// Gemini 2.5 Pro Preview version
Pro25Preview,
/// Generic Gemini model for custom or new versions
Generic(String),
}
impl fmt::Display for GcpVertexAIModel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let model_id = match self {
Self::Claude(version) => match version {
ClaudeVersion::Sonnet35 => "claude-3-5-sonnet@20240620",
ClaudeVersion::Sonnet35V2 => "claude-3-5-sonnet-v2@20241022",
ClaudeVersion::Sonnet37 => "claude-3-7-sonnet@20250219",
ClaudeVersion::Haiku35 => "claude-3-5-haiku@20241022",
ClaudeVersion::Sonnet4 => "claude-sonnet-4@20250514",
ClaudeVersion::Generic(name) => name,
},
Self::Gemini(version) => match version {
GeminiVersion::Pro15 => "gemini-1.5-pro-002",
GeminiVersion::Flash20 => "gemini-2.0-flash-001",
GeminiVersion::Pro20Exp => "gemini-2.0-pro-exp-02-05",
GeminiVersion::Pro25Exp => "gemini-2.5-pro-exp-03-25",
GeminiVersion::Flash25Preview => "gemini-2.5-flash-preview-05-20",
GeminiVersion::Pro25Preview => "gemini-2.5-pro-preview-05-06",
GeminiVersion::Generic(name) => name,
},
};
write!(f, "{model_id}")
}
}
impl GcpVertexAIModel {
/// Returns the default GCP location for the model.
///
/// Each model family has a well-known location based on availability:
/// - Claude models default to Ohio (us-east5)
/// - Gemini models default to Iowa (us-central1)
pub fn known_location(&self) -> GcpLocation {
match self {
Self::Claude(_) => GcpLocation::Ohio,
Self::Gemini(_) => GcpLocation::Iowa,
}
}
}
impl TryFrom<&str> for GcpVertexAIModel {
type Error = ModelError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
// Known models
match s {
"claude-3-5-sonnet@20240620" => Ok(Self::Claude(ClaudeVersion::Sonnet35)),
"claude-3-5-sonnet-v2@20241022" => Ok(Self::Claude(ClaudeVersion::Sonnet35V2)),
"claude-3-7-sonnet@20250219" => Ok(Self::Claude(ClaudeVersion::Sonnet37)),
"claude-3-5-haiku@20241022" => Ok(Self::Claude(ClaudeVersion::Haiku35)),
"claude-sonnet-4@20250514" => Ok(Self::Claude(ClaudeVersion::Sonnet4)),
"gemini-1.5-pro-002" => Ok(Self::Gemini(GeminiVersion::Pro15)),
"gemini-2.0-flash-001" => Ok(Self::Gemini(GeminiVersion::Flash20)),
"gemini-2.0-pro-exp-02-05" => Ok(Self::Gemini(GeminiVersion::Pro20Exp)),
"gemini-2.5-pro-exp-03-25" => Ok(Self::Gemini(GeminiVersion::Pro25Exp)),
"gemini-2.5-flash-preview-05-20" => Ok(Self::Gemini(GeminiVersion::Flash25Preview)),
"gemini-2.5-pro-preview-05-06" => Ok(Self::Gemini(GeminiVersion::Pro25Preview)),
// Generic models based on prefix matching
_ if s.starts_with("claude-") => {
Ok(Self::Claude(ClaudeVersion::Generic(s.to_string())))
}
_ if s.starts_with("gemini-") => {
Ok(Self::Gemini(GeminiVersion::Generic(s.to_string())))
}
_ => Err(ModelError::UnsupportedModel(s.to_string())),
}
}
}
/// Holds context information for a model request since the Vertex AI platform
/// supports multiple model families.
///
/// This structure maintains information about the model being used
/// and provides utility methods for handling model-specific operations.
#[derive(Debug, Clone)]
pub struct RequestContext {
/// The GCP Vertex AI model being used
pub model: GcpVertexAIModel,
}
impl RequestContext {
/// Creates a new RequestContext from a model ID string.
///
/// # Arguments
/// * `model_id` - The string identifier of the model
///
/// # Returns
/// * `Result<Self>` - A new RequestContext if the model ID is valid
pub fn new(model_id: &str) -> Result<Self> {
Ok(Self {
model: GcpVertexAIModel::try_from(model_id)
.with_context(|| format!("Failed to parse model ID: {model_id}"))?,
})
}
/// Returns the provider associated with the model.
pub fn provider(&self) -> ModelProvider {
match self.model {
GcpVertexAIModel::Claude(_) => ModelProvider::Anthropic,
GcpVertexAIModel::Gemini(_) => ModelProvider::Google,
}
}
}
/// Represents available model providers.
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
pub enum ModelProvider {
/// Anthropic provider (Claude models)
Anthropic,
/// Google provider (Gemini models)
Google,
}
impl ModelProvider {
/// Returns the string representation of the provider.
pub fn as_str(&self) -> &'static str {
match self {
Self::Anthropic => "anthropic",
Self::Google => "google",
}
}
}
/// Creates an Anthropic-specific Vertex AI request payload.
///
/// # Arguments
/// * `model_config` - Configuration for the model
/// * `system` - System prompt
/// * `messages` - Array of messages
/// * `tools` - Array of available tools
///
/// # Returns
/// * `Result<Value>` - JSON request payload for Anthropic API
fn create_anthropic_request(
model_config: &ModelConfig,
system: &str,
messages: &[Message],
tools: &[Tool],
) -> Result<Value> {
let mut request = anthropic::create_request(model_config, system, messages, tools)?;
let obj = request
.as_object_mut()
.ok_or_else(|| ModelError::InvalidRequest("Request is not a JSON object".to_string()))?;
// Note: We don't need to specify the model in the request body
// The model is determined by the endpoint URL in GCP Vertex AI
obj.remove("model");
obj.insert(
"anthropic_version".to_string(),
Value::String("vertex-2023-10-16".to_string()),
);
Ok(request)
}
/// Creates a Gemini-specific Vertex AI request payload.
///
/// # Arguments
/// * `model_config` - Configuration for the model
/// * `system` - System prompt
/// * `messages` - Array of messages
/// * `tools` - Array of available tools
///
/// # Returns
/// * `Result<Value>` - JSON request payload for Google API
fn create_google_request(
model_config: &ModelConfig,
system: &str,
messages: &[Message],
tools: &[Tool],
) -> Result<Value> {
google::create_request(model_config, system, messages, tools)
}
/// Creates a provider-specific request payload and context.
///
/// # Arguments
/// * `model_config` - Configuration for the model
/// * `system` - System prompt
/// * `messages` - Array of messages
/// * `tools` - Array of available tools
///
/// # Returns
/// * `Result<(Value, RequestContext)>` - Tuple of request payload and context
pub fn create_request(
model_config: &ModelConfig,
system: &str,
messages: &[Message],
tools: &[Tool],
) -> Result<(Value, RequestContext)> {
let context = RequestContext::new(&model_config.model_name)?;
let request = match &context.model {
GcpVertexAIModel::Claude(_) => {
create_anthropic_request(model_config, system, messages, tools)?
}
GcpVertexAIModel::Gemini(_) => {
create_google_request(model_config, system, messages, tools)?
}
};
Ok((request, context))
}
/// Converts a provider response to a Message.
///
/// # Arguments
/// * `response` - The raw response from the provider
/// * `request_context` - Context information about the request
///
/// # Returns
/// * `Result<Message>` - Converted message
pub fn response_to_message(response: Value, request_context: RequestContext) -> Result<Message> {
match request_context.provider() {
ModelProvider::Anthropic => anthropic::response_to_message(response),
ModelProvider::Google => google::response_to_message(response),
}
}
/// Extracts token usage information from the response data.
///
/// # Arguments
/// * `data` - The response data containing usage information
/// * `request_context` - Context information about the request
///
/// # Returns
/// * `Result<Usage>` - Usage statistics
pub fn get_usage(data: &Value, request_context: &RequestContext) -> Result<Usage> {
match request_context.provider() {
ModelProvider::Anthropic => anthropic::get_usage(data),
ModelProvider::Google => google::get_usage(data),
}
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::Result;
#[test]
fn test_model_parsing() -> Result<()> {
let valid_models = [
"claude-3-5-sonnet@20240620",
"claude-3-5-sonnet-v2@20241022",
"claude-3-7-sonnet@20250219",
"claude-3-5-haiku@20241022",
"claude-sonnet-4@20250514",
"gemini-1.5-pro-002",
"gemini-2.0-flash-001",
"gemini-2.0-pro-exp-02-05",
"gemini-2.5-pro-exp-03-25",
"gemini-2.5-flash-preview-05-20",
"gemini-2.5-pro-preview-05-06",
];
for model_id in valid_models {
let model = GcpVertexAIModel::try_from(model_id)?;
assert_eq!(model.to_string(), model_id);
}
assert!(GcpVertexAIModel::try_from("unsupported-model").is_err());
Ok(())
}
#[test]
fn test_default_locations() -> Result<()> {
let test_cases = [
("claude-3-5-sonnet@20240620", GcpLocation::Ohio),
("claude-3-5-sonnet-v2@20241022", GcpLocation::Ohio),
("claude-3-7-sonnet@20250219", GcpLocation::Ohio),
("claude-3-5-haiku@20241022", GcpLocation::Ohio),
("claude-sonnet-4@20250514", GcpLocation::Ohio),
("gemini-1.5-pro-002", GcpLocation::Iowa),
("gemini-2.0-flash-001", GcpLocation::Iowa),
("gemini-2.0-pro-exp-02-05", GcpLocation::Iowa),
("gemini-2.5-pro-exp-03-25", GcpLocation::Iowa),
("gemini-2.5-flash-preview-05-20", GcpLocation::Iowa),
("gemini-2.5-pro-preview-05-06", GcpLocation::Iowa),
];
for (model_id, expected_location) in test_cases {
let model = GcpVertexAIModel::try_from(model_id)?;
assert_eq!(
model.known_location(),
expected_location,
"Model {model_id} should have default location {expected_location:?}",
);
let context = RequestContext::new(model_id)?;
assert_eq!(
context.model.known_location(),
expected_location,
"RequestContext for {model_id} should have default location {expected_location:?}",
);
}
Ok(())
}
#[test]
fn test_generic_model_parsing() -> Result<()> {
// Test generic Claude models
let claude_models = [
"claude-3-8-apex@20250301",
"claude-new-version",
"claude-experimental",
];
for model_id in claude_models {
let model = GcpVertexAIModel::try_from(model_id)?;
match model {
GcpVertexAIModel::Claude(ClaudeVersion::Generic(ref name)) => {
assert_eq!(name, model_id);
}
_ => panic!("Expected Claude generic model for {model_id}"),
}
assert_eq!(model.to_string(), model_id);
assert_eq!(model.known_location(), GcpLocation::Ohio);
}
// Test generic Gemini models
let gemini_models = ["gemini-3-pro", "gemini-2.5-flash", "gemini-experimental"];
for model_id in gemini_models {
let model = GcpVertexAIModel::try_from(model_id)?;
match model {
GcpVertexAIModel::Gemini(GeminiVersion::Generic(ref name)) => {
assert_eq!(name, model_id);
}
_ => panic!("Expected Gemini generic model for {model_id}"),
}
assert_eq!(model.to_string(), model_id);
assert_eq!(model.known_location(), GcpLocation::Iowa);
}
Ok(())
}
}