Skip to content

Commit d079769

Browse files
authored
Merge pull request #139 from albertsikkema-elix/feat/native-google-gemini-api-vertex-ai
feat: native Google provider (Gemini API and Vertex AI)
2 parents 5b95c84 + 425f5d0 commit d079769

20 files changed

Lines changed: 1005 additions & 49 deletions

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ uses [Semantic Versioning](https://semver.org/).
88

99
### Added
1010

11+
- **Google provider.** `google` (Gemini API key) and `google-vertex` (Vertex AI
12+
with Google credentials: ADC or Workload Identity Federation, plus
13+
`--google-project` and `--google-location`) run on Gemini without a base
14+
URL. Agent loops use pydantic-ai's native `GoogleModel`, so Gemini 3 thought
15+
signatures survive tool-call turns, and the cost summary prices Gemini runs
16+
from genai-prices ([#20](https://github.com/FSoft-AI4Code/CodeWiki/issues/20)).
1117
- **Rust analyzer.** Structs, unions, enums, traits, free functions, and
1218
impl/trait methods become components, with trait-impl, field-type,
1319
struct-literal, and call edges. `#[cfg(test)]` modules are skipped, and

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ codewiki config set --provider claude-code \
7373
--main-model claude-sonnet-4-6 --cluster-model claude-sonnet-4-6
7474
```
7575

76-
Anthropic, Azure OpenAI, AWS Bedrock, Atlas Cloud, and Codex are also
77-
supported. See [Providers and models](./guides/providers.md).
76+
Anthropic, Azure OpenAI, AWS Bedrock, Google (Gemini API and Vertex AI),
77+
Atlas Cloud, and Codex are also supported. See [Providers and models](./guides/providers.md).
7878

7979
**3. Generate**
8080

‎codewiki/cli/adapters/doc_generator.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ def generate(self) -> DocumentationJob:
151151
fallback_model=self.config.get("fallback_model"),
152152
provider=self.config.get("provider", "openai-compatible"),
153153
aws_region=self.config.get("aws_region", "us-east-1"),
154+
google_project=self.config.get("google_project", ""),
155+
google_location=self.config.get("google_location", ""),
154156
max_tokens=self.config.get("max_tokens", 32768),
155157
max_token_per_module=self.config.get("max_token_per_module", 36369),
156158
max_token_per_leaf_module=self.config.get("max_token_per_leaf_module", 16000),

‎codewiki/cli/commands/config.py‎

Lines changed: 113 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,16 @@ def config_group():
6969
"azure-openai",
7070
"claude-code",
7171
"codex",
72+
"google",
73+
"google-vertex",
7274
],
7375
case_sensitive=False,
7476
),
7577
help=(
7678
"LLM provider type (default: openai-compatible). "
7779
"Use 'atlas-cloud' for Atlas Cloud (base URL auto-set; reads ATLASCLOUD_API_KEY). "
80+
"Use 'google' for the Gemini API (reads GEMINI_API_KEY / GOOGLE_API_KEY) or "
81+
"'google-vertex' for Vertex AI with Google credentials (no API key). "
7882
"Use 'claude-code' or 'codex' to run on a CLI subscription instead of an API key."
7983
),
8084
)
@@ -83,6 +87,17 @@ def config_group():
8387
"--api-version", type=str, help="Azure OpenAI API version (default: 2024-12-01-preview)"
8488
)
8589
@click.option("--azure-deployment", type=str, help="Azure OpenAI deployment name")
90+
@click.option(
91+
"--google-project",
92+
type=str,
93+
help="Google Cloud project for google-vertex (default: GOOGLE_CLOUD_PROJECT)",
94+
)
95+
@click.option(
96+
"--google-location",
97+
type=str,
98+
help="Vertex AI location for google-vertex, e.g. global, eu, us "
99+
"(default: GOOGLE_CLOUD_LOCATION, else global)",
100+
)
86101
@click.option(
87102
"--use-gitignore/--no-gitignore",
88103
default=None,
@@ -110,6 +125,8 @@ def config_set(
110125
aws_region: Optional[str] = None,
111126
api_version: Optional[str] = None,
112127
azure_deployment: Optional[str] = None,
128+
google_project: Optional[str] = None,
129+
google_location: Optional[str] = None,
113130
use_gitignore: Optional[bool] = None,
114131
prompt_caching: Optional[bool] = None,
115132
):
@@ -142,6 +159,16 @@ def config_set(
142159
# Subscription mode (Codex)
143160
$ codewiki config set --provider codex --main-model gpt-5.2-codex
144161
162+
\b
163+
# Vertex AI (Google credentials, no API key or base URL)
164+
$ codewiki config set --provider google-vertex --google-project my-proj \\
165+
--google-location eu --main-model gemini-3.8-flash --cluster-model gemini-3.8-flash
166+
167+
\b
168+
# Gemini API (key read from GEMINI_API_KEY if --api-key is omitted)
169+
$ codewiki config set --provider google --main-model gemini-3.8-flash \\
170+
--cluster-model gemini-3.8-flash
171+
145172
\b
146173
# Update only API key
147174
$ codewiki config set --api-key sk-new-key
@@ -181,6 +208,8 @@ def config_set(
181208
aws_region,
182209
api_version,
183210
azure_deployment,
211+
google_project,
212+
google_location,
184213
use_gitignore is not None,
185214
prompt_caching is not None,
186215
]
@@ -199,6 +228,20 @@ def config_set(
199228
if not api_key:
200229
api_key = os.getenv("ATLASCLOUD_API_KEY")
201230

231+
manager = ConfigManager()
232+
manager.load() # Load existing config if present
233+
234+
if provider and provider.lower() == "google" and not api_key:
235+
api_key = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
236+
previous = manager.get_config()
237+
if not api_key and not (previous and previous.provider == "google"):
238+
# The keyring has one key slot: reusing it would send another
239+
# provider's key to Google.
240+
raise ConfigurationError(
241+
"--provider google needs a Gemini API key: pass --api-key or set "
242+
"GEMINI_API_KEY (or GOOGLE_API_KEY)."
243+
)
244+
202245
# Validate inputs before saving
203246
validated_data = {}
204247

@@ -265,9 +308,11 @@ def config_set(
265308
if prompt_caching is not None:
266309
validated_data["prompt_caching"] = prompt_caching
267310

268-
# Create config manager and save
269-
manager = ConfigManager()
270-
manager.load() # Load existing config if present
311+
if google_project is not None:
312+
validated_data["google_project"] = google_project
313+
314+
if google_location is not None:
315+
validated_data["google_location"] = google_location
271316

272317
manager.save(
273318
api_key=validated_data.get("api_key"),
@@ -285,6 +330,8 @@ def config_set(
285330
aws_region=validated_data.get("aws_region"),
286331
api_version=validated_data.get("api_version"),
287332
azure_deployment=validated_data.get("azure_deployment"),
333+
google_project=validated_data.get("google_project"),
334+
google_location=validated_data.get("google_location"),
288335
use_gitignore=validated_data.get("use_gitignore"),
289336
prompt_caching=validated_data.get("prompt_caching"),
290337
)
@@ -352,6 +399,12 @@ def config_set(
352399
if azure_deployment:
353400
click.secho(f"✓ Azure Deployment: {azure_deployment}", fg="green")
354401

402+
if google_project:
403+
click.secho(f"✓ Google project: {google_project}", fg="green")
404+
405+
if google_location:
406+
click.secho(f"✓ Google location: {google_location}", fg="green")
407+
355408
if use_gitignore is not None:
356409
click.secho(f"✓ Use gitignore: {use_gitignore}", fg="green")
357410

@@ -405,6 +458,9 @@ def config_show(output_json: bool):
405458
"api_key": mask_api_key(api_key) if api_key else "Not set",
406459
"api_key_storage": "keychain" if manager.keyring_available else "encrypted_file",
407460
"base_url": config.base_url if config else "",
461+
"provider": config.provider if config else "",
462+
"google_project": config.google_project if config else "",
463+
"google_location": config.google_location if config else "",
408464
"main_model": config.main_model if config else "",
409465
"cluster_model": config.cluster_model if config else "",
410466
"fallback_model": config.fallback_model if config else "glm-4p5",
@@ -430,12 +486,14 @@ def config_show(output_json: bool):
430486
click.echo("━" * 40)
431487
click.echo()
432488

433-
from codewiki.src.be.backend import is_caw_provider
489+
from codewiki.src.be.backend import is_caw_provider, needs_base_url
434490

435491
caw_mode = bool(config) and is_caw_provider(config.provider)
436492

437493
click.secho("Credentials", fg="cyan", bold=True)
438-
if caw_mode:
494+
if config and config.provider == "google-vertex":
495+
click.secho(" Google ADC / Workload Identity (no API key needed)", fg="cyan")
496+
elif caw_mode:
439497
cli_name = "claude" if config.provider == "claude-code" else "codex"
440498
click.secho(
441499
f" Subscription mode: authenticate via '{cli_name} login' (no API key needed)",
@@ -453,10 +511,18 @@ def config_show(output_json: bool):
453511
click.echo(f" Provider: {config.provider}")
454512
click.echo(f" Main Model: {config.main_model or 'Not set'}")
455513
if not caw_mode:
456-
click.echo(f" Base URL: {config.base_url or 'Not set'}")
514+
if needs_base_url(config.provider):
515+
click.echo(f" Base URL: {config.base_url or 'Not set'}")
457516
click.echo(f" Cluster Model: {config.cluster_model or 'Not set'}")
458517
click.echo(f" Fallback Model: {config.fallback_model or 'Not set'}")
459-
if config.provider == "bedrock":
518+
if config.provider == "google-vertex":
519+
project = config.google_project or "from GOOGLE_CLOUD_PROJECT"
520+
location = config.google_location or (
521+
"from GOOGLE_CLOUD_LOCATION, else global"
522+
)
523+
click.echo(f" Google Project: {project}")
524+
click.echo(f" Google Location: {location}")
525+
elif config.provider == "bedrock":
460526
click.echo(f" AWS Region: {config.aws_region}")
461527
elif config.provider == "azure-openai":
462528
click.echo(f" API Version: {config.api_version}")
@@ -565,20 +631,29 @@ def config_validate(quick: bool, verbose: bool):
565631

566632
# Load config early so we know the provider for the rest of the checks.
567633
config = manager.get_config()
568-
from codewiki.src.be.backend import is_caw_provider
634+
from codewiki.src.be.backend import (
635+
is_caw_provider,
636+
is_google_provider,
637+
needs_api_key,
638+
needs_base_url,
639+
)
569640

570641
caw_mode = bool(config) and is_caw_provider(config.provider)
642+
key_needed = bool(config) and needs_api_key(config.provider)
643+
url_needed = bool(config) and needs_base_url(config.provider)
644+
api_key = None
571645

572-
# Step 2: Check API key (skipped for subscription providers)
646+
# Step 2: Check API key (skipped for subscription providers and Vertex AI)
573647
if verbose:
574648
click.echo()
575649
click.echo("[2/5] Checking API key...")
576650

577-
if caw_mode:
651+
if not key_needed:
652+
reason = "subscription mode" if caw_mode else "Google credentials"
578653
if verbose:
579-
click.secho(" ✓ API key not required (subscription mode)", fg="green")
654+
click.secho(f" ✓ API key not required ({reason})", fg="green")
580655
else:
581-
click.secho("✓ API key not required (subscription mode)", fg="green")
656+
click.secho(f"✓ API key not required ({reason})", fg="green")
582657
else:
583658
if verbose:
584659
storage = "system keychain" if manager.keyring_available else "encrypted file"
@@ -597,16 +672,17 @@ def config_validate(quick: bool, verbose: bool):
597672
else:
598673
click.secho("✓ API key present (stored in keychain)", fg="green")
599674

600-
# Step 3: Check base URL (skipped for subscription providers)
675+
# Step 3: Check base URL (skipped for subscription and Google providers)
601676
if verbose:
602677
click.echo()
603678
click.echo("[3/5] Checking base URL...")
604679

605-
if caw_mode:
680+
if not url_needed:
681+
reason = "subscription mode" if caw_mode else "Google endpoint"
606682
if verbose:
607-
click.secho(" ✓ Base URL not required (subscription mode)", fg="green")
683+
click.secho(f" ✓ Base URL not required ({reason})", fg="green")
608684
else:
609-
click.secho("✓ Base URL not required (subscription mode)", fg="green")
685+
click.secho(f"✓ Base URL not required ({reason})", fg="green")
610686
else:
611687
if verbose:
612688
click.echo(f" URL: {config.base_url}")
@@ -657,7 +733,7 @@ def config_validate(quick: bool, verbose: bool):
657733
# Warn about non-top-tier cluster model
658734
if not is_top_tier_model(config.cluster_model):
659735
click.secho(
660-
"⚠️ Cluster model is not top-tier. Consider using claude-sonnet-4 or gpt-4.",
736+
"⚠️ Cluster model is not top-tier. Consider a Claude Sonnet/Opus, GPT-5 or Gemini 3 model.",
661737
fg="yellow",
662738
)
663739

@@ -690,6 +766,26 @@ def config_validate(quick: bool, verbose: bool):
690766
f"✓ {cli_name} CLI available (run '{cli_name} login' if not yet authenticated)",
691767
fg="green",
692768
)
769+
elif is_google_provider(config.provider) and not quick:
770+
if verbose:
771+
click.echo()
772+
click.echo("[5/5] Testing Google API access...")
773+
try:
774+
from codewiki.src.be import google_provider
775+
776+
google_provider.check_access(
777+
config.provider,
778+
api_key if key_needed else None,
779+
config.main_model,
780+
config.google_project,
781+
config.google_location,
782+
)
783+
click.secho("✓ API connectivity test successful", fg="green")
784+
except Exception as e:
785+
click.secho("✗ API connectivity test failed", fg="red")
786+
if verbose:
787+
click.echo(f" Error: {e}")
788+
sys.exit(EXIT_CONFIG_ERROR)
693789
elif not quick:
694790
if verbose:
695791
click.echo()

‎codewiki/cli/commands/generate.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,8 @@ def generate_command(
793793
"api_key": api_key,
794794
"provider": getattr(config, "provider", "openai-compatible"),
795795
"aws_region": getattr(config, "aws_region", "us-east-1"),
796+
"google_project": getattr(config, "google_project", ""),
797+
"google_location": getattr(config, "google_location", ""),
796798
"agent_instructions": agent_instructions_dict,
797799
# Max token settings (runtime overrides take precedence)
798800
"max_tokens": max_tokens if max_tokens is not None else config.max_tokens,

0 commit comments

Comments
 (0)