feat(provider): add Google Vertex AI provider#8621
Conversation
Add a native Google Vertex AI provider preset that reuses the googlegenai_chat_completion adapter to call Gemini models through the Vertex AI native API (instead of an OpenAI-compatible shim). - New "Google Vertex AI" provider source template (provider: google-vertex-ai) exposing id/api_base as basic config plus the Gemini advanced options (safety settings, thinking config, native search, url context, timeout, proxy, ...). - Two auth modes: service-account JSON (also enables model discovery via the Vertex AI Model Garden publisher endpoint) and Vertex AI API key (generation only). The project id is auto-resolved from the service-account JSON when omitted. - Shared helpers in vertex_ai.py: config normalization, OAuth token refresh with proxy support, AstrBot <-> Vertex model-id mapping (google/* <-> publishers/google/models/*) and model-list fetching. - Dashboard: dedicated Vertex AI config fields, i18n copy for zh-CN/en-US/ru-RU and the provider logo. - Add the google-auth[requests] dependency. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Hey - I've found 3 issues, and left some high level feedback:
- The Vertex AI model name normalization logic is currently split between
vertex_ai.to_vertex_ai_genai_model_name,ProviderGoogleGenAI._normalize_model_name, andfetch_vertex_ai_publisher_models; consider centralizing this in a single helper to avoid future divergence in how IDs are transformed. - In
make_vertex_ai_refresh_request, when a proxy is configured butrequestsis not installed, the function silently falls back to a non-proxiedRequest; consider logging or surfacing this mismatch so users understand why their proxy setting is not being honored. - In
_get_genai_client_kwargs, whenvertex_ai_auth_typeisapi_keybut a service-account JSON is also present, the code implicitly prefers service-account auth; consider making this precedence explicit (e.g., by validating against mixed configuration or documenting the priority) to avoid confusing misconfigurations.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The Vertex AI model name normalization logic is currently split between `vertex_ai.to_vertex_ai_genai_model_name`, `ProviderGoogleGenAI._normalize_model_name`, and `fetch_vertex_ai_publisher_models`; consider centralizing this in a single helper to avoid future divergence in how IDs are transformed.
- In `make_vertex_ai_refresh_request`, when a proxy is configured but `requests` is not installed, the function silently falls back to a non-proxied `Request`; consider logging or surfacing this mismatch so users understand why their proxy setting is not being honored.
- In `_get_genai_client_kwargs`, when `vertex_ai_auth_type` is `api_key` but a service-account JSON is also present, the code implicitly prefers service-account auth; consider making this precedence explicit (e.g., by validating against mixed configuration or documenting the priority) to avoid confusing misconfigurations.
## Individual Comments
### Comment 1
<location path="astrbot/core/provider/sources/gemini_source.py" line_range="1118-1122" />
<code_context>
+ "并填写 Google Cloud 服务账号 JSON 后获取模型列表,或手动添加模型。"
+ )
+
+ credentials = self._load_vertex_ai_service_account_credentials()
+ if not getattr(credentials, "valid", False) or not getattr(
+ credentials, "token", None
+ ):
+ credentials.refresh(make_vertex_ai_refresh_request(self.provider_config))
+
+ token = getattr(credentials, "token", None)
</code_context>
<issue_to_address>
**issue (bug_risk):** Refreshing Vertex AI credentials synchronously inside an async context risks blocking the event loop.
In `_get_vertex_ai_models`, `credentials.refresh(...)` does synchronous network I/O (`google-auth` + `requests`) inside an `async` function, which can block the event loop under slow networks. Please move the refresh into a thread (e.g. `await anyio.to_thread.run_sync(credentials.refresh, make_vertex_ai_refresh_request(self.provider_config))` or `loop.run_in_executor(...)`) so the async runtime stays responsive while tokens are refreshed.
</issue_to_address>
### Comment 2
<location path="astrbot/core/provider/sources/gemini_source.py" line_range="51" />
<code_context>
+)
class SuppressNonTextPartsWarning(logging.Filter):
</code_context>
<issue_to_address>
**issue (complexity):** Consider moving most Vertex AI–specific URL, auth, and model-handling logic into shared helpers in vertex_ai.py so GeminiSource stays a thin, generic consumer of those utilities.
You can simplify this by pushing more of the Vertex-specific decisions into `vertex_ai.py` and turning `GeminiSource` into a thin consumer of those helpers. That will reduce duplication and keep the Vertex logic in one place.
### 1. Centralize base URL / SDK `HttpOptions` logic
`_get_vertex_ai_sdk_base_url` re-implements base URL normalization that’s already conceptually handled in `vertex_ai.py`. Instead, add a helper there that returns the SDK-ready base URL (and api_version when needed) and call it from `_init_client`.
**vertex_ai.py (new helper):**
```python
# vertex_ai.py
def build_vertex_ai_http_options(provider_config, timeout_seconds: int) -> dict:
"""
Return kwargs for types.HttpOptions for Vertex AI:
{
"base_url": ...,
"timeout": ...,
"api_version": ...
}
"""
configured_base_url = str(provider_config.get("api_base") or "").strip().rstrip("/")
location = normalize_vertex_ai_location(provider_config.get("vertex_ai_location"))
base_url = _normalize_base_url(configured_base_url, location)
# _normalize_base_url can encapsulate logic now in _get_vertex_ai_sdk_base_url
kwargs: dict = {
"base_url": base_url,
"timeout": timeout_seconds * 1000,
"api_version": VERTEX_AI_API_VERSION,
}
return kwargs
```
**GeminiSource._init_client (simplified):**
```python
def _init_client(self) -> None:
proxy = self.provider_config.get("proxy", "")
is_vertex_ai = self._is_vertex_ai_config()
if is_vertex_ai:
http_options_kwargs = build_vertex_ai_http_options(
self.provider_config, self.timeout
)
else:
http_options_kwargs = {
"base_url": self.api_base,
"timeout": self.timeout * 1000,
}
http_options = types.HttpOptions(**http_options_kwargs)
async_client_kwargs: dict = {"timeout": self.timeout}
base_url = http_options_kwargs.get("base_url")
if base_url:
async_client_kwargs["base_url"] = base_url
...
self._http_client = httpx.AsyncClient(**async_client_kwargs)
http_options.httpx_async_client = self._http_client
client_kwargs = self._get_genai_client_kwargs(http_options)
self.client = genai.Client(**client_kwargs).aio
```
This removes `_get_vertex_ai_sdk_base_url` entirely and delegates base URL behavior to the dedicated module.
### 2. Move auth decision tree into `vertex_ai.py`
`_get_genai_client_kwargs`, `_load_vertex_ai_service_account_credentials`, `_resolve_vertex_ai_project_id_required`, `_has_vertex_ai_key_json`, `_ensure_vertex_ai_api_key_for_generation` all mix auth-type resolution with SDK wiring. A single central helper in `vertex_ai.py` can return a ready-to-use `client_kwargs` dict, so `GeminiSource` only needs to decide “Vertex vs non-Vertex”.
**vertex_ai.py (new helper):**
```python
# vertex_ai.py
def build_vertex_ai_client_kwargs(provider_config, http_options) -> dict:
auth_type = normalize_vertex_ai_auth_type(
provider_config.get("vertex_ai_auth_type")
)
if auth_type == VERTEX_AI_API_KEY_AUTH and not extract_vertex_ai_credentials_json(provider_config).strip():
# API-key only, no service account JSON
return {
"vertexai": True,
"http_options": http_options,
"api_key": provider_config.get("api_key") or "",
}
if auth_type in {VERTEX_AI_API_KEY_AUTH, VERTEX_AI_JSON_AUTH}:
credentials = load_vertex_ai_service_account_credentials(provider_config)
project = resolve_vertex_ai_project_id(provider_config)
if not project:
raise ValueError("Vertex AI project id is required for service account auth.")
return {
"vertexai": True,
"http_options": http_options,
"credentials": credentials,
"project": project,
"location": normalize_vertex_ai_location(
provider_config.get("vertex_ai_location")
),
}
raise ValueError(
"Vertex AI key format must be api_key or json for Google GenAI provider."
)
```
(You can move the existing service-account loading logic into `load_vertex_ai_service_account_credentials` here instead of duplicating it in `GeminiSource`.)
**GeminiSource._get_genai_client_kwargs (simplified):**
```python
def _get_genai_client_kwargs(self, http_options: types.HttpOptions) -> dict:
if not self._is_vertex_ai_config():
return {
"api_key": self.chosen_api_key,
"http_options": http_options,
}
return build_vertex_ai_client_kwargs(self.provider_config, http_options)
```
**GeminiSource._ensure_vertex_ai_api_key_for_generation (can be removed or wrapped):**
If you still want the guard for pure API-key mode, you can implement that in `vertex_ai.py` (e.g., `ensure_vertex_ai_api_key_for_generation(provider_config, chosen_api_key)`) and call that from `_query` / `_query_stream`, instead of re-implementing the same decision logic locally.
### 3. Centralize model-name normalization & listing
Model name logic is now split across `_get_request_model_name`, `_normalize_model_name`, `to_vertex_ai_genai_model_name`, and Vertex publisher listing. You can delegate most of this into `vertex_ai.py` too.
**vertex_ai.py (new helpers):**
```python
# vertex_ai.py
def normalize_vertex_ai_sdk_model_name(name: str) -> str:
# logic currently in _normalize_model_name for Vertex
name = name.strip()
if VERTEX_AI_MODEL_NAME_SEPARATOR in name:
name = name.rsplit(VERTEX_AI_MODEL_NAME_SEPARATOR, 1)[1]
elif name.startswith("models/"):
name = name.removeprefix("models/")
if not name.startswith("google/"):
name = f"google/{name}"
return name
def normalize_gemini_model_name(name: str) -> str:
return name.strip().replace("models/", "")
```
**GeminiSource:**
```python
def _get_request_model_name(self, model: str) -> str:
if self._is_vertex_ai_config():
return to_vertex_ai_genai_model_name(model)
return model
def _normalize_model_name(self, name: str) -> str:
if self._is_vertex_ai_config():
return normalize_vertex_ai_sdk_model_name(name)
return normalize_gemini_model_name(name)
```
For listing models, consider moving the Vertex-specific `get_models` logic into one helper (e.g., `fetch_vertex_ai_models(provider_config, http_client)`) that encapsulates the token refresh, URL building, and header construction. `GeminiSource.get_models` then just branches and forwards:
```python
async def get_models(self):
if self._is_vertex_ai_config():
return await fetch_vertex_ai_models(self.provider_config, self._http_client)
try:
models = await self.client.models.list()
model_ids = [
self._normalize_model_name(m.name)
for m in models
if m.supported_actions
and "generateContent" in m.supported_actions
and m.name
]
return normalize_and_dedupe_strings(model_ids)
except APIError as e:
raise Exception(f"获取模型列表失败: {e.message}")
```
This way, `GeminiSource` remains responsible for high-level decisions (Vertex vs non-Vertex, streaming vs non-streaming) and delegates endpoint/auth/model-name details to `vertex_ai.py`, reducing the cognitive load and duplication without altering functionality.
</issue_to_address>
### Comment 3
<location path="dashboard/src/composables/useProviderSources.ts" line_range="187" />
<code_context>
const basicSourceConfig = computed(() => {
if (!editableProviderSource.value) return null
- const fields = ['id', 'key', 'api_base']
+ const fields = editableProviderSource.value.provider === 'google-vertex-ai'
+ ? ['id', 'api_base']
</code_context>
<issue_to_address>
**issue (complexity):** Consider centralizing provider-specific field configuration and defaulting logic into dedicated helpers so the composable stays provider-agnostic and easier to extend.
You can keep the new Vertex behavior but reduce complexity by centralizing the provider‑specific rules into a single configuration object instead of scattering `provider === 'google-vertex-ai'` checks.
### 1. Centralize basic/advanced field config per provider
Right now `basicSourceConfig`, `advancedSourceConfig`, `vertexHiddenFields`, `vertexAdvancedSourceFields`, and the Vertex‑specific branching are separate. You can keep behavior identical by introducing a config map:
```ts
const PROVIDER_FIELD_CONFIG = {
default: {
basic: ['id', 'key', 'api_base'],
hidden: new Set(['id', 'key', 'api_base', 'enable', 'type', 'provider_type', 'provider']),
advancedOrder: null as string[] | null
},
'google-vertex-ai': {
basic: ['id', 'api_base'],
hidden: new Set([
'id',
'key',
'api_base',
'enable',
'type',
'provider_type',
'provider',
'vertex_ai_project_id',
'vertex_ai_credentials_path'
]),
advancedOrder: [
'vertex_ai_auth_type',
'vertex_ai_api_key',
'vertex_ai_credentials_json',
'vertex_ai_location',
'timeout',
'proxy',
'gm_resp_image_modal',
'gm_native_search',
'gm_native_coderunner',
'gm_url_context',
'gm_safety_settings',
'gm_thinking_config'
]
}
} as const
function getFieldConfig(provider?: string) {
return PROVIDER_FIELD_CONFIG[provider as keyof typeof PROVIDER_FIELD_CONFIG] ?? PROVIDER_FIELD_CONFIG.default
}
```
Then `basicSourceConfig` and `advancedSourceConfig` become simpler and provider‑agnostic at the call sites:
```ts
const basicSourceConfig = computed(() => {
if (!editableProviderSource.value) return null
const { basic } = getFieldConfig(editableProviderSource.value.provider)
const basicObj: Record<string, any> = {}
basic.forEach((field) => {
Object.defineProperty(basicObj, field, {
get() {
return editableProviderSource.value![field]
},
set(val) {
editableProviderSource.value![field] = val
},
enumerable: true
})
})
return basicObj
})
const advancedSourceConfig = computed(() => {
if (!editableProviderSource.value) return null
const cfg = getFieldConfig(editableProviderSource.value.provider)
const excluded = new Set(cfg.hidden)
const advanced: Record<string, any> = {}
const sourceKeys = Object.keys(editableProviderSource.value)
const keys = cfg.advancedOrder
? [
...cfg.advancedOrder.filter((field) => sourceKeys.includes(field)),
...sourceKeys.filter((field) => !cfg.advancedOrder!.includes(field))
]
: sourceKeys
for (const key of keys) {
Object.defineProperty(advanced, key, {
get() {
return editableProviderSource.value![key]
},
set(val) {
editableProviderSource.value![key] = val
},
enumerable: !excluded.has(key)
})
}
return advanced
})
```
This removes scattered Vertex‑specific sets/arrays and a nested conditional in `advancedSourceConfig`, while preserving:
- Vertex’s different basic fields (`['id', 'api_base']`)
- Vertex’s hidden fields
- Vertex’s advanced field ordering
Adding new providers or tweaking Vertex just means updating `PROVIDER_FIELD_CONFIG` in one place.
### 2. Isolate Vertex default normalization
`ensureProviderSourceDefaults` is starting to accumulate provider logic. You can keep the new Vertex defaults but move them into a dedicated helper to decouple the generic function from the provider specifics:
```ts
function applyVertexDefaults(source: any) {
if (source.provider !== 'google-vertex-ai') return
if (!source.vertex_ai_auth_type || source.vertex_ai_auth_type === 'service_account') {
source.vertex_ai_auth_type = 'json'
}
if (source.vertex_ai_api_key === undefined) {
source.vertex_ai_api_key =
source.vertex_ai_auth_type === 'api_key' ? (source.key || []) : []
}
if (source.vertex_ai_credentials_json === undefined) {
source.vertex_ai_credentials_json = ''
}
if (!source.vertex_ai_location) {
source.vertex_ai_location = 'global'
}
}
function ensureProviderSourceDefaults(source: any) {
if (!source || typeof source !== 'object') {
return source
}
if (source.provider === 'ollama' && source.ollama_disable_thinking === undefined) {
source.ollama_disable_thinking = false
}
applyVertexDefaults(source)
return source
}
```
This keeps the composable’s main flow cleaner and makes it obvious where provider‑specific normalization lives, without reverting any functionality.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| ) | ||
|
|
||
|
|
||
| class SuppressNonTextPartsWarning(logging.Filter): |
There was a problem hiding this comment.
issue (complexity): Consider moving most Vertex AI–specific URL, auth, and model-handling logic into shared helpers in vertex_ai.py so GeminiSource stays a thin, generic consumer of those utilities.
You can simplify this by pushing more of the Vertex-specific decisions into vertex_ai.py and turning GeminiSource into a thin consumer of those helpers. That will reduce duplication and keep the Vertex logic in one place.
1. Centralize base URL / SDK HttpOptions logic
_get_vertex_ai_sdk_base_url re-implements base URL normalization that’s already conceptually handled in vertex_ai.py. Instead, add a helper there that returns the SDK-ready base URL (and api_version when needed) and call it from _init_client.
vertex_ai.py (new helper):
# vertex_ai.py
def build_vertex_ai_http_options(provider_config, timeout_seconds: int) -> dict:
"""
Return kwargs for types.HttpOptions for Vertex AI:
{
"base_url": ...,
"timeout": ...,
"api_version": ...
}
"""
configured_base_url = str(provider_config.get("api_base") or "").strip().rstrip("/")
location = normalize_vertex_ai_location(provider_config.get("vertex_ai_location"))
base_url = _normalize_base_url(configured_base_url, location)
# _normalize_base_url can encapsulate logic now in _get_vertex_ai_sdk_base_url
kwargs: dict = {
"base_url": base_url,
"timeout": timeout_seconds * 1000,
"api_version": VERTEX_AI_API_VERSION,
}
return kwargsGeminiSource._init_client (simplified):
def _init_client(self) -> None:
proxy = self.provider_config.get("proxy", "")
is_vertex_ai = self._is_vertex_ai_config()
if is_vertex_ai:
http_options_kwargs = build_vertex_ai_http_options(
self.provider_config, self.timeout
)
else:
http_options_kwargs = {
"base_url": self.api_base,
"timeout": self.timeout * 1000,
}
http_options = types.HttpOptions(**http_options_kwargs)
async_client_kwargs: dict = {"timeout": self.timeout}
base_url = http_options_kwargs.get("base_url")
if base_url:
async_client_kwargs["base_url"] = base_url
...
self._http_client = httpx.AsyncClient(**async_client_kwargs)
http_options.httpx_async_client = self._http_client
client_kwargs = self._get_genai_client_kwargs(http_options)
self.client = genai.Client(**client_kwargs).aioThis removes _get_vertex_ai_sdk_base_url entirely and delegates base URL behavior to the dedicated module.
2. Move auth decision tree into vertex_ai.py
_get_genai_client_kwargs, _load_vertex_ai_service_account_credentials, _resolve_vertex_ai_project_id_required, _has_vertex_ai_key_json, _ensure_vertex_ai_api_key_for_generation all mix auth-type resolution with SDK wiring. A single central helper in vertex_ai.py can return a ready-to-use client_kwargs dict, so GeminiSource only needs to decide “Vertex vs non-Vertex”.
vertex_ai.py (new helper):
# vertex_ai.py
def build_vertex_ai_client_kwargs(provider_config, http_options) -> dict:
auth_type = normalize_vertex_ai_auth_type(
provider_config.get("vertex_ai_auth_type")
)
if auth_type == VERTEX_AI_API_KEY_AUTH and not extract_vertex_ai_credentials_json(provider_config).strip():
# API-key only, no service account JSON
return {
"vertexai": True,
"http_options": http_options,
"api_key": provider_config.get("api_key") or "",
}
if auth_type in {VERTEX_AI_API_KEY_AUTH, VERTEX_AI_JSON_AUTH}:
credentials = load_vertex_ai_service_account_credentials(provider_config)
project = resolve_vertex_ai_project_id(provider_config)
if not project:
raise ValueError("Vertex AI project id is required for service account auth.")
return {
"vertexai": True,
"http_options": http_options,
"credentials": credentials,
"project": project,
"location": normalize_vertex_ai_location(
provider_config.get("vertex_ai_location")
),
}
raise ValueError(
"Vertex AI key format must be api_key or json for Google GenAI provider."
)(You can move the existing service-account loading logic into load_vertex_ai_service_account_credentials here instead of duplicating it in GeminiSource.)
GeminiSource._get_genai_client_kwargs (simplified):
def _get_genai_client_kwargs(self, http_options: types.HttpOptions) -> dict:
if not self._is_vertex_ai_config():
return {
"api_key": self.chosen_api_key,
"http_options": http_options,
}
return build_vertex_ai_client_kwargs(self.provider_config, http_options)GeminiSource._ensure_vertex_ai_api_key_for_generation (can be removed or wrapped):
If you still want the guard for pure API-key mode, you can implement that in vertex_ai.py (e.g., ensure_vertex_ai_api_key_for_generation(provider_config, chosen_api_key)) and call that from _query / _query_stream, instead of re-implementing the same decision logic locally.
3. Centralize model-name normalization & listing
Model name logic is now split across _get_request_model_name, _normalize_model_name, to_vertex_ai_genai_model_name, and Vertex publisher listing. You can delegate most of this into vertex_ai.py too.
vertex_ai.py (new helpers):
# vertex_ai.py
def normalize_vertex_ai_sdk_model_name(name: str) -> str:
# logic currently in _normalize_model_name for Vertex
name = name.strip()
if VERTEX_AI_MODEL_NAME_SEPARATOR in name:
name = name.rsplit(VERTEX_AI_MODEL_NAME_SEPARATOR, 1)[1]
elif name.startswith("models/"):
name = name.removeprefix("models/")
if not name.startswith("google/"):
name = f"google/{name}"
return name
def normalize_gemini_model_name(name: str) -> str:
return name.strip().replace("models/", "")GeminiSource:
def _get_request_model_name(self, model: str) -> str:
if self._is_vertex_ai_config():
return to_vertex_ai_genai_model_name(model)
return model
def _normalize_model_name(self, name: str) -> str:
if self._is_vertex_ai_config():
return normalize_vertex_ai_sdk_model_name(name)
return normalize_gemini_model_name(name)For listing models, consider moving the Vertex-specific get_models logic into one helper (e.g., fetch_vertex_ai_models(provider_config, http_client)) that encapsulates the token refresh, URL building, and header construction. GeminiSource.get_models then just branches and forwards:
async def get_models(self):
if self._is_vertex_ai_config():
return await fetch_vertex_ai_models(self.provider_config, self._http_client)
try:
models = await self.client.models.list()
model_ids = [
self._normalize_model_name(m.name)
for m in models
if m.supported_actions
and "generateContent" in m.supported_actions
and m.name
]
return normalize_and_dedupe_strings(model_ids)
except APIError as e:
raise Exception(f"获取模型列表失败: {e.message}")This way, GeminiSource remains responsible for high-level decisions (Vertex vs non-Vertex, streaming vs non-streaming) and delegates endpoint/auth/model-name details to vertex_ai.py, reducing the cognitive load and duplication without altering functionality.
| const basicSourceConfig = computed(() => { | ||
| if (!editableProviderSource.value) return null | ||
|
|
||
| const fields = ['id', 'key', 'api_base'] |
There was a problem hiding this comment.
issue (complexity): Consider centralizing provider-specific field configuration and defaulting logic into dedicated helpers so the composable stays provider-agnostic and easier to extend.
You can keep the new Vertex behavior but reduce complexity by centralizing the provider‑specific rules into a single configuration object instead of scattering provider === 'google-vertex-ai' checks.
1. Centralize basic/advanced field config per provider
Right now basicSourceConfig, advancedSourceConfig, vertexHiddenFields, vertexAdvancedSourceFields, and the Vertex‑specific branching are separate. You can keep behavior identical by introducing a config map:
const PROVIDER_FIELD_CONFIG = {
default: {
basic: ['id', 'key', 'api_base'],
hidden: new Set(['id', 'key', 'api_base', 'enable', 'type', 'provider_type', 'provider']),
advancedOrder: null as string[] | null
},
'google-vertex-ai': {
basic: ['id', 'api_base'],
hidden: new Set([
'id',
'key',
'api_base',
'enable',
'type',
'provider_type',
'provider',
'vertex_ai_project_id',
'vertex_ai_credentials_path'
]),
advancedOrder: [
'vertex_ai_auth_type',
'vertex_ai_api_key',
'vertex_ai_credentials_json',
'vertex_ai_location',
'timeout',
'proxy',
'gm_resp_image_modal',
'gm_native_search',
'gm_native_coderunner',
'gm_url_context',
'gm_safety_settings',
'gm_thinking_config'
]
}
} as const
function getFieldConfig(provider?: string) {
return PROVIDER_FIELD_CONFIG[provider as keyof typeof PROVIDER_FIELD_CONFIG] ?? PROVIDER_FIELD_CONFIG.default
}Then basicSourceConfig and advancedSourceConfig become simpler and provider‑agnostic at the call sites:
const basicSourceConfig = computed(() => {
if (!editableProviderSource.value) return null
const { basic } = getFieldConfig(editableProviderSource.value.provider)
const basicObj: Record<string, any> = {}
basic.forEach((field) => {
Object.defineProperty(basicObj, field, {
get() {
return editableProviderSource.value![field]
},
set(val) {
editableProviderSource.value![field] = val
},
enumerable: true
})
})
return basicObj
})
const advancedSourceConfig = computed(() => {
if (!editableProviderSource.value) return null
const cfg = getFieldConfig(editableProviderSource.value.provider)
const excluded = new Set(cfg.hidden)
const advanced: Record<string, any> = {}
const sourceKeys = Object.keys(editableProviderSource.value)
const keys = cfg.advancedOrder
? [
...cfg.advancedOrder.filter((field) => sourceKeys.includes(field)),
...sourceKeys.filter((field) => !cfg.advancedOrder!.includes(field))
]
: sourceKeys
for (const key of keys) {
Object.defineProperty(advanced, key, {
get() {
return editableProviderSource.value![key]
},
set(val) {
editableProviderSource.value![key] = val
},
enumerable: !excluded.has(key)
})
}
return advanced
})This removes scattered Vertex‑specific sets/arrays and a nested conditional in advancedSourceConfig, while preserving:
- Vertex’s different basic fields (
['id', 'api_base']) - Vertex’s hidden fields
- Vertex’s advanced field ordering
Adding new providers or tweaking Vertex just means updating PROVIDER_FIELD_CONFIG in one place.
2. Isolate Vertex default normalization
ensureProviderSourceDefaults is starting to accumulate provider logic. You can keep the new Vertex defaults but move them into a dedicated helper to decouple the generic function from the provider specifics:
function applyVertexDefaults(source: any) {
if (source.provider !== 'google-vertex-ai') return
if (!source.vertex_ai_auth_type || source.vertex_ai_auth_type === 'service_account') {
source.vertex_ai_auth_type = 'json'
}
if (source.vertex_ai_api_key === undefined) {
source.vertex_ai_api_key =
source.vertex_ai_auth_type === 'api_key' ? (source.key || []) : []
}
if (source.vertex_ai_credentials_json === undefined) {
source.vertex_ai_credentials_json = ''
}
if (!source.vertex_ai_location) {
source.vertex_ai_location = 'global'
}
}
function ensureProviderSourceDefaults(source: any) {
if (!source || typeof source !== 'object') {
return source
}
if (source.provider === 'ollama' && source.ollama_disable_thinking === undefined) {
source.ollama_disable_thinking = false
}
applyVertexDefaults(source)
return source
}This keeps the composable’s main flow cleaner and makes it obvious where provider‑specific normalization lives, without reverting any functionality.
There was a problem hiding this comment.
Code Review
This pull request introduces support for Google Vertex AI as a provider, integrating it with the native Gemini API. It adds configuration templates, localization files, frontend dashboard updates, and backend normalization logic to handle both API key and service account JSON authentication methods. Additionally, comprehensive unit tests are introduced to verify the integration. The review feedback highlights a critical issue in gemini_source.py where a synchronous blocking call to credentials.refresh is made within an asynchronous method, potentially blocking the main event loop. It is recommended to run this call in a separate thread using asyncio.to_thread.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if not getattr(credentials, "valid", False) or not getattr( | ||
| credentials, "token", None | ||
| ): | ||
| credentials.refresh(make_vertex_ai_refresh_request(self.provider_config)) |
There was a problem hiding this comment.
在 _get_vertex_ai_models 方法中,credentials.refresh(...) 是一个同步的阻塞网络请求。在单线程的 asyncio 事件循环中,直接调用它会阻塞整个事件循环,导致机器人的其他协程和任务(例如其他用户的消息处理)全部暂停。
建议使用 asyncio.to_thread 将该同步阻塞调用放到单独的线程中执行,以保持事件循环的流畅性。
| if not getattr(credentials, "valid", False) or not getattr( | |
| credentials, "token", None | |
| ): | |
| credentials.refresh(make_vertex_ai_refresh_request(self.provider_config)) | |
| if not getattr(credentials, "valid", False) or not getattr( | |
| credentials, "token", None | |
| ): | |
| await asyncio.to_thread( | |
| credentials.refresh, | |
| make_vertex_ai_refresh_request(self.provider_config), | |
| ) |
- Run the Vertex AI OAuth token refresh off the event loop (asyncio.to_thread) in _get_vertex_ai_models so model discovery does not block the async runtime on slow networks. - Log a warning when a proxy is configured but the 'requests' package is unavailable for token refresh, instead of silently ignoring the proxy. - Document that a pasted service-account JSON takes precedence over api_key auth in _get_genai_client_kwargs. - Add a test covering the credential-refresh path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
感谢 @sourcery-ai 和 @gemini-code-assist 的 review 🙏 已在 edc58c9 处理: 已修复 / Fixed
暂缓 / Deferred(建议后续单独 PR)
|
|
感谢你的详细反馈和快速响应。你所做的改进(特别是将 关于你提到的重构建议:我完全同意你的看法。为了保持 PR 的聚焦度并降低回归风险,将这些非行为变更的架构优化(如进一步下沉逻辑到 感谢你为 AstrBot 贡献这个高质量的 Vertex AI 集成! |
通过 Vertex AI 原生 Gemini API 接入 Google 模型,作为 AstrBot 的一个独立模型提供商预设(
Google Vertex AI)。复用现有的googlegenai_chat_completion适配器,而不是走 OpenAI-compatible 临时路径。Closes #7781
Closes #7653
Modifications / 改动点
新增
Google Vertex AIprovider source 预设(provider: google-vertex-ai,type: googlegenai_chat_completion):基础配置只暴露id/api_base,高级配置复用 Gemini 既有能力(安全过滤、思考配置、原生搜索 / 代码执行 / URL 上下文、图片模态、timeout、proxy)。两种鉴权方式:
project_id缺省时自动从 JSON 中读取。新增
astrbot/core/provider/sources/vertex_ai.py:配置归一化、OAuth token 刷新(支持 proxy)、模型 ID 互转(google/*↔publishers/google/models/*)、模型列表拉取等共享 helper。gemini_source.py复用同一套ProviderGoogleGenAI,Vertex 路径以vertexai=True初始化 google-genai SDK;Gemini 的高级选项对 Vertex 同样生效(无额外屏蔽分支)。Dashboard:Vertex AI 专用配置字段、i18n 文案(zh-CN / en-US / ru-RU)以及供应商 logo。
新增依赖
google-auth[requests]>=2.41.1(已写入requirements.txt与pyproject.toml)。This is NOT a breaking change. / 这不是一个破坏性变更。
Screenshots or Test Results / 运行截图或测试结果
新增单元测试
tests/test_vertex_ai.py,覆盖配置归一化、API Key / 服务账号 JSON 两条路径、原生 Vertex 端点、模型列表拉取、模型 ID 转换以及配置 metadata 与中文文案:ruff check与ruff format --check均通过。Checklist / 检查清单
google-auth[requests]已加入requirements.txt和pyproject.toml。🤖 Generated with Claude Code
Summary by Sourcery
Add a dedicated Google Vertex AI provider using the native Gemini API and integrate it with existing Gemini provider infrastructure and dashboard configuration.
New Features:
Enhancements:
Build:
Tests: