mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
feat(provider): 原生接入 Windsurf provider
This commit is contained in:
@@ -17,14 +17,18 @@ pub use provider::{ProviderPoolAdapter, ProviderPoolMemberInput};
|
||||
pub use providers::{
|
||||
build_antigravity_pool_quota_request, build_chatgpt_web_pool_quota_request,
|
||||
build_codex_pool_quota_request, build_kiro_pool_quota_request,
|
||||
enrich_chatgpt_web_quota_metadata, grok_mode_id_for_model, grok_pool_tier_from_quota_bucket,
|
||||
grok_quota_window_key_for_model, grok_supported_quota_windows_for_tier,
|
||||
normalize_chatgpt_web_image_quota_limit, AntigravityProviderPoolAdapter,
|
||||
ChatGptWebProviderPoolAdapter, CodexProviderPoolAdapter, DefaultProviderPoolAdapter,
|
||||
GrokProviderPoolAdapter, KiroPoolQuotaAuthInput, KiroProviderPoolAdapter,
|
||||
UnsupportedQuotaProviderPoolAdapter, ANTIGRAVITY_FETCH_AVAILABLE_MODELS_PATH,
|
||||
build_windsurf_pool_model_configs_request,
|
||||
build_windsurf_pool_model_configs_request_with_base_url, build_windsurf_pool_quota_request,
|
||||
build_windsurf_pool_quota_request_with_base_url, build_windsurf_pool_rate_limit_request,
|
||||
build_windsurf_pool_rate_limit_request_with_base_url, enrich_chatgpt_web_quota_metadata,
|
||||
grok_mode_id_for_model, grok_pool_tier_from_quota_bucket, grok_quota_window_key_for_model,
|
||||
grok_supported_quota_windows_for_tier, normalize_chatgpt_web_image_quota_limit,
|
||||
AntigravityProviderPoolAdapter, ChatGptWebProviderPoolAdapter, CodexProviderPoolAdapter,
|
||||
DefaultProviderPoolAdapter, GrokProviderPoolAdapter, KiroPoolQuotaAuthInput,
|
||||
KiroProviderPoolAdapter, UnsupportedQuotaProviderPoolAdapter, ANTIGRAVITY_FETCH_AVAILABLE_MODELS_PATH,
|
||||
CHATGPT_WEB_CONVERSATION_INIT_PATH, CHATGPT_WEB_DEFAULT_BASE_URL, CODEX_WHAM_USAGE_URL,
|
||||
KIRO_USAGE_LIMITS_PATH, KIRO_USAGE_SDK_VERSION,
|
||||
KIRO_USAGE_LIMITS_PATH, KIRO_USAGE_SDK_VERSION, WINDSURF_MODEL_CONFIGS_PATH,
|
||||
WINDSURF_RATE_LIMIT_PATH, WINDSURF_USER_STATUS_PATH,
|
||||
};
|
||||
pub use quota::{
|
||||
provider_pool_key_account_quota_exhausted, provider_pool_key_scheduling_label,
|
||||
@@ -69,7 +73,8 @@ mod tests {
|
||||
"gemini_cli",
|
||||
"grok",
|
||||
"kiro",
|
||||
"vertex_ai"
|
||||
"vertex_ai",
|
||||
"windsurf"
|
||||
]
|
||||
);
|
||||
assert!(service
|
||||
@@ -85,11 +90,12 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
service.provider_types_for_capability(ProviderPoolCapability::QuotaRefresh),
|
||||
["antigravity", "chatgpt_web", "codex", "grok", "kiro"]
|
||||
["antigravity", "chatgpt_web", "codex", "grok", "kiro", "windsurf"]
|
||||
);
|
||||
assert!(service.supports_quota_refresh("codex"));
|
||||
assert!(service.supports_quota_refresh("antigravity"));
|
||||
assert!(service.supports_quota_refresh("grok"));
|
||||
assert!(service.supports_quota_refresh("windsurf"));
|
||||
assert!(!service.supports_quota_refresh("gemini_cli"));
|
||||
assert_eq!(
|
||||
service.quota_refresh_unsupported_message("claude_code"),
|
||||
@@ -238,6 +244,110 @@ mod tests {
|
||||
assert_eq!(metadata["image_quota_used"], json!(33.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windsurf_quota_request_uses_user_status_connect_rpc() {
|
||||
let spec = build_windsurf_pool_quota_request("key-ws", "session-token-123");
|
||||
|
||||
assert_eq!(spec.request_id, "windsurf-quota:key-ws");
|
||||
assert_eq!(spec.method, "POST");
|
||||
assert_eq!(
|
||||
spec.url,
|
||||
format!("https://server.codeium.com{WINDSURF_USER_STATUS_PATH}")
|
||||
);
|
||||
assert_eq!(spec.content_type.as_deref(), Some("application/json"));
|
||||
assert_eq!(
|
||||
spec.headers
|
||||
.get("connect-protocol-version")
|
||||
.map(String::as_str),
|
||||
Some("1")
|
||||
);
|
||||
assert_eq!(
|
||||
spec.json_body
|
||||
.as_ref()
|
||||
.and_then(|body| body.pointer("/metadata/apiKey"))
|
||||
.and_then(Value::as_str),
|
||||
Some("session-token-123")
|
||||
);
|
||||
assert_eq!(spec.provider_api_format, "windsurf:user_status");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windsurf_model_and_rate_limit_requests_use_connect_rpc_metadata() {
|
||||
let models = build_windsurf_pool_model_configs_request("key-ws", "api-key-123");
|
||||
let rate_limit = build_windsurf_pool_rate_limit_request("key-ws", "api-key-123");
|
||||
|
||||
assert_eq!(
|
||||
models.url,
|
||||
format!("https://server.codeium.com{WINDSURF_MODEL_CONFIGS_PATH}")
|
||||
);
|
||||
assert_eq!(
|
||||
rate_limit.url,
|
||||
format!("https://server.codeium.com{WINDSURF_RATE_LIMIT_PATH}")
|
||||
);
|
||||
for spec in [models, rate_limit] {
|
||||
assert_eq!(spec.method, "POST");
|
||||
assert_eq!(
|
||||
spec.headers
|
||||
.get("connect-protocol-version")
|
||||
.map(String::as_str),
|
||||
Some("1")
|
||||
);
|
||||
assert_eq!(
|
||||
spec.json_body
|
||||
.as_ref()
|
||||
.and_then(|body| body.pointer("/metadata/apiKey"))
|
||||
.and_then(Value::as_str),
|
||||
Some("api-key-123")
|
||||
);
|
||||
assert_eq!(spec.client_api_format, "openai:chat");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windsurf_rate_limit_metadata_keeps_member_schedulable() {
|
||||
let service = ProviderPoolService::with_builtin_adapters();
|
||||
let key = sample_key(Some(json!({
|
||||
"windsurf": {
|
||||
"updated_at": 1_700_000_000u64,
|
||||
"rate_limit": {
|
||||
"limited": true,
|
||||
"retry_after_ms": 60_000
|
||||
}
|
||||
}
|
||||
})));
|
||||
|
||||
let signals = service.member_signals("windsurf", &key, None);
|
||||
|
||||
assert!(!signals.quota_exhausted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windsurf_status_snapshot_ban_marks_member_exhausted() {
|
||||
let service = ProviderPoolService::with_builtin_adapters();
|
||||
let mut key = sample_key(Some(json!({
|
||||
"windsurf": {
|
||||
"updated_at": 1_700_000_000u64,
|
||||
"daily_remaining_percent": 100.0
|
||||
}
|
||||
})));
|
||||
key.status_snapshot = Some(json!({
|
||||
"quota": {
|
||||
"provider_type": "windsurf",
|
||||
"code": "banned",
|
||||
"exhausted": false,
|
||||
"windows": [{
|
||||
"code": "daily",
|
||||
"used_ratio": 0.0,
|
||||
"remaining_ratio": 1.0
|
||||
}]
|
||||
}
|
||||
}));
|
||||
|
||||
let signals = service.member_signals("windsurf", &key, None);
|
||||
|
||||
assert!(signals.quota_exhausted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preset_payload_derives_provider_support_from_capabilities() {
|
||||
let payload = build_admin_pool_scheduling_presets_payload();
|
||||
@@ -251,10 +361,14 @@ mod tests {
|
||||
.find(|item| item["name"] == "recent_refresh")
|
||||
.expect("recent_refresh should exist");
|
||||
|
||||
assert_eq!(free_first["providers"], json!(["codex", "grok", "kiro"]));
|
||||
assert_eq!(
|
||||
free_first["providers"],
|
||||
json!(["codex", "grok", "kiro", "windsurf"])
|
||||
);
|
||||
assert_eq!(
|
||||
recent_refresh["providers"],
|
||||
json!(["codex", "grok", "kiro"])
|
||||
json!(["codex", "grok", "kiro", "windsurf"])
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ pub mod default;
|
||||
pub mod grok;
|
||||
pub mod kiro;
|
||||
pub mod unsupported;
|
||||
pub mod windsurf;
|
||||
|
||||
pub use antigravity::AntigravityProviderPoolAdapter;
|
||||
pub use antigravity::{
|
||||
@@ -32,3 +33,11 @@ pub use unsupported::{
|
||||
UnsupportedQuotaProviderPoolAdapter, CLAUDE_CODE_PROVIDER_POOL_ADAPTER,
|
||||
GEMINI_CLI_PROVIDER_POOL_ADAPTER, VERTEX_AI_PROVIDER_POOL_ADAPTER,
|
||||
};
|
||||
pub use windsurf::{
|
||||
build_windsurf_pool_model_configs_request,
|
||||
build_windsurf_pool_model_configs_request_with_base_url, build_windsurf_pool_quota_request,
|
||||
build_windsurf_pool_quota_request_with_base_url, build_windsurf_pool_rate_limit_request,
|
||||
build_windsurf_pool_rate_limit_request_with_base_url, WindsurfProviderPoolAdapter,
|
||||
WINDSURF_DEFAULT_BASE_URL, WINDSURF_MODEL_CONFIGS_PATH, WINDSURF_RATE_LIMIT_PATH,
|
||||
WINDSURF_USER_STATUS_PATH,
|
||||
};
|
||||
|
||||
286
crates/aether-provider-pool/src/providers/windsurf.rs
Normal file
286
crates/aether-provider-pool/src/providers/windsurf.rs
Normal file
@@ -0,0 +1,286 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use aether_data_contracts::repository::provider_catalog::{
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
|
||||
};
|
||||
use aether_pool_core::PoolSchedulingPreset;
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
use crate::capability::ProviderPoolCapabilities;
|
||||
use crate::provider::{
|
||||
provider_pool_endpoint_format_matches, provider_pool_matching_endpoint, ProviderPoolAdapter,
|
||||
ProviderPoolMemberInput,
|
||||
};
|
||||
use crate::quota::{
|
||||
provider_pool_json_bool, provider_pool_json_f64, provider_pool_member_quota_snapshot,
|
||||
provider_pool_metadata_bucket, provider_pool_quota_snapshot_exhausted_decision,
|
||||
};
|
||||
use crate::quota_refresh::ProviderPoolQuotaRequestSpec;
|
||||
|
||||
pub const WINDSURF_DEFAULT_BASE_URL: &str = "https://server.codeium.com";
|
||||
pub const WINDSURF_USER_STATUS_PATH: &str =
|
||||
"/exa.seat_management_pb.SeatManagementService/GetUserStatus";
|
||||
pub const WINDSURF_MODEL_CONFIGS_PATH: &str =
|
||||
"/exa.api_server_pb.ApiServerService/GetCascadeModelConfigs";
|
||||
pub const WINDSURF_RATE_LIMIT_PATH: &str =
|
||||
"/exa.api_server_pb.ApiServerService/CheckUserMessageRateLimit";
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct WindsurfProviderPoolAdapter;
|
||||
|
||||
impl ProviderPoolAdapter for WindsurfProviderPoolAdapter {
|
||||
fn provider_type(&self) -> &'static str {
|
||||
"windsurf"
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> ProviderPoolCapabilities {
|
||||
ProviderPoolCapabilities {
|
||||
plan_tier: true,
|
||||
quota_reset: true,
|
||||
quota_refresh: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn default_scheduling_presets(&self) -> Vec<PoolSchedulingPreset> {
|
||||
vec![PoolSchedulingPreset {
|
||||
preset: "recent_refresh".to_string(),
|
||||
enabled: true,
|
||||
mode: None,
|
||||
}]
|
||||
}
|
||||
|
||||
fn quota_exhausted(&self, input: &ProviderPoolMemberInput<'_>) -> bool {
|
||||
if windsurf_quota_snapshot_hard_exhausted(input.key, input.provider_type) {
|
||||
return true;
|
||||
}
|
||||
if let Some(exhausted) =
|
||||
provider_pool_quota_snapshot_exhausted_decision(input.key, input.provider_type)
|
||||
{
|
||||
return exhausted;
|
||||
}
|
||||
provider_pool_metadata_bucket(input.key.upstream_metadata.as_ref(), input.provider_type)
|
||||
.is_some_and(windsurf_quota_exhausted_from_bucket)
|
||||
}
|
||||
|
||||
fn quota_refresh_endpoint(
|
||||
&self,
|
||||
endpoints: &[StoredProviderCatalogEndpoint],
|
||||
include_inactive: bool,
|
||||
) -> Option<StoredProviderCatalogEndpoint> {
|
||||
provider_pool_matching_endpoint(endpoints, include_inactive, |endpoint| {
|
||||
provider_pool_endpoint_format_matches(endpoint, "openai:chat")
|
||||
})
|
||||
}
|
||||
|
||||
fn quota_refresh_missing_endpoint_message(&self) -> String {
|
||||
"找不到有效的 openai:chat 端点".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn windsurf_quota_snapshot_hard_exhausted(
|
||||
key: &StoredProviderCatalogKey,
|
||||
provider_type: &str,
|
||||
) -> bool {
|
||||
provider_pool_member_quota_snapshot(key, provider_type)
|
||||
.and_then(|quota| quota.get("code"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.map(str::to_ascii_lowercase)
|
||||
.is_some_and(|code| matches!(code.as_str(), "banned" | "forbidden" | "quarantined"))
|
||||
}
|
||||
|
||||
pub fn build_windsurf_pool_quota_request(
|
||||
key_id: &str,
|
||||
api_key: &str,
|
||||
) -> ProviderPoolQuotaRequestSpec {
|
||||
build_windsurf_pool_quota_request_with_base_url(key_id, WINDSURF_DEFAULT_BASE_URL, api_key)
|
||||
}
|
||||
|
||||
pub fn build_windsurf_pool_quota_request_with_base_url(
|
||||
key_id: &str,
|
||||
base_url: &str,
|
||||
api_key: &str,
|
||||
) -> ProviderPoolQuotaRequestSpec {
|
||||
build_windsurf_connect_rpc_request(
|
||||
format!("windsurf-quota:{key_id}"),
|
||||
"windsurf:user_status",
|
||||
"windsurf-user-status",
|
||||
base_url,
|
||||
WINDSURF_USER_STATUS_PATH,
|
||||
api_key,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn build_windsurf_pool_model_configs_request(
|
||||
key_id: &str,
|
||||
api_key: &str,
|
||||
) -> ProviderPoolQuotaRequestSpec {
|
||||
build_windsurf_pool_model_configs_request_with_base_url(
|
||||
key_id,
|
||||
WINDSURF_DEFAULT_BASE_URL,
|
||||
api_key,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn build_windsurf_pool_model_configs_request_with_base_url(
|
||||
key_id: &str,
|
||||
base_url: &str,
|
||||
api_key: &str,
|
||||
) -> ProviderPoolQuotaRequestSpec {
|
||||
build_windsurf_connect_rpc_request(
|
||||
format!("windsurf-models:{key_id}"),
|
||||
"windsurf:model_configs",
|
||||
"windsurf-model-configs",
|
||||
base_url,
|
||||
WINDSURF_MODEL_CONFIGS_PATH,
|
||||
api_key,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn build_windsurf_pool_rate_limit_request(
|
||||
key_id: &str,
|
||||
api_key: &str,
|
||||
) -> ProviderPoolQuotaRequestSpec {
|
||||
build_windsurf_pool_rate_limit_request_with_base_url(key_id, WINDSURF_DEFAULT_BASE_URL, api_key)
|
||||
}
|
||||
|
||||
pub fn build_windsurf_pool_rate_limit_request_with_base_url(
|
||||
key_id: &str,
|
||||
base_url: &str,
|
||||
api_key: &str,
|
||||
) -> ProviderPoolQuotaRequestSpec {
|
||||
build_windsurf_connect_rpc_request(
|
||||
format!("windsurf-rate-limit:{key_id}"),
|
||||
"windsurf:rate_limit",
|
||||
"windsurf-rate-limit",
|
||||
base_url,
|
||||
WINDSURF_RATE_LIMIT_PATH,
|
||||
api_key,
|
||||
)
|
||||
}
|
||||
|
||||
fn build_windsurf_connect_rpc_request(
|
||||
request_id: String,
|
||||
provider_api_format: &str,
|
||||
model_name: &str,
|
||||
base_url: &str,
|
||||
path: &str,
|
||||
api_key: &str,
|
||||
) -> ProviderPoolQuotaRequestSpec {
|
||||
let mut headers = BTreeMap::new();
|
||||
headers.insert("content-type".to_string(), "application/json".to_string());
|
||||
headers.insert("accept".to_string(), "application/json".to_string());
|
||||
headers.insert("connect-protocol-version".to_string(), "1".to_string());
|
||||
headers.insert("user-agent".to_string(), "windsurf/1.9600.41".to_string());
|
||||
|
||||
ProviderPoolQuotaRequestSpec {
|
||||
request_id,
|
||||
provider_name: "windsurf".to_string(),
|
||||
quota_kind: "windsurf".to_string(),
|
||||
method: "POST".to_string(),
|
||||
url: format!("{}{}", base_url.trim_end_matches('/'), path),
|
||||
headers,
|
||||
content_type: Some("application/json".to_string()),
|
||||
json_body: Some(json!({
|
||||
"metadata": windsurf_metadata(api_key),
|
||||
})),
|
||||
client_api_format: "openai:chat".to_string(),
|
||||
provider_api_format: provider_api_format.to_string(),
|
||||
model_name: Some(model_name.to_string()),
|
||||
accept_invalid_certs: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn windsurf_metadata(api_key: &str) -> Value {
|
||||
json!({
|
||||
"apiKey": api_key,
|
||||
"ideName": "windsurf",
|
||||
"ideVersion": "1.9600.41",
|
||||
"extensionName": "windsurf",
|
||||
"extensionVersion": "1.9600.41",
|
||||
"locale": "en",
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn windsurf_quota_exhausted_from_bucket(bucket: &Map<String, Value>) -> bool {
|
||||
if provider_pool_json_bool(bucket.get("banned"))
|
||||
.or_else(|| provider_pool_json_bool(bucket.get("quarantined")))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
let daily_remaining = provider_pool_json_f64(bucket.get("daily_remaining_percent"));
|
||||
let weekly_remaining = provider_pool_json_f64(bucket.get("weekly_remaining_percent"));
|
||||
daily_remaining.is_some_and(|value| value <= 0.0)
|
||||
|| weekly_remaining.is_some_and(|value| value <= 0.0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{windsurf_quota_exhausted_from_bucket, windsurf_quota_snapshot_hard_exhausted};
|
||||
use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey;
|
||||
use serde_json::json;
|
||||
|
||||
fn sample_key_with_quota(code: &str, exhausted: bool) -> StoredProviderCatalogKey {
|
||||
let mut key = StoredProviderCatalogKey::new(
|
||||
"key-windsurf".to_string(),
|
||||
"provider-windsurf".to_string(),
|
||||
"windsurf@example.com".to_string(),
|
||||
"oauth".to_string(),
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.expect("sample key should build");
|
||||
key.status_snapshot = Some(json!({
|
||||
"quota": {
|
||||
"provider_type": "windsurf",
|
||||
"code": code,
|
||||
"exhausted": exhausted,
|
||||
"windows": [{
|
||||
"code": "daily",
|
||||
"used_ratio": 0.0,
|
||||
"remaining_ratio": 1.0
|
||||
}]
|
||||
}
|
||||
}));
|
||||
key
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windsurf_rate_limit_bucket_does_not_mark_quota_exhausted() {
|
||||
let bucket = json!({
|
||||
"rate_limit": {
|
||||
"limited": true,
|
||||
"retry_after_ms": 60_000u64
|
||||
},
|
||||
"daily_remaining_percent": 50.0,
|
||||
"weekly_remaining_percent": 50.0
|
||||
});
|
||||
let bucket = bucket.as_object().expect("bucket should be object");
|
||||
|
||||
assert!(!windsurf_quota_exhausted_from_bucket(bucket));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windsurf_banned_and_quarantined_snapshot_codes_are_hard_exhausted() {
|
||||
for code in ["banned", "forbidden", "quarantined"] {
|
||||
let key = sample_key_with_quota(code, false);
|
||||
|
||||
assert!(
|
||||
windsurf_quota_snapshot_hard_exhausted(&key, "windsurf"),
|
||||
"{code} should be hard exhausted"
|
||||
);
|
||||
}
|
||||
|
||||
let cooldown_key = sample_key_with_quota("cooldown", false);
|
||||
assert!(!windsurf_quota_snapshot_hard_exhausted(
|
||||
&cooldown_key,
|
||||
"windsurf"
|
||||
));
|
||||
let rate_limited_key = sample_key_with_quota("rate_limited", false);
|
||||
assert!(!windsurf_quota_snapshot_hard_exhausted(
|
||||
&rate_limited_key,
|
||||
"windsurf"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ use crate::provider::{ProviderPoolAdapter, ProviderPoolMemberInput};
|
||||
use crate::providers::{
|
||||
AntigravityProviderPoolAdapter, ChatGptWebProviderPoolAdapter, CodexProviderPoolAdapter,
|
||||
DefaultProviderPoolAdapter, GrokProviderPoolAdapter, KiroProviderPoolAdapter,
|
||||
WindsurfProviderPoolAdapter,
|
||||
CLAUDE_CODE_PROVIDER_POOL_ADAPTER, GEMINI_CLI_PROVIDER_POOL_ADAPTER,
|
||||
VERTEX_AI_PROVIDER_POOL_ADAPTER,
|
||||
};
|
||||
@@ -54,6 +55,7 @@ impl ProviderPoolService {
|
||||
.with_adapter(Arc::new(GrokProviderPoolAdapter))
|
||||
.with_adapter(Arc::new(KiroProviderPoolAdapter))
|
||||
.with_adapter(Arc::new(ChatGptWebProviderPoolAdapter))
|
||||
.with_adapter(Arc::new(WindsurfProviderPoolAdapter))
|
||||
.with_adapter(Arc::new(VERTEX_AI_PROVIDER_POOL_ADAPTER))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user