fix(gateway): use Vertex model garden catalog endpoint

This commit is contained in:
MMEXA
2026-05-18 16:51:12 +00:00
parent b480f3aaff
commit 5ed8325592
2 changed files with 37 additions and 56 deletions

View File

@@ -464,8 +464,6 @@ async fn fetch_vertex_service_account_models(
}); });
}; };
let token = exchange_vertex_service_account_token(runtime, &transports[0], auth_config).await?; let token = exchange_vertex_service_account_token(runtime, &transports[0], auth_config).await?;
let project_id = json_string(auth_config.get("project_id"))
.ok_or_else(|| "vertex_ai(service_account): missing project_id".to_string())?;
let gemini_transport = let gemini_transport =
select_transport_for_api_format(transports, "gemini:").unwrap_or(&transports[0]); select_transport_for_api_format(transports, "gemini:").unwrap_or(&transports[0]);
let claude_transport = let claude_transport =
@@ -476,18 +474,12 @@ async fn fetch_vertex_service_account_models(
let mut soft_errors = Vec::new(); let mut soft_errors = Vec::new();
let mut has_success = false; let mut has_success = false;
for region in vertex_regions(auth_config) { for base in iter_vertex_base_urls(transports) {
let base = if region == "global" {
VERTEX_API_BASE_URL.to_string()
} else {
format!("https://{region}-aiplatform.googleapis.com")
};
for (publisher, transport, api_format) in [ for (publisher, transport, api_format) in [
("google", gemini_transport, "gemini:generate_content"), ("google", gemini_transport, "gemini:generate_content"),
("anthropic", claude_transport, "claude:messages"), ("anthropic", claude_transport, "claude:messages"),
] { ] {
let url = let url = build_vertex_service_account_list_url(&base, publisher, None);
build_vertex_service_account_list_url(&base, &project_id, &region, publisher, None);
let outcome = fetch_vertex_models_from_url( let outcome = fetch_vertex_models_from_url(
runtime, runtime,
transport, transport,
@@ -929,14 +921,7 @@ fn iter_vertex_base_urls(transports: &[GatewayProviderTransportSnapshot]) -> Vec
} }
fn build_vertex_google_list_url(base_url: &str, api_key: &str, page_token: Option<&str>) -> String { fn build_vertex_google_list_url(base_url: &str, api_key: &str, page_token: Option<&str>) -> String {
let path = if base_url.trim_end_matches('/').ends_with("/v1") let url = build_vertex_publisher_models_list_base_url(base_url, "google");
|| base_url.trim_end_matches('/').ends_with("/v1beta")
{
"/publishers/google/models"
} else {
"/v1/publishers/google/models"
};
let url = build_simple_path_url(base_url, path);
let mut url = append_query_param(url, "key", api_key); let mut url = append_query_param(url, "key", api_key);
url = append_query_param(url, "pageSize", VERTEX_PAGE_SIZE); url = append_query_param(url, "pageSize", VERTEX_PAGE_SIZE);
if let Some(page_token) = page_token { if let Some(page_token) = page_token {
@@ -947,14 +932,10 @@ fn build_vertex_google_list_url(base_url: &str, api_key: &str, page_token: Optio
fn build_vertex_service_account_list_url( fn build_vertex_service_account_list_url(
base_url: &str, base_url: &str,
project_id: &str,
region: &str,
publisher: &str, publisher: &str,
page_token: Option<&str>, page_token: Option<&str>,
) -> String { ) -> String {
let path = let mut url = build_vertex_publisher_models_list_base_url(base_url, publisher);
format!("/v1/projects/{project_id}/locations/{region}/publishers/{publisher}/models");
let mut url = build_simple_path_url(base_url, &path);
url = append_query_param(url, "pageSize", VERTEX_PAGE_SIZE); url = append_query_param(url, "pageSize", VERTEX_PAGE_SIZE);
if let Some(page_token) = page_token { if let Some(page_token) = page_token {
url = append_query_param(url, "pageToken", page_token); url = append_query_param(url, "pageToken", page_token);
@@ -962,6 +943,16 @@ fn build_vertex_service_account_list_url(
url url
} }
fn build_vertex_publisher_models_list_base_url(base_url: &str, publisher: &str) -> String {
let trimmed_base = base_url.trim().trim_end_matches('/');
let path = if trimmed_base.ends_with("/v1") || trimmed_base.ends_with("/v1beta1") {
format!("/publishers/{publisher}/models")
} else {
format!("/v1beta1/publishers/{publisher}/models")
};
build_simple_path_url(trimmed_base, &path)
}
fn build_simple_path_url(base_url: &str, path: &str) -> String { fn build_simple_path_url(base_url: &str, path: &str) -> String {
format!("{}{}", base_url.trim().trim_end_matches('/'), path.trim()) format!("{}{}", base_url.trim().trim_end_matches('/'), path.trim())
} }
@@ -1087,38 +1078,6 @@ fn vertex_effective_format(model_id: &str, auth_config: Option<&Value>) -> Strin
} }
} }
fn vertex_regions(auth_config: &Value) -> Vec<String> {
let mut seen = BTreeSet::new();
let mut regions = Vec::new();
let auth_config = auth_config.as_object();
let mut push_region = |region: Option<&str>| {
let Some(region) = region.map(str::trim).filter(|value| !value.is_empty()) else {
return;
};
if seen.insert(region.to_string()) {
regions.push(region.to_string());
}
};
push_region(
auth_config
.and_then(|value| value.get("region"))
.and_then(Value::as_str),
);
if let Some(model_regions) = auth_config
.and_then(|value| value.get("model_regions"))
.and_then(Value::as_object)
{
for value in model_regions.values() {
push_region(value.as_str());
}
}
push_region(Some("global"));
push_region(Some("us-central1"));
regions
}
fn is_soft_not_found(error: &str) -> bool { fn is_soft_not_found(error: &str) -> bool {
error.trim().starts_with("HTTP 404:") error.trim().starts_with("HTTP 404:")
} }
@@ -1498,7 +1457,7 @@ mod tests {
let urls = executed_urls.lock().expect("executed_urls lock"); let urls = executed_urls.lock().expect("executed_urls lock");
assert_eq!( assert_eq!(
urls.as_slice(), urls.as_slice(),
&["https://aiplatform.googleapis.com/v1/publishers/google/models?key=vertex-secret&pageSize=100"] &["https://aiplatform.googleapis.com/v1beta1/publishers/google/models?key=vertex-secret&pageSize=100"]
); );
assert_eq!(outcome.fetched_model_ids, vec!["gemini-3.1-pro-preview"]); assert_eq!(outcome.fetched_model_ids, vec!["gemini-3.1-pro-preview"]);
assert_eq!(outcome.cached_models.len(), 1); assert_eq!(outcome.cached_models.len(), 1);
@@ -1508,6 +1467,24 @@ mod tests {
); );
} }
#[test]
fn vertex_publisher_models_list_url_uses_model_garden_resource_not_runtime_resource() {
let url = super::build_vertex_service_account_list_url(
"https://aiplatform.googleapis.com",
"google",
None,
);
assert_eq!(
url,
"https://aiplatform.googleapis.com/v1beta1/publishers/google/models?pageSize=100"
);
assert!(
!url.contains("/projects/") && !url.contains("/locations/"),
"Model Garden publisher list must not use Vertex runtime project/location path"
);
}
#[tokio::test] #[tokio::test]
async fn codex_transport_fetches_upstream_models_instead_of_preset_catalog() { async fn codex_transport_fetches_upstream_models_instead_of_preset_catalog() {
let executed_urls = Arc::new(Mutex::new(Vec::new())); let executed_urls = Arc::new(Mutex::new(Vec::new()));

View File

@@ -79,6 +79,7 @@ Vertex AI 的 Gemini API REST reference 使用 `aiplatform.googleapis.com` 或 r
- Vertex AI Embed Content REST: <https://docs.cloud.google.com/vertex-ai/generative-ai/docs/reference/rest/v1/projects.locations.publishers.models/embedContent> - Vertex AI Embed Content REST: <https://docs.cloud.google.com/vertex-ai/generative-ai/docs/reference/rest/v1/projects.locations.publishers.models/embedContent>
- Vertex AI Predict REST: <https://docs.cloud.google.com/vertex-ai/generative-ai/docs/reference/rest/v1/projects.locations.publishers.models/predict> - Vertex AI Predict REST: <https://docs.cloud.google.com/vertex-ai/generative-ai/docs/reference/rest/v1/projects.locations.publishers.models/predict>
- Vertex AI REST resources: <https://docs.cloud.google.com/vertex-ai/generative-ai/docs/reference/rest/v1/projects.locations.publishers.models> - Vertex AI REST resources: <https://docs.cloud.google.com/vertex-ai/generative-ai/docs/reference/rest/v1/projects.locations.publishers.models>
- Vertex AI Model Garden publisher model list: <https://docs.cloud.google.com/vertex-ai/docs/reference/rest/v1beta1/publishers.models/list>
- Vertex AI text embeddings API: <https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/text-embeddings-api> - Vertex AI text embeddings API: <https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/text-embeddings-api>
- Vertex AI OpenAI compatibility: <https://cloud.google.com/vertex-ai/generative-ai/docs/start/openai> - Vertex AI OpenAI compatibility: <https://cloud.google.com/vertex-ai/generative-ai/docs/start/openai>
@@ -89,6 +90,9 @@ Vertex AI 的 Gemini API REST reference 使用 `aiplatform.googleapis.com` 或 r
-`global` location可使用 `https://aiplatform.googleapis.com/v1/projects/{project}/locations/global/...` -`global` location可使用 `https://aiplatform.googleapis.com/v1/projects/{project}/locations/global/...`
- Vertex API key 路径可走: - Vertex API key 路径可走:
- `https://aiplatform.googleapis.com/v1/publishers/google/models/{model}:{action}?key=...` - `https://aiplatform.googleapis.com/v1/publishers/google/models/{model}:{action}?key=...`
- Vertex 模型目录拉取不是推理请求,必须走 Model Garden publisher list
- `https://aiplatform.googleapis.com/v1beta1/publishers/{publisher}/models`
- 不得使用 `projects/{project}/locations/{location}/publishers/{publisher}/models``projects.locations.publishers.models` 资源没有 list 方法,只有 generate / stream / predict / embed 等动作。
- Vertex 文本 embedding API 文档使用 `:predict`,请求体是 `instances[]`,可选参数在 `parameters` 下;响应是 `predictions[].embeddings.values` - Vertex 文本 embedding API 文档使用 `:predict`,请求体是 `instances[]`,可选参数在 `parameters` 下;响应是 `predictions[].embeddings.values`
- Vertex REST reference 也列出 `embedContent`,但 Aether 当前 text embedding 主链使用 text embeddings guide 和 Predict API 的 contract。 - Vertex REST reference 也列出 `embedContent`,但 Aether 当前 text embedding 主链使用 text embeddings guide 和 Predict API 的 contract。
- Vertex `instances[]` 是在线 Predict 请求体,不等同于异步 batch prediction job。模型级输入数量限制由 Vertex 返回Aether 不把超出限制的请求静默改走其他产品面。 - Vertex `instances[]` 是在线 Predict 请求体,不等同于异步 batch prediction job。模型级输入数量限制由 Vertex 返回Aether 不把超出限制的请求静默改走其他产品面。