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.
- foundry_mcp/__init__.py +7 -0
- foundry_mcp/cli/__init__.py +80 -0
- foundry_mcp/cli/__main__.py +9 -0
- foundry_mcp/cli/agent.py +96 -0
- foundry_mcp/cli/commands/__init__.py +37 -0
- foundry_mcp/cli/commands/cache.py +137 -0
- foundry_mcp/cli/commands/dashboard.py +148 -0
- foundry_mcp/cli/commands/dev.py +446 -0
- foundry_mcp/cli/commands/journal.py +377 -0
- foundry_mcp/cli/commands/lifecycle.py +274 -0
- foundry_mcp/cli/commands/modify.py +824 -0
- foundry_mcp/cli/commands/plan.py +633 -0
- foundry_mcp/cli/commands/pr.py +393 -0
- foundry_mcp/cli/commands/review.py +652 -0
- foundry_mcp/cli/commands/session.py +479 -0
- foundry_mcp/cli/commands/specs.py +856 -0
- foundry_mcp/cli/commands/tasks.py +807 -0
- foundry_mcp/cli/commands/testing.py +676 -0
- foundry_mcp/cli/commands/validate.py +982 -0
- foundry_mcp/cli/config.py +98 -0
- foundry_mcp/cli/context.py +259 -0
- foundry_mcp/cli/flags.py +266 -0
- foundry_mcp/cli/logging.py +212 -0
- foundry_mcp/cli/main.py +44 -0
- foundry_mcp/cli/output.py +122 -0
- foundry_mcp/cli/registry.py +110 -0
- foundry_mcp/cli/resilience.py +178 -0
- foundry_mcp/cli/transcript.py +217 -0
- foundry_mcp/config.py +850 -0
- foundry_mcp/core/__init__.py +144 -0
- foundry_mcp/core/ai_consultation.py +1636 -0
- foundry_mcp/core/cache.py +195 -0
- foundry_mcp/core/capabilities.py +446 -0
- foundry_mcp/core/concurrency.py +898 -0
- foundry_mcp/core/context.py +540 -0
- foundry_mcp/core/discovery.py +1603 -0
- foundry_mcp/core/error_collection.py +728 -0
- foundry_mcp/core/error_store.py +592 -0
- foundry_mcp/core/feature_flags.py +592 -0
- foundry_mcp/core/health.py +749 -0
- foundry_mcp/core/journal.py +694 -0
- foundry_mcp/core/lifecycle.py +412 -0
- foundry_mcp/core/llm_config.py +1350 -0
- foundry_mcp/core/llm_patterns.py +510 -0
- foundry_mcp/core/llm_provider.py +1569 -0
- foundry_mcp/core/logging_config.py +374 -0
- foundry_mcp/core/metrics_persistence.py +584 -0
- foundry_mcp/core/metrics_registry.py +327 -0
- foundry_mcp/core/metrics_store.py +641 -0
- foundry_mcp/core/modifications.py +224 -0
- foundry_mcp/core/naming.py +123 -0
- foundry_mcp/core/observability.py +1216 -0
- foundry_mcp/core/otel.py +452 -0
- foundry_mcp/core/otel_stubs.py +264 -0
- foundry_mcp/core/pagination.py +255 -0
- foundry_mcp/core/progress.py +317 -0
- foundry_mcp/core/prometheus.py +577 -0
- foundry_mcp/core/prompts/__init__.py +464 -0
- foundry_mcp/core/prompts/fidelity_review.py +546 -0
- foundry_mcp/core/prompts/markdown_plan_review.py +511 -0
- foundry_mcp/core/prompts/plan_review.py +623 -0
- foundry_mcp/core/providers/__init__.py +225 -0
- foundry_mcp/core/providers/base.py +476 -0
- foundry_mcp/core/providers/claude.py +460 -0
- foundry_mcp/core/providers/codex.py +619 -0
- foundry_mcp/core/providers/cursor_agent.py +642 -0
- foundry_mcp/core/providers/detectors.py +488 -0
- foundry_mcp/core/providers/gemini.py +405 -0
- foundry_mcp/core/providers/opencode.py +616 -0
- foundry_mcp/core/providers/opencode_wrapper.js +302 -0
- foundry_mcp/core/providers/package-lock.json +24 -0
- foundry_mcp/core/providers/package.json +25 -0
- foundry_mcp/core/providers/registry.py +607 -0
- foundry_mcp/core/providers/test_provider.py +171 -0
- foundry_mcp/core/providers/validation.py +729 -0
- foundry_mcp/core/rate_limit.py +427 -0
- foundry_mcp/core/resilience.py +600 -0
- foundry_mcp/core/responses.py +934 -0
- foundry_mcp/core/review.py +366 -0
- foundry_mcp/core/security.py +438 -0
- foundry_mcp/core/spec.py +1650 -0
- foundry_mcp/core/task.py +1289 -0
- foundry_mcp/core/testing.py +450 -0
- foundry_mcp/core/validation.py +2081 -0
- foundry_mcp/dashboard/__init__.py +32 -0
- foundry_mcp/dashboard/app.py +119 -0
- foundry_mcp/dashboard/components/__init__.py +17 -0
- foundry_mcp/dashboard/components/cards.py +88 -0
- foundry_mcp/dashboard/components/charts.py +234 -0
- foundry_mcp/dashboard/components/filters.py +136 -0
- foundry_mcp/dashboard/components/tables.py +195 -0
- foundry_mcp/dashboard/data/__init__.py +11 -0
- foundry_mcp/dashboard/data/stores.py +433 -0
- foundry_mcp/dashboard/launcher.py +289 -0
- foundry_mcp/dashboard/views/__init__.py +12 -0
- foundry_mcp/dashboard/views/errors.py +217 -0
- foundry_mcp/dashboard/views/metrics.py +174 -0
- foundry_mcp/dashboard/views/overview.py +160 -0
- foundry_mcp/dashboard/views/providers.py +83 -0
- foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
- foundry_mcp/dashboard/views/tool_usage.py +139 -0
- foundry_mcp/prompts/__init__.py +9 -0
- foundry_mcp/prompts/workflows.py +525 -0
- foundry_mcp/resources/__init__.py +9 -0
- foundry_mcp/resources/specs.py +591 -0
- foundry_mcp/schemas/__init__.py +38 -0
- foundry_mcp/schemas/sdd-spec-schema.json +386 -0
- foundry_mcp/server.py +164 -0
- foundry_mcp/tools/__init__.py +10 -0
- foundry_mcp/tools/unified/__init__.py +71 -0
- foundry_mcp/tools/unified/authoring.py +1487 -0
- foundry_mcp/tools/unified/context_helpers.py +98 -0
- foundry_mcp/tools/unified/documentation_helpers.py +198 -0
- foundry_mcp/tools/unified/environment.py +939 -0
- foundry_mcp/tools/unified/error.py +462 -0
- foundry_mcp/tools/unified/health.py +225 -0
- foundry_mcp/tools/unified/journal.py +841 -0
- foundry_mcp/tools/unified/lifecycle.py +632 -0
- foundry_mcp/tools/unified/metrics.py +777 -0
- foundry_mcp/tools/unified/plan.py +745 -0
- foundry_mcp/tools/unified/pr.py +294 -0
- foundry_mcp/tools/unified/provider.py +629 -0
- foundry_mcp/tools/unified/review.py +685 -0
- foundry_mcp/tools/unified/review_helpers.py +299 -0
- foundry_mcp/tools/unified/router.py +102 -0
- foundry_mcp/tools/unified/server.py +580 -0
- foundry_mcp/tools/unified/spec.py +808 -0
- foundry_mcp/tools/unified/task.py +2202 -0
- foundry_mcp/tools/unified/test.py +370 -0
- foundry_mcp/tools/unified/verification.py +520 -0
- foundry_mcp-0.3.3.dist-info/METADATA +337 -0
- foundry_mcp-0.3.3.dist-info/RECORD +135 -0
- foundry_mcp-0.3.3.dist-info/WHEEL +4 -0
- foundry_mcp-0.3.3.dist-info/entry_points.txt +3 -0
- 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
|
+
]
|