-
Notifications
You must be signed in to change notification settings - Fork 170
Add base_url support for AI providers (#1) #85
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -228,15 +228,37 @@ def resolve_provider( | |||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
| from repowise.core.providers import get_provider | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| cfg: dict[str, Any] = {} | ||||||||||||||||||||||||||||||
| if repo_path is not None: | ||||||||||||||||||||||||||||||
| cfg = load_config(repo_path) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if provider_name is None: | ||||||||||||||||||||||||||||||
| provider_name = os.environ.get("REPOWISE_PROVIDER") | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if provider_name is None and repo_path is not None: | ||||||||||||||||||||||||||||||
| cfg = load_config(repo_path) | ||||||||||||||||||||||||||||||
| if cfg.get("provider"): | ||||||||||||||||||||||||||||||
| provider_name = cfg["provider"] | ||||||||||||||||||||||||||||||
| if model is None and cfg.get("model"): | ||||||||||||||||||||||||||||||
| model = cfg["model"] | ||||||||||||||||||||||||||||||
| if provider_name is None and cfg.get("provider"): | ||||||||||||||||||||||||||||||
| provider_name = cfg["provider"] | ||||||||||||||||||||||||||||||
| if model is None and cfg.get("model"): | ||||||||||||||||||||||||||||||
| model = cfg["model"] | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| def _resolve_base_url(name: str) -> str | None: | ||||||||||||||||||||||||||||||
| """Return base_url from env or repo config for the provider.""" | ||||||||||||||||||||||||||||||
| env_vars = { | ||||||||||||||||||||||||||||||
| "anthropic": ["ANTHROPIC_BASE_URL"], | ||||||||||||||||||||||||||||||
| "openai": ["OPENAI_BASE_URL"], | ||||||||||||||||||||||||||||||
| "gemini": ["GEMINI_BASE_URL"], | ||||||||||||||||||||||||||||||
| "ollama": ["OLLAMA_BASE_URL"], | ||||||||||||||||||||||||||||||
| "litellm": ["LITELLM_BASE_URL", "LITELLM_API_BASE"], | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| for var in env_vars.get(name, []): | ||||||||||||||||||||||||||||||
|
Comment on lines
+245
to
+252
|
||||||||||||||||||||||||||||||
| env_vars = { | |
| "anthropic": ["ANTHROPIC_BASE_URL"], | |
| "openai": ["OPENAI_BASE_URL"], | |
| "gemini": ["GEMINI_BASE_URL"], | |
| "ollama": ["OLLAMA_BASE_URL"], | |
| "litellm": ["LITELLM_BASE_URL", "LITELLM_API_BASE"], | |
| } | |
| for var in env_vars.get(name, []): | |
| normalized_name = name.upper().replace("-", "_") | |
| env_vars = [f"{normalized_name}_BASE_URL"] | |
| if normalized_name == "LITELLM": | |
| env_vars.append("LITELLM_API_BASE") | |
| for var in env_vars: |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -55,6 +55,7 @@ class GeminiProvider(BaseProvider): | |||||||||||||||||||||||||
| Args: | ||||||||||||||||||||||||||
| model: Gemini model name. Defaults to gemini-3.1-flash-lite-preview. | ||||||||||||||||||||||||||
| api_key: Google API key. Falls back to GEMINI_API_KEY or GOOGLE_API_KEY env var. | ||||||||||||||||||||||||||
| base_url: Optional custom base URL (e.g., for proxy/self-hosted endpoints). | ||||||||||||||||||||||||||
| rate_limiter: Optional RateLimiter instance. | ||||||||||||||||||||||||||
| cost_tracker: Optional CostTracker for recording token usage and cost. | ||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||
|
|
@@ -63,6 +64,7 @@ def __init__( | |||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||
| model: str = "gemini-3.1-flash-lite-preview", | ||||||||||||||||||||||||||
| api_key: str | None = None, | ||||||||||||||||||||||||||
| base_url: str | None = None, | ||||||||||||||||||||||||||
| rate_limiter: RateLimiter | None = None, | ||||||||||||||||||||||||||
| cost_tracker: "CostTracker | None" = None, | ||||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||||
|
|
@@ -77,6 +79,7 @@ def __init__( | |||||||||||||||||||||||||
| "gemini", | ||||||||||||||||||||||||||
| "No API key found. Pass api_key= or set GEMINI_API_KEY / GOOGLE_API_KEY env var.", | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
| self._base_url = base_url or os.environ.get("GEMINI_BASE_URL") | ||||||||||||||||||||||||||
| self._rate_limiter = rate_limiter | ||||||||||||||||||||||||||
| self._cost_tracker = cost_tracker | ||||||||||||||||||||||||||
| self._client: object | None = None # cached; created once on first call | ||||||||||||||||||||||||||
|
|
@@ -138,13 +141,36 @@ async def _generate_with_retry( | |||||||||||||||||||||||||
| # Capture self attrs for thread safety (avoids closing over self) | ||||||||||||||||||||||||||
| model = self._model | ||||||||||||||||||||||||||
| api_key = self._api_key | ||||||||||||||||||||||||||
| base_url = self._base_url | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def _call_sync() -> GeneratedResponse: | ||||||||||||||||||||||||||
| from google import genai # type: ignore[import-untyped] | ||||||||||||||||||||||||||
| from google.genai import types as genai_types # type: ignore[import-untyped] | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if self._client is None: | ||||||||||||||||||||||||||
| self._client = genai.Client(api_key=api_key) | ||||||||||||||||||||||||||
| client_kwargs: dict[str, Any] = {"api_key": api_key} | ||||||||||||||||||||||||||
| http_opts = None | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if base_url: | ||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||
| http_opts = genai_types.HttpOptions(base_url=base_url) | ||||||||||||||||||||||||||
| except TypeError: | ||||||||||||||||||||||||||
| log.warning( | ||||||||||||||||||||||||||
| "gemini.http_options.base_url_unsupported", | ||||||||||||||||||||||||||
| base_url=base_url, | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if http_opts is not None: | ||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||
| self._client = genai.Client(**client_kwargs, http_options=http_opts) | ||||||||||||||||||||||||||
| except TypeError: | ||||||||||||||||||||||||||
| log.warning( | ||||||||||||||||||||||||||
| "gemini.client.http_options_unsupported", | ||||||||||||||||||||||||||
| base_url=base_url, | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
| self._client = genai.Client(**client_kwargs) | ||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||
| self._client = genai.Client(**client_kwargs) | ||||||||||||||||||||||||||
| client = self._client | ||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||
| response = client.models.generate_content( | ||||||||||||||||||||||||||
|
|
@@ -235,13 +261,16 @@ async def stream_chat( | |||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| model_name = self._model | ||||||||||||||||||||||||||
| api_key = self._api_key | ||||||||||||||||||||||||||
| base_url = self._base_url | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def _call_sync(contents, config): | ||||||||||||||||||||||||||
| """Single Gemini generate_content call in thread.""" | ||||||||||||||||||||||||||
| from google import genai # type: ignore[import-untyped] | ||||||||||||||||||||||||||
| from google.genai import types as genai_types # type: ignore[import-untyped] | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if self._client is None: | ||||||||||||||||||||||||||
| self._client = genai.Client(api_key=api_key) | ||||||||||||||||||||||||||
| http_opts = genai_types.HttpOptions(base_url=base_url) if base_url else None | ||||||||||||||||||||||||||
| self._client = genai.Client(api_key=api_key, http_options=http_opts) | ||||||||||||||||||||||||||
|
Comment on lines
+272
to
+273
|
||||||||||||||||||||||||||
| http_opts = genai_types.HttpOptions(base_url=base_url) if base_url else None | |
| self._client = genai.Client(api_key=api_key, http_options=http_opts) | |
| http_opts = None | |
| if base_url: | |
| try: | |
| http_opts = genai_types.HttpOptions(base_url=base_url) | |
| except TypeError: | |
| http_opts = genai_types.HttpOptions() | |
| try: | |
| self._client = genai.Client(api_key=api_key, http_options=http_opts) | |
| except TypeError: | |
| self._client = genai.Client(api_key=api_key) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The env var table marks
OLLAMA_BASE_URLas required “If using Ollama”, but the description states there is a default (http://localhost:11434). If the app works with the default URL, this variable isn’t actually required and the “Required” column should likely beNo(or clarify that it’s only required when using a non-default host).