mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 16:37:46 +08:00
feat(embedding): 接入阿里云多模态向量端点
This commit is contained in:
@@ -184,7 +184,11 @@ pub fn request_pair_transport_unsupported_reason(
|
||||
)
|
||||
}
|
||||
}
|
||||
"openai:embedding" | "jina:embedding" | "doubao:embedding" | "openai:rerank"
|
||||
"openai:embedding"
|
||||
| "jina:embedding"
|
||||
| "doubao:embedding"
|
||||
| "aliyun:multimodal_embedding"
|
||||
| "openai:rerank"
|
||||
| "jina:rerank" => local_standard_transport_unsupported_reason_with_network(
|
||||
transport,
|
||||
provider_api_format.as_str(),
|
||||
@@ -221,6 +225,7 @@ fn request_direct_auth_for_provider_format(
|
||||
| "openai:embedding"
|
||||
| "jina:embedding"
|
||||
| "doubao:embedding"
|
||||
| "aliyun:multimodal_embedding"
|
||||
| "openai:rerank"
|
||||
| "jina:rerank" => resolve_local_openai_bearer_auth(transport),
|
||||
"gemini:generate_content" | "gemini:embedding" => {
|
||||
|
||||
@@ -225,7 +225,7 @@ fn endpoint_kind_allows_embedding(endpoint_kind: Option<&str>) -> bool {
|
||||
.map(|value| {
|
||||
matches!(
|
||||
value.to_ascii_lowercase().as_str(),
|
||||
"embedding" | "embeddings"
|
||||
"embedding" | "embeddings" | "multimodal_embedding" | "multimodal_embeddings"
|
||||
)
|
||||
})
|
||||
.unwrap_or(true)
|
||||
@@ -348,10 +348,13 @@ mod tests {
|
||||
("jina", "jina:embedding"),
|
||||
("doubao", "doubao:embedding"),
|
||||
("volcengine", "doubao:embedding"),
|
||||
("aliyun", "aliyun:multimodal_embedding"),
|
||||
("dashscope", "aliyun:multimodal_embedding"),
|
||||
("custom", "openai:embedding"),
|
||||
("custom", "gemini:embedding"),
|
||||
("custom", "jina:embedding"),
|
||||
("custom", "doubao:embedding"),
|
||||
("custom", "aliyun:multimodal_embedding"),
|
||||
] {
|
||||
let transport = sample_transport(provider_type, api_format, Some("embedding"));
|
||||
assert_eq!(
|
||||
|
||||
@@ -84,6 +84,7 @@ pub enum ProviderLocalEmbeddingSupport {
|
||||
Gemini,
|
||||
Jina,
|
||||
Doubao,
|
||||
Aliyun,
|
||||
}
|
||||
|
||||
impl ProviderLocalEmbeddingSupport {
|
||||
@@ -99,11 +100,13 @@ impl ProviderLocalEmbeddingSupport {
|
||||
| "jina:embedding"
|
||||
| "jina:rerank"
|
||||
| "doubao:embedding"
|
||||
| "aliyun:multimodal_embedding"
|
||||
),
|
||||
Self::OpenAi => matches!(api_format.as_str(), "openai:embedding" | "openai:rerank"),
|
||||
Self::Gemini => api_format == "gemini:embedding",
|
||||
Self::Jina => matches!(api_format.as_str(), "jina:embedding" | "jina:rerank"),
|
||||
Self::Doubao => api_format == "doubao:embedding",
|
||||
Self::Aliyun => api_format == "aliyun:multimodal_embedding",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,6 +190,10 @@ const DOUBAO_RUNTIME_POLICY: ProviderRuntimePolicy = ProviderRuntimePolicy {
|
||||
local_embedding_support: ProviderLocalEmbeddingSupport::Doubao,
|
||||
..STANDARD_RUNTIME_POLICY
|
||||
};
|
||||
const ALIYUN_RUNTIME_POLICY: ProviderRuntimePolicy = ProviderRuntimePolicy {
|
||||
local_embedding_support: ProviderLocalEmbeddingSupport::Aliyun,
|
||||
..STANDARD_RUNTIME_POLICY
|
||||
};
|
||||
|
||||
const CLAUDE_CODE_RUNTIME_POLICY: ProviderRuntimePolicy = ProviderRuntimePolicy {
|
||||
fixed_provider: true,
|
||||
@@ -465,6 +472,7 @@ pub fn provider_runtime_policy(provider_type: &str) -> ProviderRuntimePolicy {
|
||||
"gemini" | "google" => GEMINI_RUNTIME_POLICY,
|
||||
"jina" => JINA_RUNTIME_POLICY,
|
||||
"doubao" | "volcengine" => DOUBAO_RUNTIME_POLICY,
|
||||
"aliyun" | "dashscope" => ALIYUN_RUNTIME_POLICY,
|
||||
_ => STANDARD_RUNTIME_POLICY,
|
||||
}
|
||||
}
|
||||
@@ -846,6 +854,8 @@ mod tests {
|
||||
("jina", "jina:embedding"),
|
||||
("doubao", "doubao:embedding"),
|
||||
("volcengine", "doubao:embedding"),
|
||||
("aliyun", "aliyun:multimodal_embedding"),
|
||||
("dashscope", "aliyun:multimodal_embedding"),
|
||||
] {
|
||||
assert!(
|
||||
provider_type_supports_local_embedding_transport(provider_type, api_format),
|
||||
@@ -859,6 +869,8 @@ mod tests {
|
||||
("vertex_ai", "openai:embedding"),
|
||||
("jina", "doubao:embedding"),
|
||||
("doubao", "jina:embedding"),
|
||||
("aliyun", "openai:embedding"),
|
||||
("openai", "aliyun:multimodal_embedding"),
|
||||
("claude_code", "openai:embedding"),
|
||||
("openai", "openai:chat"),
|
||||
] {
|
||||
|
||||
@@ -125,6 +125,10 @@ fn build_transport_request_url_inner(
|
||||
"openai:embedding" | "jina:embedding" => {
|
||||
build_provider_embedding_v1_url(&transport.endpoint.base_url, params.request_query)
|
||||
}
|
||||
"aliyun:multimodal_embedding" => build_aliyun_multimodal_embedding_url(
|
||||
&transport.endpoint.base_url,
|
||||
params.request_query,
|
||||
),
|
||||
"openai:rerank" | "jina:rerank" => {
|
||||
build_provider_rerank_v1_url(&transport.endpoint.base_url, params.request_query)
|
||||
}
|
||||
@@ -386,6 +390,18 @@ fn build_provider_embedding_v1_url(upstream_base_url: &str, query: Option<&str>)
|
||||
build_provider_v1_url(upstream_base_url, "/embeddings", "/v1/embeddings", query)
|
||||
}
|
||||
|
||||
fn build_aliyun_multimodal_embedding_url(
|
||||
upstream_base_url: &str,
|
||||
query: Option<&str>,
|
||||
) -> Option<String> {
|
||||
build_passthrough_path_url(
|
||||
upstream_base_url,
|
||||
"/api/v1/services/embeddings/multimodal-embedding/multimodal-embedding",
|
||||
query,
|
||||
&[],
|
||||
)
|
||||
}
|
||||
|
||||
fn build_provider_rerank_v1_url(upstream_base_url: &str, query: Option<&str>) -> Option<String> {
|
||||
build_provider_v1_url(upstream_base_url, "/rerank", "/v1/rerank", query)
|
||||
}
|
||||
@@ -936,6 +952,12 @@ mod tests {
|
||||
"https://ark.volces.example/api/v3",
|
||||
None,
|
||||
);
|
||||
let aliyun = sample_transport(
|
||||
"aliyun",
|
||||
"aliyun:multimodal_embedding",
|
||||
"https://dashscope.aliyuncs.com",
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
build_transport_request_url(
|
||||
@@ -995,6 +1017,20 @@ mod tests {
|
||||
.as_deref(),
|
||||
Some("https://ark.volces.example/api/v3/embeddings")
|
||||
);
|
||||
assert_eq!(
|
||||
build_transport_request_url(
|
||||
&aliyun,
|
||||
TransportRequestUrlParams {
|
||||
provider_api_format: "aliyun:multimodal_embedding",
|
||||
mapped_model: Some("qwen3-vl-embedding"),
|
||||
upstream_is_stream: false,
|
||||
request_query: None,
|
||||
kiro_api_region: None,
|
||||
},
|
||||
)
|
||||
.as_deref(),
|
||||
Some("https://dashscope.aliyuncs.com/api/v1/services/embeddings/multimodal-embedding/multimodal-embedding")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -159,6 +159,14 @@ pub fn build_same_format_provider_request_body(
|
||||
);
|
||||
}
|
||||
|
||||
if embedding_multimodal_input_requires_aliyun_provider(
|
||||
input.client_api_format,
|
||||
input.provider_api_format,
|
||||
input.body_json,
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut provider_request_body = if aether_ai_formats::api_format_alias_matches(
|
||||
input.client_api_format,
|
||||
input.provider_api_format,
|
||||
@@ -235,6 +243,31 @@ pub fn build_same_format_provider_request_body(
|
||||
Some(provider_request_body)
|
||||
}
|
||||
|
||||
fn embedding_multimodal_input_requires_aliyun_provider(
|
||||
client_api_format: &str,
|
||||
provider_api_format: &str,
|
||||
body_json: &Value,
|
||||
) -> bool {
|
||||
aether_ai_formats::is_embedding_api_format(client_api_format)
|
||||
&& embedding_input_is_multimodal(body_json.get("input"))
|
||||
&& aether_ai_formats::normalize_api_format_alias(provider_api_format)
|
||||
!= "aliyun:multimodal_embedding"
|
||||
}
|
||||
|
||||
fn embedding_input_is_multimodal(value: Option<&Value>) -> bool {
|
||||
value
|
||||
.and_then(Value::as_array)
|
||||
.is_some_and(|items| !items.is_empty() && items.iter().all(embedding_content_is_multimodal))
|
||||
}
|
||||
|
||||
fn embedding_content_is_multimodal(value: &Value) -> bool {
|
||||
value.as_object().is_some_and(|object| {
|
||||
["text", "image", "video", "multi_images"]
|
||||
.iter()
|
||||
.any(|key| object.contains_key(*key))
|
||||
})
|
||||
}
|
||||
|
||||
fn strip_gemini_function_response_ids(value: &mut Value) {
|
||||
match value {
|
||||
Value::Object(object) => {
|
||||
@@ -728,6 +761,33 @@ mod tests {
|
||||
assert_eq!(body.get("stream"), Some(&json!(true)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_format_embedding_body_rejects_multimodal_for_openai_like_provider() {
|
||||
let body = build_same_format_provider_request_body(SameFormatProviderRequestBodyInput {
|
||||
body_json: &json!({
|
||||
"model": "qwen3-vl-embedding",
|
||||
"input": [
|
||||
{"text": "white running shoes"},
|
||||
{"image": "https://example.com/shoe.png"}
|
||||
]
|
||||
}),
|
||||
mapped_model: "openai-qwen-fallback",
|
||||
client_api_format: "openai:embedding",
|
||||
provider_api_format: "openai:embedding",
|
||||
source_model: Some("qwen3-vl-embedding"),
|
||||
family: SameFormatProviderFamily::Standard,
|
||||
body_rules: None,
|
||||
request_headers: None,
|
||||
upstream_is_stream: false,
|
||||
force_body_stream_field: false,
|
||||
kiro_auth_config: None,
|
||||
is_claude_code: false,
|
||||
enable_model_directives: false,
|
||||
});
|
||||
|
||||
assert!(body.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_format_standard_body_overrides_client_stream_for_non_stream_upstream() {
|
||||
let body = build_same_format_provider_request_body(SameFormatProviderRequestBodyInput {
|
||||
|
||||
Reference in New Issue
Block a user