mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
fix(gateway): use Vertex model garden catalog endpoint
This commit is contained in:
@@ -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, ®ion, 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()));
|
||||||
|
|||||||
@@ -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 不把超出限制的请求静默改走其他产品面。
|
||||||
|
|||||||
Reference in New Issue
Block a user