mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
fix(gateway): cover Google OpenAI-compatible roots
This commit is contained in:
@@ -161,8 +161,12 @@ pub(crate) fn validate_vertex_api_formats(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let allowed = match auth_type {
|
let allowed = match auth_type {
|
||||||
"api_key" => &["gemini:generate_content"][..],
|
"api_key" => &["gemini:generate_content", "gemini:embedding"][..],
|
||||||
"service_account" | "vertex_ai" => &["claude:messages", "gemini:generate_content"][..],
|
"service_account" | "vertex_ai" => &[
|
||||||
|
"claude:messages",
|
||||||
|
"gemini:generate_content",
|
||||||
|
"gemini:embedding",
|
||||||
|
][..],
|
||||||
_ => return Ok(()),
|
_ => return Ok(()),
|
||||||
};
|
};
|
||||||
let invalid = api_formats
|
let invalid = api_formats
|
||||||
@@ -367,4 +371,27 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.is_err());
|
.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_vertex_api_formats_allows_gemini_embedding() {
|
||||||
|
assert!(validate_vertex_api_formats(
|
||||||
|
"vertex_ai",
|
||||||
|
"api_key",
|
||||||
|
&[
|
||||||
|
"gemini:generate_content".to_string(),
|
||||||
|
"gemini:embedding".to_string()
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.is_ok());
|
||||||
|
assert!(validate_vertex_api_formats(
|
||||||
|
"vertex_ai",
|
||||||
|
"service_account",
|
||||||
|
&[
|
||||||
|
"claude:messages".to_string(),
|
||||||
|
"gemini:generate_content".to_string(),
|
||||||
|
"gemini:embedding".to_string()
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.is_ok());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ use crate::claude_code::build_claude_code_messages_url;
|
|||||||
use crate::snapshot::GatewayProviderTransportSnapshot;
|
use crate::snapshot::GatewayProviderTransportSnapshot;
|
||||||
use crate::url::{
|
use crate::url::{
|
||||||
build_claude_messages_url, build_gemini_content_url, build_openai_chat_url,
|
build_claude_messages_url, build_gemini_content_url, build_openai_chat_url,
|
||||||
build_openai_responses_url, build_passthrough_path_url, normalize_gemini_content_action_path,
|
build_openai_responses_url, build_passthrough_path_url,
|
||||||
|
google_openai_compat_base_includes_api_root, normalize_gemini_content_action_path,
|
||||||
};
|
};
|
||||||
use crate::vertex::{
|
use crate::vertex::{
|
||||||
build_vertex_api_key_gemini_content_url, build_vertex_api_key_gemini_embedding_url,
|
build_vertex_api_key_gemini_content_url, build_vertex_api_key_gemini_embedding_url,
|
||||||
@@ -409,7 +410,9 @@ fn build_provider_v1_url(
|
|||||||
.map(|(base, _)| base)
|
.map(|(base, _)| base)
|
||||||
.unwrap_or_else(|| upstream_base_url.trim())
|
.unwrap_or_else(|| upstream_base_url.trim())
|
||||||
.trim_end_matches('/');
|
.trim_end_matches('/');
|
||||||
let path = if base_without_query.ends_with("/v1") {
|
let path = if base_without_query.ends_with("/v1")
|
||||||
|
|| google_openai_compat_base_includes_api_root(base_without_query)
|
||||||
|
{
|
||||||
v1_path
|
v1_path
|
||||||
} else {
|
} else {
|
||||||
default_path
|
default_path
|
||||||
@@ -997,6 +1000,53 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn embedding_request_url_preserves_google_openai_compat_roots() {
|
||||||
|
let developer_api_openai = sample_transport(
|
||||||
|
"custom",
|
||||||
|
"openai:embedding",
|
||||||
|
"https://generativelanguage.googleapis.com/v1beta/openai",
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let vertex_openai = sample_transport(
|
||||||
|
"custom",
|
||||||
|
"openai:embedding",
|
||||||
|
"https://aiplatform.googleapis.com/v1/projects/project-1/locations/global/endpoints/openapi",
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
build_transport_request_url(
|
||||||
|
&developer_api_openai,
|
||||||
|
TransportRequestUrlParams {
|
||||||
|
provider_api_format: "openai:embedding",
|
||||||
|
mapped_model: Some("gemini-embedding-001"),
|
||||||
|
upstream_is_stream: false,
|
||||||
|
request_query: Some("trace=1"),
|
||||||
|
kiro_api_region: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.as_deref(),
|
||||||
|
Some("https://generativelanguage.googleapis.com/v1beta/openai/embeddings?trace=1")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
build_transport_request_url(
|
||||||
|
&vertex_openai,
|
||||||
|
TransportRequestUrlParams {
|
||||||
|
provider_api_format: "openai:embedding",
|
||||||
|
mapped_model: Some("gemini-embedding-001"),
|
||||||
|
upstream_is_stream: false,
|
||||||
|
request_query: None,
|
||||||
|
kiro_api_region: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.as_deref(),
|
||||||
|
Some(
|
||||||
|
"https://aiplatform.googleapis.com/v1/projects/project-1/locations/global/endpoints/openapi/embeddings"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn gemini_embedding_batch_body_uses_batch_endpoint() {
|
fn gemini_embedding_batch_body_uses_batch_endpoint() {
|
||||||
let gemini = sample_transport(
|
let gemini = sample_transport(
|
||||||
|
|||||||
@@ -2,15 +2,17 @@ use std::collections::BTreeMap;
|
|||||||
|
|
||||||
use super::provider_types::is_codex_cli_backend_url;
|
use super::provider_types::is_codex_cli_backend_url;
|
||||||
use url::form_urlencoded;
|
use url::form_urlencoded;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
pub fn build_openai_chat_url(upstream_base_url: &str, query: Option<&str>) -> String {
|
pub fn build_openai_chat_url(upstream_base_url: &str, query: Option<&str>) -> String {
|
||||||
let (trimmed, base_query) = split_base_url_query(upstream_base_url);
|
let (trimmed, base_query) = split_base_url_query(upstream_base_url);
|
||||||
let trimmed = trimmed.trim_end_matches('/');
|
let trimmed = trimmed.trim_end_matches('/');
|
||||||
let mut url = if trimmed.ends_with("/v1") {
|
let mut url =
|
||||||
format!("{trimmed}/chat/completions")
|
if trimmed.ends_with("/v1") || google_openai_compat_base_includes_api_root(trimmed) {
|
||||||
} else {
|
format!("{trimmed}/chat/completions")
|
||||||
format!("{trimmed}/v1/chat/completions")
|
} else {
|
||||||
};
|
format!("{trimmed}/v1/chat/completions")
|
||||||
|
};
|
||||||
append_merged_query(&mut url, base_query, None, query, &[]);
|
append_merged_query(&mut url, base_query, None, query, &[]);
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
@@ -195,6 +197,33 @@ fn split_base_url_query(base_url: &str) -> (&str, Option<&str>) {
|
|||||||
.unwrap_or((trimmed, None))
|
.unwrap_or((trimmed, None))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn google_openai_compat_base_includes_api_root(base_url: &str) -> bool {
|
||||||
|
let Ok(parsed) = Url::parse(base_url.trim()) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some(host) = parsed.host_str().map(|value| value.to_ascii_lowercase()) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let path = parsed.path().trim_end_matches('/');
|
||||||
|
|
||||||
|
if host == "generativelanguage.googleapis.com" {
|
||||||
|
return path == "/v1beta/openai" || path == "/v1/openai";
|
||||||
|
}
|
||||||
|
|
||||||
|
if looks_like_vertex_ai_host(&host) {
|
||||||
|
return path.ends_with("/endpoints/openapi");
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn looks_like_vertex_ai_host(host: &str) -> bool {
|
||||||
|
const VERTEX_AI_HOST: &str = "aiplatform.googleapis.com";
|
||||||
|
host == VERTEX_AI_HOST
|
||||||
|
|| host.ends_with(&format!(".{VERTEX_AI_HOST}"))
|
||||||
|
|| host.ends_with(&format!("-{VERTEX_AI_HOST}"))
|
||||||
|
}
|
||||||
|
|
||||||
fn split_path_query(path: &str) -> (&str, Option<&str>) {
|
fn split_path_query(path: &str) -> (&str, Option<&str>) {
|
||||||
path.split_once('?')
|
path.split_once('?')
|
||||||
.map(|(path, query)| (path, Some(query)))
|
.map(|(path, query)| (path, Some(query)))
|
||||||
@@ -294,6 +323,24 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn openai_chat_url_preserves_google_openai_compat_roots() {
|
||||||
|
assert_eq!(
|
||||||
|
build_openai_chat_url(
|
||||||
|
"https://generativelanguage.googleapis.com/v1beta/openai",
|
||||||
|
Some("trace=1")
|
||||||
|
),
|
||||||
|
"https://generativelanguage.googleapis.com/v1beta/openai/chat/completions?trace=1"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
build_openai_chat_url(
|
||||||
|
"https://aiplatform.googleapis.com/v1/projects/project-1/locations/global/endpoints/openapi",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
"https://aiplatform.googleapis.com/v1/projects/project-1/locations/global/endpoints/openapi/chat/completions"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn openai_responses_url_preserves_codex_path_prefix() {
|
fn openai_responses_url_preserves_codex_path_prefix() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -82,7 +82,20 @@ fn is_vertex_host_format_context(transport: &GatewayProviderTransportSnapshot) -
|
|||||||
}
|
}
|
||||||
|
|
||||||
let endpoint_api_format = transport.endpoint.api_format.trim().to_ascii_lowercase();
|
let endpoint_api_format = transport.endpoint.api_format.trim().to_ascii_lowercase();
|
||||||
endpoint_api_format.starts_with("gemini:") || endpoint_api_format.starts_with("claude:")
|
endpoint_api_format.starts_with("gemini:")
|
||||||
|
|| endpoint_api_format.starts_with("claude:")
|
||||||
|
|| (endpoint_api_format.starts_with("openai:")
|
||||||
|
&& looks_like_vertex_openai_compat_base(&transport.endpoint.base_url))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn looks_like_vertex_openai_compat_base(base_url: &str) -> bool {
|
||||||
|
let Ok(parsed) = Url::parse(base_url.trim()) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
parsed
|
||||||
|
.path()
|
||||||
|
.trim_end_matches('/')
|
||||||
|
.ends_with("/endpoints/openapi")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -198,4 +211,28 @@ mod tests {
|
|||||||
"claude:messages"
|
"claude:messages"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn infers_vertex_service_account_context_for_openai_compat_endpoint_root() {
|
||||||
|
let mut transport = sample_transport();
|
||||||
|
transport.endpoint.api_format = "openai:chat".to_string();
|
||||||
|
transport.endpoint.base_url =
|
||||||
|
"https://aiplatform.googleapis.com/v1/projects/project-1/locations/global/endpoints/openapi"
|
||||||
|
.to_string();
|
||||||
|
transport.key.auth_type = "service_account".to_string();
|
||||||
|
|
||||||
|
assert!(is_vertex_service_account_transport_context(&transport));
|
||||||
|
assert!(is_vertex_transport_context(&transport));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn does_not_infer_vertex_context_for_generic_openai_format_on_aiplatform_root() {
|
||||||
|
let mut transport = sample_transport();
|
||||||
|
transport.endpoint.api_format = "openai:chat".to_string();
|
||||||
|
transport.endpoint.base_url = "https://aiplatform.googleapis.com".to_string();
|
||||||
|
transport.key.auth_type = "service_account".to_string();
|
||||||
|
|
||||||
|
assert!(!is_vertex_service_account_transport_context(&transport));
|
||||||
|
assert!(!is_vertex_transport_context(&transport));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Gemini API Endpoint Routing Design
|
# Gemini API Endpoint Routing Design
|
||||||
|
|
||||||
**状态:** implementation design
|
**状态:** implementation design
|
||||||
**最后更新:** 2026-05-17
|
**最后更新:** 2026-05-18
|
||||||
**目标:** 把 Gemini Developer API 和 Vertex AI 的端点语义在 Aether 内部做成明确、可测试、可审计的一等路由语义,根治 `generativelanguage.googleapis.com` 与 `aiplatform.googleapis.com` 混用、批量 embedding 伪成功、provider 能力声明不完整等问题。
|
**目标:** 把 Gemini Developer API 和 Vertex AI 的端点语义在 Aether 内部做成明确、可测试、可审计的一等路由语义,根治 `generativelanguage.googleapis.com` 与 `aiplatform.googleapis.com` 混用、批量 embedding 伪成功、provider 能力声明不完整等问题。
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -15,6 +15,13 @@ Aether 里同一个 `api_format` 只描述请求/响应数据形态,不等于
|
|||||||
| Google / Gemini Developer API | Gemini Developer API, 也就是 AI Studio 这条 Gemini API | `generativelanguage.googleapis.com` | API key | 默认 Gemini provider 应走这里 |
|
| Google / Gemini Developer API | Gemini Developer API, 也就是 AI Studio 这条 Gemini API | `generativelanguage.googleapis.com` | API key | 默认 Gemini provider 应走这里 |
|
||||||
| Vertex AI | Vertex AI Gemini API | `aiplatform.googleapis.com` 或 `{region}-aiplatform.googleapis.com` | service account / Vertex API key | `provider_type = vertex_ai` 应走这里 |
|
| Vertex AI | Vertex AI Gemini API | `aiplatform.googleapis.com` 或 `{region}-aiplatform.googleapis.com` | service account / Vertex API key | `provider_type = vertex_ai` 应走这里 |
|
||||||
|
|
||||||
|
Aether 还必须区分 Google 官方的 OpenAI-compatible 表面。它们使用 OpenAI request/response schema,但不等于 native `generateContent` / `embedContent` endpoint:
|
||||||
|
|
||||||
|
| OpenAI-compatible 表面 | 后端产品面 | 官方 API root | 主要认证形态 | Aether 处理原则 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| Gemini Developer API OpenAI compatibility | Gemini Developer API / AI Studio | `https://generativelanguage.googleapis.com/v1beta/openai` | Gemini API key as Bearer | 只在 provider format 是 `openai:*` 且显式配置该 root 时使用 |
|
||||||
|
| Vertex AI OpenAI compatibility | Vertex AI / Google Cloud | `https://aiplatform.googleapis.com/v1/projects/{project}/locations/{location}/endpoints/openapi` | Google Cloud access token / service account | 只在显式 OpenAI-compatible endpoint 上使用;不得替代 native Vertex provider 主链 |
|
||||||
|
|
||||||
端点动作必须按后端产品面区分:
|
端点动作必须按后端产品面区分:
|
||||||
|
|
||||||
| 能力 | Gemini Developer API | Vertex AI | Aether 处理原则 |
|
| 能力 | Gemini Developer API | Vertex AI | Aether 处理原则 |
|
||||||
@@ -31,6 +38,8 @@ Aether 里同一个 `api_format` 只描述请求/响应数据形态,不等于
|
|||||||
3. Vertex embedding 批量请求在没有官方 batch 端点前不能静默改走 `generativelanguage.googleapis.com:batchEmbedContents`。
|
3. Vertex embedding 批量请求在没有官方 batch 端点前不能静默改走 `generativelanguage.googleapis.com:batchEmbedContents`。
|
||||||
4. 任何“不支持”的情况必须在调度/URL 构造阶段显式暴露为不可用,不能伪成功。
|
4. 任何“不支持”的情况必须在调度/URL 构造阶段显式暴露为不可用,不能伪成功。
|
||||||
5. Provider 模板、runtime policy、URL builder、conversion policy、测试连接、live DB reconciliation 必须消费同一个语义模型。
|
5. Provider 模板、runtime policy、URL builder、conversion policy、测试连接、live DB reconciliation 必须消费同一个语义模型。
|
||||||
|
6. Google 官方 OpenAI-compatible root 已经包含 API root,Aether 不得额外拼接 `/v1`,否则会生成 `.../openai/v1/...` 或 `.../endpoints/openapi/v1/...` 这类错误 URL。
|
||||||
|
7. Native Gemini endpoint 与 Google OpenAI-compatible endpoint 不是互相 fallback 的关系。显式配置 `openai:*` 才能走 OpenAI-compatible;显式配置 `gemini:*` 才能走 native Gemini REST。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -48,6 +57,7 @@ Aether 里同一个 `api_format` 只描述请求/响应数据形态,不等于
|
|||||||
- Gemini API Generate Content: <https://ai.google.dev/api/generate-content>
|
- Gemini API Generate Content: <https://ai.google.dev/api/generate-content>
|
||||||
- Gemini API Embeddings guide: <https://ai.google.dev/gemini-api/docs/embeddings>
|
- Gemini API Embeddings guide: <https://ai.google.dev/gemini-api/docs/embeddings>
|
||||||
- Gemini API embeddings reference: <https://ai.google.dev/api/embeddings>
|
- Gemini API embeddings reference: <https://ai.google.dev/api/embeddings>
|
||||||
|
- Gemini API OpenAI compatibility: <https://ai.google.dev/gemini-api/docs/openai>
|
||||||
- Gemini API migrate to cloud / Vertex AI: <https://ai.google.dev/gemini-api/docs/migrate-to-cloud>
|
- Gemini API migrate to cloud / Vertex AI: <https://ai.google.dev/gemini-api/docs/migrate-to-cloud>
|
||||||
|
|
||||||
工程含义:
|
工程含义:
|
||||||
@@ -56,6 +66,7 @@ Aether 里同一个 `api_format` 只描述请求/响应数据形态,不等于
|
|||||||
- `embedContent` 是单条 embedding。
|
- `embedContent` 是单条 embedding。
|
||||||
- `batchEmbedContents` 是 Developer API 的批量 embedding 方法;批量 body 形态是顶层 `requests[]`,每项包含 `model` 和 `content`。
|
- `batchEmbedContents` 是 Developer API 的批量 embedding 方法;批量 body 形态是顶层 `requests[]`,每项包含 `model` 和 `content`。
|
||||||
- Developer API key 不应被拼进 path;Aether URL builder 应继续过滤或独立处理 `key` query,避免 query 重复或泄露。
|
- Developer API key 不应被拼进 path;Aether URL builder 应继续过滤或独立处理 `key` query,避免 query 重复或泄露。
|
||||||
|
- Developer API 的 OpenAI-compatible root 是 `/v1beta/openai`,其 chat / embedding path 是 `/chat/completions` 与 `/embeddings`,不是 `/v1/chat/completions` 与 `/v1/embeddings`。
|
||||||
|
|
||||||
### Vertex AI Gemini API
|
### Vertex AI Gemini API
|
||||||
|
|
||||||
@@ -68,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 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 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>
|
||||||
|
|
||||||
工程含义:
|
工程含义:
|
||||||
|
|
||||||
@@ -77,6 +89,8 @@ Vertex AI 的 Gemini API REST reference 使用 `aiplatform.googleapis.com` 或 r
|
|||||||
- 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 REST reference 当前列出 `embedContent`,未列出 `batchEmbedContents`。因此 Aether 不得自行构造 Vertex batch endpoint。
|
- Vertex REST reference 当前列出 `embedContent`,未列出 `batchEmbedContents`。因此 Aether 不得自行构造 Vertex batch endpoint。
|
||||||
|
- Vertex OpenAI-compatible root 是 `/v1/projects/{project}/locations/{location}/endpoints/openapi`,其 OpenAI path 直接挂在这个 root 之后。
|
||||||
|
- 自定义 Vertex OpenAI-compatible endpoint 可以使用 service account token 刷新,但只有 base URL 明确落在 `/endpoints/openapi` 时才能启用该 Vertex auth 语义。普通 `aiplatform.googleapis.com` + `openai:*` 不能被误判成 Vertex OpenAI compatibility。
|
||||||
|
|
||||||
### Google Gen AI SDK 的后端切换语义
|
### Google Gen AI SDK 的后端切换语义
|
||||||
|
|
||||||
@@ -171,6 +185,23 @@ Vertex 单条 embedding 的模型由 URL path 承载,body 不得重复携带
|
|||||||
|
|
||||||
本次先做第一阶段,因为它不会隐藏批量语义差异;后续如果实现 fan-out,必须有单独设计与负载控制,不得把 fan-out 塞进 URL builder。
|
本次先做第一阶段,因为它不会隐藏批量语义差异;后续如果实现 fan-out,必须有单独设计与负载控制,不得把 fan-out 塞进 URL builder。
|
||||||
|
|
||||||
|
### Google OpenAI-Compatible URL
|
||||||
|
|
||||||
|
OpenAI-compatible URL 属于显式 passthrough root,不参与 native Gemini URL builder。
|
||||||
|
|
||||||
|
| Aether api_format | Gemini Developer API OpenAI compatibility | Vertex AI OpenAI compatibility | Aether 处理原则 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `openai:chat` | `/v1beta/openai/chat/completions` | `/v1/projects/{project}/locations/{location}/endpoints/openapi/chat/completions` | root 已含 API 版本,不再补 `/v1` |
|
||||||
|
| `openai:embedding` | `/v1beta/openai/embeddings` | `/v1/projects/{project}/locations/{location}/endpoints/openapi/embeddings` | root 已含 API 版本,不再补 `/v1` |
|
||||||
|
|
||||||
|
这条链路的关键边界:
|
||||||
|
|
||||||
|
1. `openai:*` provider format 走 OpenAI-compatible schema,不做 OpenAI -> Gemini native body 转换。
|
||||||
|
2. `gemini:*` provider format 走 native Gemini schema,不因目标是 Google provider 就切到 OpenAI-compatible endpoint。
|
||||||
|
3. 如果用户请求 `openai:*`、provider endpoint 是 `gemini:*`,Aether 走格式转换后打 native Gemini endpoint。
|
||||||
|
4. 如果用户请求 `openai:*`、provider endpoint 也是 `openai:*`,Aether 保持 OpenAI schema 并打显式 OpenAI-compatible root。
|
||||||
|
5. 以上两条都是正式主链,不能互相静默顶替。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 请求体转换边界
|
## 请求体转换边界
|
||||||
@@ -210,6 +241,8 @@ Vertex 单条 embedding 的模型由 URL path 承载,body 不得重复携带
|
|||||||
| `gemini:embedding` 批量 | `openai:embedding` | 支持 | 支持于格式层;执行层仍受 Vertex batch 限制 | Gemini `requests[]` -> OpenAI `input[]` |
|
| `gemini:embedding` 批量 | `openai:embedding` | 支持 | 支持于格式层;执行层仍受 Vertex batch 限制 | Gemini `requests[]` -> OpenAI `input[]` |
|
||||||
| `gemini:embedding` response | `openai:embedding` response | 支持 | 支持 | Gemini `embedding.values` / `embeddings[].values` -> OpenAI `data[].embedding` |
|
| `gemini:embedding` response | `openai:embedding` response | 支持 | 支持 | Gemini `embedding.values` / `embeddings[].values` -> OpenAI `data[].embedding` |
|
||||||
| `openai:embedding` response | `gemini:embedding` response | 支持 | 支持于格式层 | OpenAI `data[]` -> Gemini single `embedding` 或 batch `embeddings[]` |
|
| `openai:embedding` response | `gemini:embedding` response | 支持 | 支持于格式层 | OpenAI `data[]` -> Gemini single `embedding` 或 batch `embeddings[]` |
|
||||||
|
| `openai:chat` | `openai:chat` on Google OpenAI-compatible root | 支持 | 支持 | passthrough OpenAI schema,不做 native Gemini 转换 |
|
||||||
|
| `openai:embedding` | `openai:embedding` on Google OpenAI-compatible root | 支持 | 支持 | passthrough OpenAI schema,不做 native Gemini 转换 |
|
||||||
|
|
||||||
这张矩阵的关键点:
|
这张矩阵的关键点:
|
||||||
|
|
||||||
@@ -217,6 +250,7 @@ Vertex 单条 embedding 的模型由 URL path 承载,body 不得重复携带
|
|||||||
2. Vertex 不支持 batch endpoint 是 transport/execution 能力限制,不是格式转换器不能表达 batch。
|
2. Vertex 不支持 batch endpoint 是 transport/execution 能力限制,不是格式转换器不能表达 batch。
|
||||||
3. 一旦 provider family 是 Vertex,批量请求不能借格式转换之名回退到 Developer API。
|
3. 一旦 provider family 是 Vertex,批量请求不能借格式转换之名回退到 Developer API。
|
||||||
4. 对 OpenAI embedding 单项数组,转换器必须生成 Gemini 单条 body,避免把“单条业务请求”误判成 Vertex batch。
|
4. 对 OpenAI embedding 单项数组,转换器必须生成 Gemini 单条 body,避免把“单条业务请求”误判成 Vertex batch。
|
||||||
|
5. Google OpenAI-compatible passthrough 与 OpenAI -> Gemini native conversion 是两条显式路径。管理员通过 provider endpoint format 选择路径,Aether 不得自动“择优”改路。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -274,6 +308,14 @@ Runtime policy 必须表达:
|
|||||||
8. Gateway test connection:
|
8. Gateway test connection:
|
||||||
- Gemini generate content 测试不能强制 `maxOutputTokens = 5`
|
- Gemini generate content 测试不能强制 `maxOutputTokens = 5`
|
||||||
- Gemini 3 / thinking 模型返回 HTTP 200 但无 visible content 时必须判失败,不能写成成功
|
- Gemini 3 / thinking 模型返回 HTTP 200 但无 visible content 时必须判失败,不能写成成功
|
||||||
|
9. Google OpenAI-compatible roots:
|
||||||
|
- Developer API OpenAI root `.../v1beta/openai` 打 `openai:chat` 时生成 `.../chat/completions`,不得生成 `.../openai/v1/chat/completions`
|
||||||
|
- Developer API OpenAI root `.../v1beta/openai` 打 `openai:embedding` 时生成 `.../embeddings`,不得生成 `.../openai/v1/embeddings`
|
||||||
|
- Vertex OpenAI root `.../endpoints/openapi` 打 `openai:chat` / `openai:embedding` 时直接挂对应 OpenAI path
|
||||||
|
- 自定义 Vertex OpenAI-compatible service account endpoint 必须进入 Vertex auth refresh 上下文;普通 `aiplatform.googleapis.com` + `openai:*` 不得被误认成 Vertex OpenAI-compatible
|
||||||
|
10. Admin write validation:
|
||||||
|
- Vertex API key key formats 允许 `gemini:generate_content` 与 `gemini:embedding`
|
||||||
|
- Vertex service account key formats 允许 `claude:messages`、`gemini:generate_content` 与 `gemini:embedding`
|
||||||
|
|
||||||
测试断言必须检查具体 URL、具体 action、具体 unsupported 结果,不能只检查 `Some(url)` 或状态码。
|
测试断言必须检查具体 URL、具体 action、具体 unsupported 结果,不能只检查 `Some(url)` 或状态码。
|
||||||
|
|
||||||
@@ -318,6 +360,7 @@ http://aether-app:8084/v1
|
|||||||
3. 不为了让 HTTP 200 看起来成功而接受空 candidate / MAX_TOKENS 无 visible content。
|
3. 不为了让 HTTP 200 看起来成功而接受空 candidate / MAX_TOKENS 无 visible content。
|
||||||
4. 不改前端视觉定制、字体、品牌名、landing page 设计。
|
4. 不改前端视觉定制、字体、品牌名、landing page 设计。
|
||||||
5. 不用旧 provider endpoint 继续承担新主链。
|
5. 不用旧 provider endpoint 继续承担新主链。
|
||||||
|
6. 不把 Google OpenAI-compatible endpoint 当成 native Gemini endpoint 的隐藏 fallback;它只能通过显式 `openai:*` endpoint 进入。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user