feat(gemini): normalize base URL to strip API version suffixes
Automatically remove trailing slashes and version paths (e.g., /v1beta) from GEMINI_BASE_URL Update GeminiAnalyzer to use the normalized URL and add type hints Add test coverage for Gemini client configuration
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -214,3 +214,5 @@ __marimo__/
|
|||||||
|
|
||||||
# Streamlit
|
# Streamlit
|
||||||
.streamlit/secrets.toml
|
.streamlit/secrets.toml
|
||||||
|
|
||||||
|
workspace
|
||||||
@@ -19,6 +19,7 @@ GEMINI_MODEL=gemini-2.5-flash
|
|||||||
```
|
```
|
||||||
|
|
||||||
`GEMINI_BASE_URL` 留空时走官方默认地址;如果你前面挂了代理或网关,可以填自定义地址。
|
`GEMINI_BASE_URL` 留空时走官方默认地址;如果你前面挂了代理或网关,可以填自定义地址。
|
||||||
|
这里填写网关根地址即可,例如 `https://your-gateway.example.com`,不要自己追加 `/v1beta`、`/v1alpha` 或 `/v1`。
|
||||||
|
|
||||||
## 使用
|
## 使用
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import re
|
||||||
from typing import Self
|
from typing import Self
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
@@ -30,3 +31,10 @@ class Settings:
|
|||||||
def require_api_key(self) -> None:
|
def require_api_key(self) -> None:
|
||||||
if not self.api_key:
|
if not self.api_key:
|
||||||
raise ValueError("Missing GEMINI_API_KEY (or GOOGLE_API_KEY) in the environment.")
|
raise ValueError("Missing GEMINI_API_KEY (or GOOGLE_API_KEY) in the environment.")
|
||||||
|
|
||||||
|
def normalized_base_url(self) -> str | None:
|
||||||
|
if not self.base_url:
|
||||||
|
return None
|
||||||
|
|
||||||
|
base_url = self.base_url.strip().rstrip("/")
|
||||||
|
return re.sub(r"/v\d+(?:alpha|beta)?$", "", base_url, flags=re.IGNORECASE)
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from .config import Settings
|
from .config import Settings
|
||||||
@@ -10,6 +11,8 @@ from .config import Settings
|
|||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class GeminiAnalyzer:
|
class GeminiAnalyzer:
|
||||||
settings: Settings
|
settings: Settings
|
||||||
|
_types: Any = field(init=False, repr=False)
|
||||||
|
_client: Any = field(init=False, repr=False)
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
self.settings.require_api_key()
|
self.settings.require_api_key()
|
||||||
@@ -18,8 +21,9 @@ class GeminiAnalyzer:
|
|||||||
from google.genai import types
|
from google.genai import types
|
||||||
|
|
||||||
http_options = types.HttpOptions(timeout=120_000)
|
http_options = types.HttpOptions(timeout=120_000)
|
||||||
if self.settings.base_url:
|
base_url = self.settings.normalized_base_url()
|
||||||
http_options = types.HttpOptions(base_url=self.settings.base_url, timeout=120_000)
|
if base_url:
|
||||||
|
http_options = types.HttpOptions(base_url=base_url, timeout=120_000)
|
||||||
|
|
||||||
self._types = types
|
self._types = types
|
||||||
self._client = genai.Client(api_key=self.settings.api_key, http_options=http_options)
|
self._client = genai.Client(api_key=self.settings.api_key, http_options=http_options)
|
||||||
|
|||||||
86
tests/test_gemini.py
Normal file
86
tests/test_gemini.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from types import ModuleType, SimpleNamespace
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT / "src"))
|
||||||
|
|
||||||
|
fake_dotenv = ModuleType("dotenv")
|
||||||
|
fake_dotenv.load_dotenv = lambda *args, **kwargs: None
|
||||||
|
sys.modules.setdefault("dotenv", fake_dotenv)
|
||||||
|
|
||||||
|
from uipath_explainator.config import Settings
|
||||||
|
from uipath_explainator.gemini import GeminiAnalyzer
|
||||||
|
|
||||||
|
|
||||||
|
class FakeHttpOptions:
|
||||||
|
def __init__(self, **kwargs) -> None:
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
|
||||||
|
class FakeGenerateContentConfig:
|
||||||
|
def __init__(self, **kwargs) -> None:
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
|
||||||
|
class FakeClient:
|
||||||
|
def __init__(self, api_key: str, http_options: FakeHttpOptions) -> None:
|
||||||
|
self.api_key = api_key
|
||||||
|
self.http_options = http_options
|
||||||
|
self.models = SimpleNamespace()
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiAnalyzerTests(unittest.TestCase):
|
||||||
|
def test_init_with_slots_declares_runtime_fields(self) -> None:
|
||||||
|
fake_types = SimpleNamespace(
|
||||||
|
HttpOptions=FakeHttpOptions,
|
||||||
|
GenerateContentConfig=FakeGenerateContentConfig,
|
||||||
|
)
|
||||||
|
fake_genai = ModuleType("google.genai")
|
||||||
|
fake_genai.Client = FakeClient
|
||||||
|
fake_genai.types = fake_types
|
||||||
|
|
||||||
|
fake_google = ModuleType("google")
|
||||||
|
fake_google.genai = fake_genai
|
||||||
|
|
||||||
|
with patch.dict(sys.modules, {"google": fake_google, "google.genai": fake_genai}):
|
||||||
|
analyzer = GeminiAnalyzer(Settings(api_key="test-key", base_url=None, model="gemini-test"))
|
||||||
|
|
||||||
|
self.assertIs(analyzer._types, fake_types)
|
||||||
|
self.assertIsInstance(analyzer._client, FakeClient)
|
||||||
|
self.assertEqual(analyzer._client.api_key, "test-key")
|
||||||
|
self.assertEqual(analyzer._client.http_options.kwargs, {"timeout": 120_000})
|
||||||
|
|
||||||
|
def test_init_strips_version_suffix_from_custom_base_url(self) -> None:
|
||||||
|
fake_types = SimpleNamespace(
|
||||||
|
HttpOptions=FakeHttpOptions,
|
||||||
|
GenerateContentConfig=FakeGenerateContentConfig,
|
||||||
|
)
|
||||||
|
fake_genai = ModuleType("google.genai")
|
||||||
|
fake_genai.Client = FakeClient
|
||||||
|
fake_genai.types = fake_types
|
||||||
|
|
||||||
|
fake_google = ModuleType("google")
|
||||||
|
fake_google.genai = fake_genai
|
||||||
|
|
||||||
|
with patch.dict(sys.modules, {"google": fake_google, "google.genai": fake_genai}):
|
||||||
|
analyzer = GeminiAnalyzer(
|
||||||
|
Settings(
|
||||||
|
api_key="test-key",
|
||||||
|
base_url="https://newapi.tootaio.com/v1beta/",
|
||||||
|
model="gemini-test",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
analyzer._client.http_options.kwargs,
|
||||||
|
{"base_url": "https://newapi.tootaio.com", "timeout": 120_000},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user