foundry-mcp 0.3.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. foundry_mcp/__init__.py +7 -0
  2. foundry_mcp/cli/__init__.py +80 -0
  3. foundry_mcp/cli/__main__.py +9 -0
  4. foundry_mcp/cli/agent.py +96 -0
  5. foundry_mcp/cli/commands/__init__.py +37 -0
  6. foundry_mcp/cli/commands/cache.py +137 -0
  7. foundry_mcp/cli/commands/dashboard.py +148 -0
  8. foundry_mcp/cli/commands/dev.py +446 -0
  9. foundry_mcp/cli/commands/journal.py +377 -0
  10. foundry_mcp/cli/commands/lifecycle.py +274 -0
  11. foundry_mcp/cli/commands/modify.py +824 -0
  12. foundry_mcp/cli/commands/plan.py +633 -0
  13. foundry_mcp/cli/commands/pr.py +393 -0
  14. foundry_mcp/cli/commands/review.py +652 -0
  15. foundry_mcp/cli/commands/session.py +479 -0
  16. foundry_mcp/cli/commands/specs.py +856 -0
  17. foundry_mcp/cli/commands/tasks.py +807 -0
  18. foundry_mcp/cli/commands/testing.py +676 -0
  19. foundry_mcp/cli/commands/validate.py +982 -0
  20. foundry_mcp/cli/config.py +98 -0
  21. foundry_mcp/cli/context.py +259 -0
  22. foundry_mcp/cli/flags.py +266 -0
  23. foundry_mcp/cli/logging.py +212 -0
  24. foundry_mcp/cli/main.py +44 -0
  25. foundry_mcp/cli/output.py +122 -0
  26. foundry_mcp/cli/registry.py +110 -0
  27. foundry_mcp/cli/resilience.py +178 -0
  28. foundry_mcp/cli/transcript.py +217 -0
  29. foundry_mcp/config.py +850 -0
  30. foundry_mcp/core/__init__.py +144 -0
  31. foundry_mcp/core/ai_consultation.py +1636 -0
  32. foundry_mcp/core/cache.py +195 -0
  33. foundry_mcp/core/capabilities.py +446 -0
  34. foundry_mcp/core/concurrency.py +898 -0
  35. foundry_mcp/core/context.py +540 -0
  36. foundry_mcp/core/discovery.py +1603 -0
  37. foundry_mcp/core/error_collection.py +728 -0
  38. foundry_mcp/core/error_store.py +592 -0
  39. foundry_mcp/core/feature_flags.py +592 -0
  40. foundry_mcp/core/health.py +749 -0
  41. foundry_mcp/core/journal.py +694 -0
  42. foundry_mcp/core/lifecycle.py +412 -0
  43. foundry_mcp/core/llm_config.py +1350 -0
  44. foundry_mcp/core/llm_patterns.py +510 -0
  45. foundry_mcp/core/llm_provider.py +1569 -0
  46. foundry_mcp/core/logging_config.py +374 -0
  47. foundry_mcp/core/metrics_persistence.py +584 -0
  48. foundry_mcp/core/metrics_registry.py +327 -0
  49. foundry_mcp/core/metrics_store.py +641 -0
  50. foundry_mcp/core/modifications.py +224 -0
  51. foundry_mcp/core/naming.py +123 -0
  52. foundry_mcp/core/observability.py +1216 -0
  53. foundry_mcp/core/otel.py +452 -0
  54. foundry_mcp/core/otel_stubs.py +264 -0
  55. foundry_mcp/core/pagination.py +255 -0
  56. foundry_mcp/core/progress.py +317 -0
  57. foundry_mcp/core/prometheus.py +577 -0
  58. foundry_mcp/core/prompts/__init__.py +464 -0
  59. foundry_mcp/core/prompts/fidelity_review.py +546 -0
  60. foundry_mcp/core/prompts/markdown_plan_review.py +511 -0
  61. foundry_mcp/core/prompts/plan_review.py +623 -0
  62. foundry_mcp/core/providers/__init__.py +225 -0
  63. foundry_mcp/core/providers/base.py +476 -0
  64. foundry_mcp/core/providers/claude.py +460 -0
  65. foundry_mcp/core/providers/codex.py +619 -0
  66. foundry_mcp/core/providers/cursor_agent.py +642 -0
  67. foundry_mcp/core/providers/detectors.py +488 -0
  68. foundry_mcp/core/providers/gemini.py +405 -0
  69. foundry_mcp/core/providers/opencode.py +616 -0
  70. foundry_mcp/core/providers/opencode_wrapper.js +302 -0
  71. foundry_mcp/core/providers/package-lock.json +24 -0
  72. foundry_mcp/core/providers/package.json +25 -0
  73. foundry_mcp/core/providers/registry.py +607 -0
  74. foundry_mcp/core/providers/test_provider.py +171 -0
  75. foundry_mcp/core/providers/validation.py +729 -0
  76. foundry_mcp/core/rate_limit.py +427 -0
  77. foundry_mcp/core/resilience.py +600 -0
  78. foundry_mcp/core/responses.py +934 -0
  79. foundry_mcp/core/review.py +366 -0
  80. foundry_mcp/core/security.py +438 -0
  81. foundry_mcp/core/spec.py +1650 -0
  82. foundry_mcp/core/task.py +1289 -0
  83. foundry_mcp/core/testing.py +450 -0
  84. foundry_mcp/core/validation.py +2081 -0
  85. foundry_mcp/dashboard/__init__.py +32 -0
  86. foundry_mcp/dashboard/app.py +119 -0
  87. foundry_mcp/dashboard/components/__init__.py +17 -0
  88. foundry_mcp/dashboard/components/cards.py +88 -0
  89. foundry_mcp/dashboard/components/charts.py +234 -0
  90. foundry_mcp/dashboard/components/filters.py +136 -0
  91. foundry_mcp/dashboard/components/tables.py +195 -0
  92. foundry_mcp/dashboard/data/__init__.py +11 -0
  93. foundry_mcp/dashboard/data/stores.py +433 -0
  94. foundry_mcp/dashboard/launcher.py +289 -0
  95. foundry_mcp/dashboard/views/__init__.py +12 -0
  96. foundry_mcp/dashboard/views/errors.py +217 -0
  97. foundry_mcp/dashboard/views/metrics.py +174 -0
  98. foundry_mcp/dashboard/views/overview.py +160 -0
  99. foundry_mcp/dashboard/views/providers.py +83 -0
  100. foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
  101. foundry_mcp/dashboard/views/tool_usage.py +139 -0
  102. foundry_mcp/prompts/__init__.py +9 -0
  103. foundry_mcp/prompts/workflows.py +525 -0
  104. foundry_mcp/resources/__init__.py +9 -0
  105. foundry_mcp/resources/specs.py +591 -0
  106. foundry_mcp/schemas/__init__.py +38 -0
  107. foundry_mcp/schemas/sdd-spec-schema.json +386 -0
  108. foundry_mcp/server.py +164 -0
  109. foundry_mcp/tools/__init__.py +10 -0
  110. foundry_mcp/tools/unified/__init__.py +71 -0
  111. foundry_mcp/tools/unified/authoring.py +1487 -0
  112. foundry_mcp/tools/unified/context_helpers.py +98 -0
  113. foundry_mcp/tools/unified/documentation_helpers.py +198 -0
  114. foundry_mcp/tools/unified/environment.py +939 -0
  115. foundry_mcp/tools/unified/error.py +462 -0
  116. foundry_mcp/tools/unified/health.py +225 -0
  117. foundry_mcp/tools/unified/journal.py +841 -0
  118. foundry_mcp/tools/unified/lifecycle.py +632 -0
  119. foundry_mcp/tools/unified/metrics.py +777 -0
  120. foundry_mcp/tools/unified/plan.py +745 -0
  121. foundry_mcp/tools/unified/pr.py +294 -0
  122. foundry_mcp/tools/unified/provider.py +629 -0
  123. foundry_mcp/tools/unified/review.py +685 -0
  124. foundry_mcp/tools/unified/review_helpers.py +299 -0
  125. foundry_mcp/tools/unified/router.py +102 -0
  126. foundry_mcp/tools/unified/server.py +580 -0
  127. foundry_mcp/tools/unified/spec.py +808 -0
  128. foundry_mcp/tools/unified/task.py +2202 -0
  129. foundry_mcp/tools/unified/test.py +370 -0
  130. foundry_mcp/tools/unified/verification.py +520 -0
  131. foundry_mcp-0.3.3.dist-info/METADATA +337 -0
  132. foundry_mcp-0.3.3.dist-info/RECORD +135 -0
  133. foundry_mcp-0.3.3.dist-info/WHEEL +4 -0
  134. foundry_mcp-0.3.3.dist-info/entry_points.txt +3 -0
  135. foundry_mcp-0.3.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,405 @@
1
+ """
2
+ Gemini CLI provider implementation.
3
+
4
+ Bridges the `gemini` command-line interface to the ProviderContext contract by
5
+ handling availability checks, safe command construction, response parsing, and
6
+ token usage normalization.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import logging
13
+ import os
14
+ import subprocess
15
+ from typing import Any, Dict, List, Optional, Protocol, Sequence
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ from .base import (
20
+ ModelDescriptor,
21
+ ProviderCapability,
22
+ ProviderContext,
23
+ ProviderExecutionError,
24
+ ProviderHooks,
25
+ ProviderMetadata,
26
+ ProviderRequest,
27
+ ProviderResult,
28
+ ProviderStatus,
29
+ ProviderTimeoutError,
30
+ ProviderUnavailableError,
31
+ StreamChunk,
32
+ TokenUsage,
33
+ )
34
+ from .detectors import detect_provider_availability
35
+ from .registry import register_provider
36
+
37
+ DEFAULT_BINARY = "gemini"
38
+ DEFAULT_TIMEOUT_SECONDS = 360
39
+ AVAILABILITY_OVERRIDE_ENV = "GEMINI_CLI_AVAILABLE_OVERRIDE"
40
+ CUSTOM_BINARY_ENV = "GEMINI_CLI_BINARY"
41
+
42
+ # Read-only tools allowed for safe codebase exploration
43
+ # Based on Gemini CLI tool names (both class names and function names supported)
44
+ ALLOWED_TOOLS = [
45
+ # Core file operations (read-only)
46
+ "ReadFileTool",
47
+ "read_file",
48
+ "ReadManyFilesTool",
49
+ "read_many_files",
50
+ "LSTool",
51
+ "list_directory",
52
+ "GlobTool",
53
+ "glob",
54
+ "GrepTool",
55
+ "search_file_content",
56
+ # Shell commands - file viewing
57
+ "ShellTool(cat)",
58
+ "ShellTool(head)",
59
+ "ShellTool(tail)",
60
+ "ShellTool(bat)",
61
+ # Shell commands - directory listing/navigation
62
+ "ShellTool(ls)",
63
+ "ShellTool(tree)",
64
+ "ShellTool(pwd)",
65
+ "ShellTool(which)",
66
+ "ShellTool(whereis)",
67
+ # Shell commands - search/find
68
+ "ShellTool(grep)",
69
+ "ShellTool(rg)",
70
+ "ShellTool(ag)",
71
+ "ShellTool(find)",
72
+ "ShellTool(fd)",
73
+ # Shell commands - git operations (read-only)
74
+ "ShellTool(git log)",
75
+ "ShellTool(git show)",
76
+ "ShellTool(git diff)",
77
+ "ShellTool(git status)",
78
+ "ShellTool(git grep)",
79
+ "ShellTool(git blame)",
80
+ # Shell commands - text processing
81
+ "ShellTool(wc)",
82
+ "ShellTool(cut)",
83
+ "ShellTool(paste)",
84
+ "ShellTool(column)",
85
+ "ShellTool(sort)",
86
+ "ShellTool(uniq)",
87
+ # Shell commands - data formats
88
+ "ShellTool(jq)",
89
+ "ShellTool(yq)",
90
+ # Shell commands - file analysis
91
+ "ShellTool(file)",
92
+ "ShellTool(stat)",
93
+ "ShellTool(du)",
94
+ "ShellTool(df)",
95
+ # Shell commands - checksums/hashing
96
+ "ShellTool(md5sum)",
97
+ "ShellTool(shasum)",
98
+ "ShellTool(sha256sum)",
99
+ "ShellTool(sha512sum)",
100
+ ]
101
+
102
+ # System prompt addition warning about piped command vulnerability
103
+ PIPED_COMMAND_WARNING = """
104
+ IMPORTANT SECURITY NOTE: When using shell commands, avoid piped commands (e.g., cat file.txt | wc -l).
105
+ Piped commands bypass the tool allowlist checks in Gemini CLI - only the first command in a pipe is validated.
106
+ Instead, use sequential commands or alternative approaches to achieve the same result safely.
107
+ """
108
+
109
+
110
+ class RunnerProtocol(Protocol):
111
+ """Callable signature used for executing Gemini CLI commands."""
112
+
113
+ def __call__(
114
+ self,
115
+ command: Sequence[str],
116
+ *,
117
+ timeout: Optional[int] = None,
118
+ env: Optional[Dict[str, str]] = None,
119
+ ) -> subprocess.CompletedProcess[str]:
120
+ raise NotImplementedError
121
+
122
+
123
+ def _default_runner(
124
+ command: Sequence[str],
125
+ *,
126
+ timeout: Optional[int] = None,
127
+ env: Optional[Dict[str, str]] = None,
128
+ ) -> subprocess.CompletedProcess[str]:
129
+ """Invoke the Gemini CLI via subprocess."""
130
+ return subprocess.run( # noqa: S603,S607 - intentional CLI invocation
131
+ list(command),
132
+ capture_output=True,
133
+ text=True,
134
+ timeout=timeout,
135
+ env=env,
136
+ check=False,
137
+ )
138
+
139
+
140
+ GEMINI_MODELS: List[ModelDescriptor] = [
141
+ ModelDescriptor(
142
+ id="pro",
143
+ display_name="Gemini 3.0 Pro",
144
+ capabilities={
145
+ ProviderCapability.TEXT,
146
+ ProviderCapability.STREAMING,
147
+ ProviderCapability.VISION,
148
+ },
149
+ routing_hints={"tier": "pro", "context_window": "1M"},
150
+ ),
151
+ ModelDescriptor(
152
+ id="gemini-2.5-pro",
153
+ display_name="Gemini 2.5 Pro",
154
+ capabilities={
155
+ ProviderCapability.TEXT,
156
+ ProviderCapability.STREAMING,
157
+ ProviderCapability.VISION,
158
+ },
159
+ routing_hints={"tier": "pro", "context_window": "1M"},
160
+ ),
161
+ ModelDescriptor(
162
+ id="gemini-2.5-flash",
163
+ display_name="Gemini 2.5 Flash",
164
+ capabilities={
165
+ ProviderCapability.TEXT,
166
+ ProviderCapability.STREAMING,
167
+ ProviderCapability.VISION,
168
+ },
169
+ routing_hints={"tier": "flash"},
170
+ ),
171
+ ]
172
+
173
+ GEMINI_METADATA = ProviderMetadata(
174
+ provider_id="gemini",
175
+ display_name="Google Gemini CLI",
176
+ models=GEMINI_MODELS,
177
+ default_model="gemini-2.5-flash",
178
+ capabilities={ProviderCapability.TEXT, ProviderCapability.STREAMING, ProviderCapability.VISION},
179
+ security_flags={"writes_allowed": False},
180
+ extra={"cli": "gemini", "output_format": "json"},
181
+ )
182
+
183
+
184
+ class GeminiProvider(ProviderContext):
185
+ """ProviderContext implementation backed by the Gemini CLI."""
186
+
187
+ def __init__(
188
+ self,
189
+ metadata: ProviderMetadata,
190
+ hooks: ProviderHooks,
191
+ *,
192
+ model: Optional[str] = None,
193
+ binary: Optional[str] = None,
194
+ runner: Optional[RunnerProtocol] = None,
195
+ env: Optional[Dict[str, str]] = None,
196
+ timeout: Optional[int] = None,
197
+ ):
198
+ super().__init__(metadata, hooks)
199
+ self._runner = runner or _default_runner
200
+ self._binary = binary or os.environ.get(CUSTOM_BINARY_ENV, DEFAULT_BINARY)
201
+ self._env = env
202
+ self._timeout = timeout or DEFAULT_TIMEOUT_SECONDS
203
+ self._model = self._ensure_model(model or metadata.default_model or self._first_model_id())
204
+
205
+ def _first_model_id(self) -> str:
206
+ if not self.metadata.models:
207
+ raise ProviderUnavailableError(
208
+ "Gemini provider metadata is missing model descriptors.",
209
+ provider=self.metadata.provider_id,
210
+ )
211
+ return self.metadata.models[0].id
212
+
213
+ def _ensure_model(self, candidate: str) -> str:
214
+ available = {descriptor.id for descriptor in self.metadata.models}
215
+ if candidate not in available:
216
+ raise ProviderExecutionError(
217
+ f"Unsupported Gemini model '{candidate}'. Available: {', '.join(sorted(available))}",
218
+ provider=self.metadata.provider_id,
219
+ )
220
+ return candidate
221
+
222
+ def _validate_request(self, request: ProviderRequest) -> None:
223
+ """Validate and normalize request, ignoring unsupported parameters."""
224
+ unsupported: List[str] = []
225
+ if request.temperature is not None:
226
+ unsupported.append("temperature")
227
+ if request.max_tokens is not None:
228
+ unsupported.append("max_tokens")
229
+ if request.attachments:
230
+ unsupported.append("attachments")
231
+ if unsupported:
232
+ # Log warning but continue - ignore unsupported parameters
233
+ logger.warning(
234
+ f"Gemini CLI ignoring unsupported parameters: {', '.join(unsupported)}"
235
+ )
236
+
237
+ def _build_prompt(self, request: ProviderRequest) -> str:
238
+ # Build the system prompt with security warning
239
+ system_parts = []
240
+ if request.system_prompt:
241
+ system_parts.append(request.system_prompt.strip())
242
+ system_parts.append(PIPED_COMMAND_WARNING.strip())
243
+
244
+ if system_parts:
245
+ return f"{chr(10).join(system_parts)}\n\n{request.prompt}"
246
+ return request.prompt
247
+
248
+ def _build_command(self, model: str, prompt: str) -> List[str]:
249
+ command = [self._binary, "--output-format", "json"]
250
+
251
+ # Add allowed tools for read-only enforcement
252
+ for tool in ALLOWED_TOOLS:
253
+ command.extend(["--allowed-tools", tool])
254
+
255
+ # Add prompt at the end
256
+ command.extend(["-p", prompt])
257
+
258
+ # Insert model if specified
259
+ if model:
260
+ command[1:1] = ["-m", model]
261
+
262
+ return command
263
+
264
+ def _run(self, command: Sequence[str], timeout: Optional[float]) -> subprocess.CompletedProcess[str]:
265
+ try:
266
+ return self._runner(command, timeout=int(timeout) if timeout else None, env=self._env)
267
+ except FileNotFoundError as exc:
268
+ raise ProviderUnavailableError(
269
+ f"Gemini CLI '{self._binary}' is not available on PATH.",
270
+ provider=self.metadata.provider_id,
271
+ ) from exc
272
+ except subprocess.TimeoutExpired as exc:
273
+ raise ProviderTimeoutError(
274
+ f"Command timed out after {exc.timeout} seconds",
275
+ provider=self.metadata.provider_id,
276
+ ) from exc
277
+
278
+ def _parse_output(self, raw: str) -> Dict[str, Any]:
279
+ text = raw.strip()
280
+ if not text:
281
+ raise ProviderExecutionError(
282
+ "Gemini CLI returned empty output.",
283
+ provider=self.metadata.provider_id,
284
+ )
285
+ try:
286
+ return json.loads(text)
287
+ except json.JSONDecodeError as exc:
288
+ logger.debug(f"Gemini CLI JSON parse error: {exc}")
289
+ raise ProviderExecutionError(
290
+ "Gemini CLI returned invalid JSON response",
291
+ provider=self.metadata.provider_id,
292
+ ) from exc
293
+
294
+ def _extract_usage(self, payload: Dict[str, Any]) -> TokenUsage:
295
+ stats = payload.get("stats") or {}
296
+ models_section = stats.get("models") or {}
297
+ first_model = next(iter(models_section.values()), {})
298
+ tokens = first_model.get("tokens") or {}
299
+ return TokenUsage(
300
+ input_tokens=int(tokens.get("prompt") or tokens.get("input") or 0),
301
+ output_tokens=int(tokens.get("candidates") or tokens.get("output") or 0),
302
+ total_tokens=int(tokens.get("total") or 0),
303
+ )
304
+
305
+ def _resolve_model(self, request: ProviderRequest) -> str:
306
+ model_override = request.metadata.get("model") if request.metadata else None
307
+ if model_override:
308
+ return self._ensure_model(str(model_override))
309
+ return self._model
310
+
311
+ def _emit_stream_if_requested(self, content: str, *, stream: bool) -> None:
312
+ if not stream or not content:
313
+ return
314
+ self._emit_stream_chunk(StreamChunk(content=content, index=0))
315
+
316
+ def _execute(self, request: ProviderRequest) -> ProviderResult:
317
+ self._validate_request(request)
318
+ model = self._resolve_model(request)
319
+ prompt = self._build_prompt(request)
320
+ command = self._build_command(model, prompt)
321
+ timeout = request.timeout or self._timeout
322
+ completed = self._run(command, timeout=timeout)
323
+
324
+ if completed.returncode != 0:
325
+ stderr = (completed.stderr or "").strip()
326
+ logger.debug(f"Gemini CLI stderr: {stderr or 'no stderr'}")
327
+ raise ProviderExecutionError(
328
+ f"Gemini CLI exited with code {completed.returncode}",
329
+ provider=self.metadata.provider_id,
330
+ )
331
+
332
+ payload = self._parse_output(completed.stdout)
333
+ content = str(payload.get("response") or payload.get("content") or "").strip()
334
+ reported_model = payload.get("model") or next(
335
+ iter((payload.get("stats") or {}).get("models") or {}), model
336
+ )
337
+ usage = self._extract_usage(payload)
338
+
339
+ self._emit_stream_if_requested(content, stream=request.stream)
340
+
341
+ return ProviderResult(
342
+ content=content,
343
+ provider_id=self.metadata.provider_id,
344
+ model_used=f"{self.metadata.provider_id}:{reported_model}",
345
+ status=ProviderStatus.SUCCESS,
346
+ tokens=usage,
347
+ stderr=(completed.stderr or "").strip() or None,
348
+ raw_payload=payload,
349
+ )
350
+
351
+
352
+ def is_gemini_available() -> bool:
353
+ """Gemini CLI availability check."""
354
+ return detect_provider_availability("gemini")
355
+
356
+
357
+ def create_provider(
358
+ *,
359
+ hooks: ProviderHooks,
360
+ model: Optional[str] = None,
361
+ dependencies: Optional[Dict[str, object]] = None,
362
+ overrides: Optional[Dict[str, object]] = None,
363
+ ) -> GeminiProvider:
364
+ """
365
+ Factory used by the provider registry.
366
+
367
+ dependencies/overrides allow callers (or tests) to inject runner/env/binary.
368
+ """
369
+ dependencies = dependencies or {}
370
+ overrides = overrides or {}
371
+ runner = dependencies.get("runner")
372
+ env = dependencies.get("env")
373
+ binary = overrides.get("binary") or dependencies.get("binary")
374
+ timeout = overrides.get("timeout")
375
+ selected_model = overrides.get("model") if overrides.get("model") else model
376
+
377
+ return GeminiProvider(
378
+ metadata=GEMINI_METADATA,
379
+ hooks=hooks,
380
+ model=selected_model, # type: ignore[arg-type]
381
+ binary=binary, # type: ignore[arg-type]
382
+ runner=runner if runner is not None else None, # type: ignore[arg-type]
383
+ env=env if env is not None else None, # type: ignore[arg-type]
384
+ timeout=timeout if timeout is not None else None, # type: ignore[arg-type]
385
+ )
386
+
387
+
388
+ # Register the provider immediately so consumers can resolve it by id.
389
+ register_provider(
390
+ "gemini",
391
+ factory=create_provider,
392
+ metadata=GEMINI_METADATA,
393
+ availability_check=is_gemini_available,
394
+ description="Google Gemini CLI adapter",
395
+ tags=("cli", "text", "vision"),
396
+ replace=True,
397
+ )
398
+
399
+
400
+ __all__ = [
401
+ "GeminiProvider",
402
+ "create_provider",
403
+ "is_gemini_available",
404
+ "GEMINI_METADATA",
405
+ ]