gemini-cli-proxy 1.0.3__tar.gz → 1.1.0__tar.gz
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.
- {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/.gitignore +3 -0
- {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/PKG-INFO +8 -3
- {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/README.md +7 -2
- {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/README_zh.md +7 -2
- {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/pyproject.toml +1 -1
- {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/src/gemini_cli_proxy/cli.py +14 -9
- {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/src/gemini_cli_proxy/config.py +6 -3
- gemini_cli_proxy-1.1.0/src/gemini_cli_proxy/gemini_client.py +336 -0
- {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/src/gemini_cli_proxy/models.py +8 -1
- {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/src/gemini_cli_proxy/openai_adapter.py +3 -1
- {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/src/gemini_cli_proxy/server.py +8 -3
- {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/uv.lock +1 -1
- gemini_cli_proxy-1.0.3/src/gemini_cli_proxy/gemini_client.py +0 -175
- {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/.cursor/rules/coding-conventions.mdc +0 -0
- {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/.cursor/rules/project-overview.mdc +0 -0
- {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/.python-version +0 -0
- {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/LICENSE +0 -0
- {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/img/cherry-studio-1.jpg +0 -0
- {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/img/cherry-studio-2.jpg +0 -0
- {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/src/gemini_cli_proxy/__init__.py +0 -0
- {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/tests/__init__.py +0 -0
- {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/tests/integration/__init__.py +0 -0
- {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/tests/unit/__init__.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: gemini-cli-proxy
|
3
|
-
Version: 1.0
|
3
|
+
Version: 1.1.0
|
4
4
|
Summary: OpenAI-compatible API wrapper for Gemini CLI
|
5
5
|
Author: nettee
|
6
6
|
License: MIT
|
@@ -59,6 +59,12 @@ gemini -p "Hello, Gemini"
|
|
59
59
|
|
60
60
|
### Start Gemini CLI Proxy
|
61
61
|
|
62
|
+
Method 1: Direct startup
|
63
|
+
```bash
|
64
|
+
uvx gemini-cli-proxy
|
65
|
+
```
|
66
|
+
|
67
|
+
Method 2: Clone this repository and run:
|
62
68
|
```bash
|
63
69
|
uv run gemini-cli-proxy
|
64
70
|
```
|
@@ -122,11 +128,10 @@ gemini-cli-proxy --help
|
|
122
128
|
Available options:
|
123
129
|
- `--host`: Server host address (default: 127.0.0.1)
|
124
130
|
- `--port`: Server port (default: 8765)
|
125
|
-
- `--log-level`: Log level (debug/info/warning/error/critical)
|
126
131
|
- `--rate-limit`: Max requests per minute (default: 60)
|
127
132
|
- `--max-concurrency`: Max concurrent subprocesses (default: 4)
|
128
133
|
- `--timeout`: Gemini CLI command timeout in seconds (default: 30.0)
|
129
|
-
- `--debug`: Enable debug mode
|
134
|
+
- `--debug`: Enable debug mode (enables debug logging and file watching)
|
130
135
|
|
131
136
|
## ❓ FAQ
|
132
137
|
|
@@ -43,6 +43,12 @@ gemini -p "Hello, Gemini"
|
|
43
43
|
|
44
44
|
### Start Gemini CLI Proxy
|
45
45
|
|
46
|
+
Method 1: Direct startup
|
47
|
+
```bash
|
48
|
+
uvx gemini-cli-proxy
|
49
|
+
```
|
50
|
+
|
51
|
+
Method 2: Clone this repository and run:
|
46
52
|
```bash
|
47
53
|
uv run gemini-cli-proxy
|
48
54
|
```
|
@@ -106,11 +112,10 @@ gemini-cli-proxy --help
|
|
106
112
|
Available options:
|
107
113
|
- `--host`: Server host address (default: 127.0.0.1)
|
108
114
|
- `--port`: Server port (default: 8765)
|
109
|
-
- `--log-level`: Log level (debug/info/warning/error/critical)
|
110
115
|
- `--rate-limit`: Max requests per minute (default: 60)
|
111
116
|
- `--max-concurrency`: Max concurrent subprocesses (default: 4)
|
112
117
|
- `--timeout`: Gemini CLI command timeout in seconds (default: 30.0)
|
113
|
-
- `--debug`: Enable debug mode
|
118
|
+
- `--debug`: Enable debug mode (enables debug logging and file watching)
|
114
119
|
|
115
120
|
## ❓ FAQ
|
116
121
|
|
@@ -43,6 +43,12 @@ gemini -p "Hello, Gemini"
|
|
43
43
|
|
44
44
|
### 启动 Gemini CLI Proxy
|
45
45
|
|
46
|
+
方法一:直接启动运行
|
47
|
+
```bash
|
48
|
+
uvx gemini-cli-proxy
|
49
|
+
```
|
50
|
+
|
51
|
+
方法二:克隆本仓库,然后运行:
|
46
52
|
```bash
|
47
53
|
uv run gemini-cli-proxy
|
48
54
|
```
|
@@ -106,11 +112,10 @@ gemini-cli-proxy --help
|
|
106
112
|
可用选项:
|
107
113
|
- `--host`: 服务器主机地址 (默认: 127.0.0.1)
|
108
114
|
- `--port`: 服务器端口 (默认: 8765)
|
109
|
-
- `--log-level`: 日志级别 (debug/info/warning/error/critical)
|
110
115
|
- `--rate-limit`: 每分钟最大请求数 (默认: 60)
|
111
116
|
- `--max-concurrency`: 最大并发子进程数 (默认: 4)
|
112
117
|
- `--timeout`: Gemini CLI 命令超时时间,单位秒 (默认: 30.0)
|
113
|
-
- `--debug`: 启用调试模式
|
118
|
+
- `--debug`: 启用调试模式 (启用调试日志和文件监控)
|
114
119
|
|
115
120
|
## ❓ 常见问题
|
116
121
|
|
@@ -20,12 +20,6 @@ import uvicorn
|
|
20
20
|
type=int,
|
21
21
|
help="Server port"
|
22
22
|
)
|
23
|
-
@click.option(
|
24
|
-
"--log-level",
|
25
|
-
default="info",
|
26
|
-
type=click.Choice(["debug", "info", "warning", "error", "critical"]),
|
27
|
-
help="Log level"
|
28
|
-
)
|
29
23
|
@click.option(
|
30
24
|
"--rate-limit",
|
31
25
|
default=60,
|
@@ -52,7 +46,6 @@ import uvicorn
|
|
52
46
|
def main(
|
53
47
|
host: str,
|
54
48
|
port: int,
|
55
|
-
log_level: str,
|
56
49
|
rate_limit: int,
|
57
50
|
max_concurrency: int,
|
58
51
|
timeout: float,
|
@@ -61,21 +54,33 @@ def main(
|
|
61
54
|
"""Start Gemini CLI Proxy server"""
|
62
55
|
|
63
56
|
# Set configuration
|
57
|
+
import os
|
64
58
|
from .config import config
|
59
|
+
|
60
|
+
# Set environment variable for reload mode
|
61
|
+
os.environ['GEMINI_CLI_PROXY_DEBUG'] = str(debug)
|
62
|
+
|
65
63
|
config.host = host
|
66
64
|
config.port = port
|
67
|
-
config.log_level =
|
65
|
+
config.log_level = "debug" if debug else "info"
|
68
66
|
config.rate_limit = rate_limit
|
69
67
|
config.max_concurrency = max_concurrency
|
70
68
|
config.timeout = timeout
|
71
69
|
config.debug = debug
|
72
70
|
|
71
|
+
# Update logging level based on configuration
|
72
|
+
import logging
|
73
|
+
# Set root logger level
|
74
|
+
logging.getLogger().setLevel(getattr(logging, config.log_level.upper()))
|
75
|
+
# Also set level for all gemini_cli_proxy loggers
|
76
|
+
logging.getLogger('gemini_cli_proxy').setLevel(getattr(logging, config.log_level.upper()))
|
77
|
+
|
73
78
|
# Start server
|
74
79
|
uvicorn.run(
|
75
80
|
"gemini_cli_proxy.server:app",
|
76
81
|
host=host,
|
77
82
|
port=port,
|
78
|
-
log_level=log_level,
|
83
|
+
log_level=config.log_level,
|
79
84
|
reload=debug
|
80
85
|
)
|
81
86
|
|
@@ -9,15 +9,18 @@ class Config:
|
|
9
9
|
"""Application configuration class"""
|
10
10
|
|
11
11
|
def __init__(self):
|
12
|
+
import os
|
13
|
+
|
12
14
|
# Server configuration
|
13
15
|
self.host: str = "127.0.0.1"
|
14
16
|
self.port: int = 8765
|
15
|
-
|
16
|
-
self.debug: bool =
|
17
|
+
# Read from environment variable if available (for reload mode)
|
18
|
+
self.debug: bool = os.environ.get('GEMINI_CLI_PROXY_DEBUG', 'false').lower() == 'true'
|
19
|
+
self.log_level: str = "debug" if self.debug else "info"
|
17
20
|
|
18
21
|
# Gemini CLI configuration
|
19
22
|
self.gemini_command: str = "gemini" # Gemini CLI command path
|
20
|
-
self.timeout: float =
|
23
|
+
self.timeout: float = 120.0
|
21
24
|
|
22
25
|
# Limit configuration
|
23
26
|
self.rate_limit: int = 60 # Requests per minute
|
@@ -0,0 +1,336 @@
|
|
1
|
+
"""
|
2
|
+
Gemini client module
|
3
|
+
|
4
|
+
Handles interaction with Gemini CLI tool
|
5
|
+
"""
|
6
|
+
|
7
|
+
import asyncio
|
8
|
+
import logging
|
9
|
+
import os
|
10
|
+
import tempfile
|
11
|
+
import uuid
|
12
|
+
import base64
|
13
|
+
import re
|
14
|
+
from typing import List, Optional, AsyncGenerator, Tuple
|
15
|
+
from .models import ChatMessage
|
16
|
+
from .config import config
|
17
|
+
|
18
|
+
logger = logging.getLogger('gemini_cli_proxy')
|
19
|
+
|
20
|
+
|
21
|
+
class GeminiClient:
|
22
|
+
"""Gemini CLI client"""
|
23
|
+
|
24
|
+
def __init__(self):
|
25
|
+
self.semaphore = asyncio.Semaphore(config.max_concurrency)
|
26
|
+
|
27
|
+
def _simplify_error_message(self, raw_error: str) -> Optional[str]:
|
28
|
+
"""
|
29
|
+
Convert Gemini CLI error messages to more readable user-friendly messages
|
30
|
+
|
31
|
+
Args:
|
32
|
+
raw_error: Raw error message from Gemini CLI
|
33
|
+
|
34
|
+
Returns:
|
35
|
+
Simplified error message, or None if the error cannot be recognized
|
36
|
+
"""
|
37
|
+
if not raw_error:
|
38
|
+
return None
|
39
|
+
|
40
|
+
lower_err = raw_error.lower()
|
41
|
+
|
42
|
+
# Check for rate limiting related keywords
|
43
|
+
rate_limit_indicators = [
|
44
|
+
"code\": 429",
|
45
|
+
"status 429",
|
46
|
+
"ratelimitexceeded",
|
47
|
+
"resource_exhausted",
|
48
|
+
"quota exceeded",
|
49
|
+
"quota metric",
|
50
|
+
"requests per day",
|
51
|
+
"requests per minute",
|
52
|
+
"limit exceeded"
|
53
|
+
]
|
54
|
+
|
55
|
+
if any(keyword in lower_err for keyword in rate_limit_indicators):
|
56
|
+
return "Gemini CLI rate limit exceeded. Please run `gemini` directly to check."
|
57
|
+
|
58
|
+
return None
|
59
|
+
|
60
|
+
async def chat_completion(
|
61
|
+
self,
|
62
|
+
messages: List[ChatMessage],
|
63
|
+
model: str,
|
64
|
+
temperature: Optional[float] = None,
|
65
|
+
max_tokens: Optional[int] = None,
|
66
|
+
**kwargs
|
67
|
+
) -> str:
|
68
|
+
"""
|
69
|
+
Execute chat completion request
|
70
|
+
|
71
|
+
Args:
|
72
|
+
messages: List of chat messages
|
73
|
+
model: Model name to use
|
74
|
+
temperature: Temperature parameter
|
75
|
+
max_tokens: Maximum number of tokens
|
76
|
+
**kwargs: Other parameters
|
77
|
+
|
78
|
+
Returns:
|
79
|
+
Response text from Gemini CLI
|
80
|
+
|
81
|
+
Raises:
|
82
|
+
asyncio.TimeoutError: Timeout error
|
83
|
+
subprocess.CalledProcessError: Command execution error
|
84
|
+
"""
|
85
|
+
async with self.semaphore:
|
86
|
+
return await self._execute_gemini_command(
|
87
|
+
messages, model, temperature, max_tokens, **kwargs
|
88
|
+
)
|
89
|
+
|
90
|
+
async def chat_completion_stream(
|
91
|
+
self,
|
92
|
+
messages: List[ChatMessage],
|
93
|
+
model: str,
|
94
|
+
temperature: Optional[float] = None,
|
95
|
+
max_tokens: Optional[int] = None,
|
96
|
+
**kwargs
|
97
|
+
) -> AsyncGenerator[str, None]:
|
98
|
+
"""
|
99
|
+
Execute streaming chat completion request (fake streaming implementation)
|
100
|
+
|
101
|
+
Args:
|
102
|
+
messages: List of chat messages
|
103
|
+
model: Model name to use
|
104
|
+
temperature: Temperature parameter
|
105
|
+
max_tokens: Maximum number of tokens
|
106
|
+
**kwargs: Other parameters
|
107
|
+
|
108
|
+
Yields:
|
109
|
+
Response text chunks split by lines
|
110
|
+
"""
|
111
|
+
# First get complete response
|
112
|
+
full_response = await self.chat_completion(
|
113
|
+
messages, model, temperature, max_tokens, **kwargs
|
114
|
+
)
|
115
|
+
|
116
|
+
# Split by lines and yield one by one
|
117
|
+
lines = full_response.split('\n')
|
118
|
+
for line in lines:
|
119
|
+
if line.strip(): # Skip empty lines
|
120
|
+
yield line.strip()
|
121
|
+
# Add small delay to simulate streaming effect
|
122
|
+
await asyncio.sleep(0.05)
|
123
|
+
|
124
|
+
async def _execute_gemini_command(
|
125
|
+
self,
|
126
|
+
messages: List[ChatMessage],
|
127
|
+
model: str,
|
128
|
+
temperature: Optional[float] = None,
|
129
|
+
max_tokens: Optional[int] = None,
|
130
|
+
**kwargs
|
131
|
+
) -> str:
|
132
|
+
"""
|
133
|
+
Execute Gemini CLI command
|
134
|
+
|
135
|
+
Args:
|
136
|
+
messages: List of chat messages
|
137
|
+
model: Model name to use
|
138
|
+
temperature: Temperature parameter
|
139
|
+
max_tokens: Maximum number of tokens
|
140
|
+
**kwargs: Other parameters
|
141
|
+
|
142
|
+
Returns:
|
143
|
+
Command output result
|
144
|
+
"""
|
145
|
+
# Build command arguments and get temporary files
|
146
|
+
prompt, temp_files = self._build_prompt_with_images(messages)
|
147
|
+
|
148
|
+
cmd_args = [config.gemini_command]
|
149
|
+
cmd_args.extend(["-m", model])
|
150
|
+
cmd_args.extend(["-p", prompt])
|
151
|
+
|
152
|
+
# Note: Real gemini CLI doesn't support temperature and max_tokens parameters
|
153
|
+
# We ignore these parameters here but log them
|
154
|
+
if temperature is not None:
|
155
|
+
logger.debug(f"Ignoring temperature parameter: {temperature} (gemini CLI doesn't support)")
|
156
|
+
if max_tokens is not None:
|
157
|
+
logger.debug(f"Ignoring max_tokens parameter: {max_tokens} (gemini CLI doesn't support)")
|
158
|
+
|
159
|
+
logger.debug(f"Executing command: {' '.join(cmd_args)}")
|
160
|
+
|
161
|
+
try:
|
162
|
+
# Use asyncio to execute subprocess
|
163
|
+
process = await asyncio.create_subprocess_exec(
|
164
|
+
*cmd_args,
|
165
|
+
stdout=asyncio.subprocess.PIPE,
|
166
|
+
stderr=asyncio.subprocess.PIPE
|
167
|
+
)
|
168
|
+
|
169
|
+
# Wait for command execution to complete with timeout
|
170
|
+
stdout, stderr = await asyncio.wait_for(
|
171
|
+
process.communicate(),
|
172
|
+
timeout=config.timeout
|
173
|
+
)
|
174
|
+
|
175
|
+
# Check return code
|
176
|
+
if process.returncode != 0:
|
177
|
+
error_msg = stderr.decode('utf-8').strip()
|
178
|
+
|
179
|
+
# Try to simplify error message to more user-friendly format
|
180
|
+
simplified_msg = self._simplify_error_message(error_msg)
|
181
|
+
if simplified_msg:
|
182
|
+
logger.warning(f"Gemini CLI error (simplified): {simplified_msg}")
|
183
|
+
raise RuntimeError(simplified_msg)
|
184
|
+
else:
|
185
|
+
logger.warning(f"Gemini CLI execution failed: {error_msg}")
|
186
|
+
raise RuntimeError(f"Gemini CLI execution failed (exit code: {process.returncode}): {error_msg}")
|
187
|
+
|
188
|
+
# Return standard output
|
189
|
+
result = stdout.decode('utf-8').strip()
|
190
|
+
logger.debug(f"Gemini CLI response: {result}")
|
191
|
+
return result
|
192
|
+
|
193
|
+
except asyncio.TimeoutError:
|
194
|
+
logger.error(f"Gemini CLI command timeout ({config.timeout}s)")
|
195
|
+
raise RuntimeError(f"Gemini CLI execution timeout ({config.timeout} seconds), please retry later or check your network connection")
|
196
|
+
except RuntimeError:
|
197
|
+
# Re-raise already processed RuntimeError
|
198
|
+
raise
|
199
|
+
except Exception as e:
|
200
|
+
logger.error(f"Error executing Gemini CLI command: {e}")
|
201
|
+
raise RuntimeError(f"Error executing Gemini CLI command: {str(e)}")
|
202
|
+
finally:
|
203
|
+
# Clean up temporary files (skip in debug mode)
|
204
|
+
if not config.debug:
|
205
|
+
for temp_file in temp_files:
|
206
|
+
try:
|
207
|
+
if os.path.exists(temp_file):
|
208
|
+
os.unlink(temp_file)
|
209
|
+
except Exception as e:
|
210
|
+
logger.warning(f"Failed to clean up temp file {temp_file}: {e}")
|
211
|
+
|
212
|
+
def _build_prompt_with_images(self, messages: List[ChatMessage]) -> Tuple[str, List[str]]:
|
213
|
+
"""
|
214
|
+
Build prompt text with image processing
|
215
|
+
|
216
|
+
Args:
|
217
|
+
messages: List of chat messages
|
218
|
+
|
219
|
+
Returns:
|
220
|
+
Tuple of (formatted prompt text, list of temporary file paths)
|
221
|
+
"""
|
222
|
+
prompt_parts = []
|
223
|
+
temp_files = []
|
224
|
+
|
225
|
+
for i, message in enumerate(messages):
|
226
|
+
if isinstance(message.content, str):
|
227
|
+
# Simple string content
|
228
|
+
if message.role == "system":
|
229
|
+
prompt_parts.append(f"System: {message.content}")
|
230
|
+
elif message.role == "user":
|
231
|
+
prompt_parts.append(f"User: {message.content}")
|
232
|
+
elif message.role == "assistant":
|
233
|
+
prompt_parts.append(f"Assistant: {message.content}")
|
234
|
+
else:
|
235
|
+
# List of content parts (vision support)
|
236
|
+
content_parts = []
|
237
|
+
|
238
|
+
for j, part in enumerate(message.content):
|
239
|
+
if part.type == "text" and part.text:
|
240
|
+
content_parts.append(part.text)
|
241
|
+
elif part.type == "image_url" and part.image_url:
|
242
|
+
url = part.image_url.get("url", "")
|
243
|
+
if url.startswith("data:"):
|
244
|
+
# Process base64 image
|
245
|
+
temp_file_path = self._save_base64_image(url)
|
246
|
+
temp_files.append(temp_file_path)
|
247
|
+
content_parts.append(f"@{temp_file_path}")
|
248
|
+
else:
|
249
|
+
# For regular URLs, we'll just pass them through for now
|
250
|
+
# TODO: Download and save remote images if needed
|
251
|
+
content_parts.append(f"<image_url>{url}</image_url>")
|
252
|
+
|
253
|
+
combined_content = " ".join(content_parts)
|
254
|
+
if message.role == "system":
|
255
|
+
prompt_parts.append(f"System: {combined_content}")
|
256
|
+
elif message.role == "user":
|
257
|
+
prompt_parts.append(f"User: {combined_content}")
|
258
|
+
elif message.role == "assistant":
|
259
|
+
prompt_parts.append(f"Assistant: {combined_content}")
|
260
|
+
|
261
|
+
final_prompt = "\n".join(prompt_parts)
|
262
|
+
logger.debug(f"Prompt sent to Gemini CLI: {final_prompt}")
|
263
|
+
|
264
|
+
return final_prompt, temp_files
|
265
|
+
|
266
|
+
def _save_base64_image(self, data_url: str) -> str:
|
267
|
+
"""
|
268
|
+
Save base64 image data to temporary file
|
269
|
+
|
270
|
+
Args:
|
271
|
+
data_url: Data URL in format "data:image/type;base64,..."
|
272
|
+
|
273
|
+
Returns:
|
274
|
+
Path to temporary file
|
275
|
+
|
276
|
+
Raises:
|
277
|
+
ValueError: Invalid data URL format
|
278
|
+
"""
|
279
|
+
try:
|
280
|
+
# Parse data URL
|
281
|
+
if not data_url.startswith("data:"):
|
282
|
+
raise ValueError("Invalid data URL format")
|
283
|
+
|
284
|
+
# Extract MIME type and base64 data
|
285
|
+
header, data = data_url.split(",", 1)
|
286
|
+
mime_info = header.split(";")[0].split(":")[1] # e.g., "image/png"
|
287
|
+
|
288
|
+
# Determine file extension
|
289
|
+
if "png" in mime_info.lower():
|
290
|
+
ext = ".png"
|
291
|
+
elif "jpeg" in mime_info.lower() or "jpg" in mime_info.lower():
|
292
|
+
ext = ".jpg"
|
293
|
+
elif "gif" in mime_info.lower():
|
294
|
+
ext = ".gif"
|
295
|
+
elif "webp" in mime_info.lower():
|
296
|
+
ext = ".webp"
|
297
|
+
else:
|
298
|
+
ext = ".png" # Default to PNG
|
299
|
+
|
300
|
+
# Decode base64 data
|
301
|
+
image_data = base64.b64decode(data)
|
302
|
+
|
303
|
+
# Create .gemini-cli-proxy directory in project root
|
304
|
+
temp_dir = ".gemini-cli-proxy"
|
305
|
+
os.makedirs(temp_dir, exist_ok=True)
|
306
|
+
|
307
|
+
# Create temporary file with simplified name
|
308
|
+
filename = f"{uuid.uuid4().hex[:8]}{ext}"
|
309
|
+
temp_file_path = os.path.join(temp_dir, filename)
|
310
|
+
|
311
|
+
# Write image data
|
312
|
+
with open(temp_file_path, 'wb') as f:
|
313
|
+
f.write(image_data)
|
314
|
+
|
315
|
+
return temp_file_path
|
316
|
+
|
317
|
+
except Exception as e:
|
318
|
+
logger.error(f"Error saving base64 image: {e}")
|
319
|
+
raise ValueError(f"Failed to save base64 image: {e}")
|
320
|
+
|
321
|
+
def _build_prompt(self, messages: List[ChatMessage]) -> str:
|
322
|
+
"""
|
323
|
+
Build prompt text (legacy method, kept for compatibility)
|
324
|
+
|
325
|
+
Args:
|
326
|
+
messages: List of chat messages
|
327
|
+
|
328
|
+
Returns:
|
329
|
+
Formatted prompt text
|
330
|
+
"""
|
331
|
+
prompt, _ = self._build_prompt_with_images(messages)
|
332
|
+
return prompt
|
333
|
+
|
334
|
+
|
335
|
+
# Global client instance
|
336
|
+
gemini_client = GeminiClient()
|
@@ -10,10 +10,17 @@ import time
|
|
10
10
|
import uuid
|
11
11
|
|
12
12
|
|
13
|
+
class ChatContentPart(BaseModel):
|
14
|
+
"""Chat content part model for vision support"""
|
15
|
+
type: Literal["text", "image_url"]
|
16
|
+
text: Optional[str] = None
|
17
|
+
image_url: Optional[Dict[str, str]] = None # {"url": "..."}
|
18
|
+
|
19
|
+
|
13
20
|
class ChatMessage(BaseModel):
|
14
21
|
"""Chat message model"""
|
15
22
|
role: Literal["system", "user", "assistant"]
|
16
|
-
content: str
|
23
|
+
content: Union[str, List[ChatContentPart]]
|
17
24
|
|
18
25
|
|
19
26
|
class ChatCompletionRequest(BaseModel):
|
@@ -20,7 +20,7 @@ from .models import (
|
|
20
20
|
)
|
21
21
|
from .gemini_client import gemini_client
|
22
22
|
|
23
|
-
logger = logging.getLogger(
|
23
|
+
logger = logging.getLogger('gemini_cli_proxy')
|
24
24
|
|
25
25
|
|
26
26
|
class OpenAIAdapter:
|
@@ -42,6 +42,7 @@ class OpenAIAdapter:
|
|
42
42
|
# Call Gemini CLI
|
43
43
|
response_text = await gemini_client.chat_completion(
|
44
44
|
messages=request.messages,
|
45
|
+
model=request.model,
|
45
46
|
temperature=request.temperature,
|
46
47
|
max_tokens=request.max_tokens
|
47
48
|
)
|
@@ -89,6 +90,7 @@ class OpenAIAdapter:
|
|
89
90
|
# Get streaming data generator
|
90
91
|
stream_generator = gemini_client.chat_completion_stream(
|
91
92
|
messages=request.messages,
|
93
|
+
model=request.model,
|
92
94
|
temperature=request.temperature,
|
93
95
|
max_tokens=request.max_tokens
|
94
96
|
)
|
@@ -29,12 +29,12 @@ from .models import (
|
|
29
29
|
)
|
30
30
|
from .openai_adapter import openai_adapter
|
31
31
|
|
32
|
-
# Configure logging
|
32
|
+
# Configure logging (will be updated when config is set)
|
33
33
|
logging.basicConfig(
|
34
|
-
level=
|
34
|
+
level=logging.INFO, # Default level, will be updated in CLI
|
35
35
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
36
36
|
)
|
37
|
-
logger = logging.getLogger(
|
37
|
+
logger = logging.getLogger('gemini_cli_proxy')
|
38
38
|
|
39
39
|
# Create rate limiter
|
40
40
|
limiter = Limiter(key_func=get_remote_address)
|
@@ -43,8 +43,13 @@ limiter = Limiter(key_func=get_remote_address)
|
|
43
43
|
@asynccontextmanager
|
44
44
|
async def lifespan(app: FastAPI):
|
45
45
|
"""Application lifecycle management"""
|
46
|
+
# Ensure logging level is applied after uvicorn starts
|
47
|
+
import logging
|
48
|
+
logging.getLogger('gemini_cli_proxy').setLevel(getattr(logging, config.log_level.upper()))
|
49
|
+
|
46
50
|
logger.info(f"Starting Gemini CLI Proxy v{__version__}")
|
47
51
|
logger.info(f"Configuration: port={config.port}, rate_limit={config.rate_limit}/min, concurrency={config.max_concurrency}")
|
52
|
+
logger.debug(f"Debug logging is enabled (log_level={config.log_level})")
|
48
53
|
yield
|
49
54
|
logger.info("Shutting down Gemini CLI Proxy")
|
50
55
|
|
@@ -379,7 +379,7 @@ wheels = [
|
|
379
379
|
|
380
380
|
[[package]]
|
381
381
|
name = "gemini-cli-proxy"
|
382
|
-
version = "1.0
|
382
|
+
version = "1.1.0"
|
383
383
|
source = { editable = "." }
|
384
384
|
dependencies = [
|
385
385
|
{ name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
@@ -1,175 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Gemini client module
|
3
|
-
|
4
|
-
Handles interaction with Gemini CLI tool
|
5
|
-
"""
|
6
|
-
|
7
|
-
import asyncio
|
8
|
-
import logging
|
9
|
-
from typing import List, Optional, AsyncGenerator
|
10
|
-
from .models import ChatMessage
|
11
|
-
from .config import config
|
12
|
-
|
13
|
-
logger = logging.getLogger(__name__)
|
14
|
-
|
15
|
-
|
16
|
-
class GeminiClient:
|
17
|
-
"""Gemini CLI client"""
|
18
|
-
|
19
|
-
def __init__(self):
|
20
|
-
self.semaphore = asyncio.Semaphore(config.max_concurrency)
|
21
|
-
|
22
|
-
async def chat_completion(
|
23
|
-
self,
|
24
|
-
messages: List[ChatMessage],
|
25
|
-
temperature: Optional[float] = None,
|
26
|
-
max_tokens: Optional[int] = None,
|
27
|
-
**kwargs
|
28
|
-
) -> str:
|
29
|
-
"""
|
30
|
-
Execute chat completion request
|
31
|
-
|
32
|
-
Args:
|
33
|
-
messages: List of chat messages
|
34
|
-
temperature: Temperature parameter
|
35
|
-
max_tokens: Maximum number of tokens
|
36
|
-
**kwargs: Other parameters
|
37
|
-
|
38
|
-
Returns:
|
39
|
-
Response text from Gemini CLI
|
40
|
-
|
41
|
-
Raises:
|
42
|
-
asyncio.TimeoutError: Timeout error
|
43
|
-
subprocess.CalledProcessError: Command execution error
|
44
|
-
"""
|
45
|
-
async with self.semaphore:
|
46
|
-
return await self._execute_gemini_command(
|
47
|
-
messages, temperature, max_tokens, **kwargs
|
48
|
-
)
|
49
|
-
|
50
|
-
async def chat_completion_stream(
|
51
|
-
self,
|
52
|
-
messages: List[ChatMessage],
|
53
|
-
temperature: Optional[float] = None,
|
54
|
-
max_tokens: Optional[int] = None,
|
55
|
-
**kwargs
|
56
|
-
) -> AsyncGenerator[str, None]:
|
57
|
-
"""
|
58
|
-
Execute streaming chat completion request (fake streaming implementation)
|
59
|
-
|
60
|
-
Args:
|
61
|
-
messages: List of chat messages
|
62
|
-
temperature: Temperature parameter
|
63
|
-
max_tokens: Maximum number of tokens
|
64
|
-
**kwargs: Other parameters
|
65
|
-
|
66
|
-
Yields:
|
67
|
-
Response text chunks split by lines
|
68
|
-
"""
|
69
|
-
# First get complete response
|
70
|
-
full_response = await self.chat_completion(
|
71
|
-
messages, temperature, max_tokens, **kwargs
|
72
|
-
)
|
73
|
-
|
74
|
-
# Split by lines and yield one by one
|
75
|
-
lines = full_response.split('\n')
|
76
|
-
for line in lines:
|
77
|
-
if line.strip(): # Skip empty lines
|
78
|
-
yield line.strip()
|
79
|
-
# Add small delay to simulate streaming effect
|
80
|
-
await asyncio.sleep(0.05)
|
81
|
-
|
82
|
-
async def _execute_gemini_command(
|
83
|
-
self,
|
84
|
-
messages: List[ChatMessage],
|
85
|
-
temperature: Optional[float] = None,
|
86
|
-
max_tokens: Optional[int] = None,
|
87
|
-
**kwargs
|
88
|
-
) -> str:
|
89
|
-
"""
|
90
|
-
Execute Gemini CLI command
|
91
|
-
|
92
|
-
Args:
|
93
|
-
messages: List of chat messages
|
94
|
-
temperature: Temperature parameter
|
95
|
-
max_tokens: Maximum number of tokens
|
96
|
-
**kwargs: Other parameters
|
97
|
-
|
98
|
-
Returns:
|
99
|
-
Command output result
|
100
|
-
"""
|
101
|
-
# Build command arguments
|
102
|
-
cmd_args = [config.gemini_command]
|
103
|
-
|
104
|
-
# Build prompt text (simplified implementation: combine all messages)
|
105
|
-
prompt = self._build_prompt(messages)
|
106
|
-
|
107
|
-
# Use --prompt parameter to pass prompt text
|
108
|
-
cmd_args.extend(["--prompt", prompt])
|
109
|
-
|
110
|
-
# Note: Real gemini CLI doesn't support temperature and max_tokens parameters
|
111
|
-
# We ignore these parameters here but log them
|
112
|
-
if temperature is not None:
|
113
|
-
logger.debug(f"Ignoring temperature parameter: {temperature} (gemini CLI doesn't support)")
|
114
|
-
if max_tokens is not None:
|
115
|
-
logger.debug(f"Ignoring max_tokens parameter: {max_tokens} (gemini CLI doesn't support)")
|
116
|
-
|
117
|
-
logger.debug(f"Executing command: {' '.join(cmd_args)}")
|
118
|
-
|
119
|
-
try:
|
120
|
-
# Use asyncio to execute subprocess
|
121
|
-
process = await asyncio.create_subprocess_exec(
|
122
|
-
*cmd_args,
|
123
|
-
stdout=asyncio.subprocess.PIPE,
|
124
|
-
stderr=asyncio.subprocess.PIPE
|
125
|
-
)
|
126
|
-
|
127
|
-
# Wait for command execution to complete with timeout
|
128
|
-
stdout, stderr = await asyncio.wait_for(
|
129
|
-
process.communicate(),
|
130
|
-
timeout=config.timeout
|
131
|
-
)
|
132
|
-
|
133
|
-
# Check return code
|
134
|
-
if process.returncode != 0:
|
135
|
-
error_msg = stderr.decode('utf-8').strip()
|
136
|
-
raise RuntimeError(f"Gemini CLI execution failed (exit code: {process.returncode}): {error_msg}")
|
137
|
-
|
138
|
-
# Return standard output
|
139
|
-
result = stdout.decode('utf-8').strip()
|
140
|
-
logger.debug(f"Command executed successfully, output length: {len(result)}")
|
141
|
-
return result
|
142
|
-
|
143
|
-
except asyncio.TimeoutError:
|
144
|
-
logger.error(f"Gemini CLI command timeout ({config.timeout}s)")
|
145
|
-
raise
|
146
|
-
except Exception as e:
|
147
|
-
logger.error(f"Error executing Gemini CLI command: {e}")
|
148
|
-
raise
|
149
|
-
|
150
|
-
def _build_prompt(self, messages: List[ChatMessage]) -> str:
|
151
|
-
"""
|
152
|
-
Build prompt text
|
153
|
-
|
154
|
-
Args:
|
155
|
-
messages: List of chat messages
|
156
|
-
|
157
|
-
Returns:
|
158
|
-
Formatted prompt text
|
159
|
-
"""
|
160
|
-
# Simplified implementation: format all messages by role
|
161
|
-
prompt_parts = []
|
162
|
-
|
163
|
-
for message in messages:
|
164
|
-
if message.role == "system":
|
165
|
-
prompt_parts.append(f"System: {message.content}")
|
166
|
-
elif message.role == "user":
|
167
|
-
prompt_parts.append(f"User: {message.content}")
|
168
|
-
elif message.role == "assistant":
|
169
|
-
prompt_parts.append(f"Assistant: {message.content}")
|
170
|
-
|
171
|
-
return "\n".join(prompt_parts)
|
172
|
-
|
173
|
-
|
174
|
-
# Global client instance
|
175
|
-
gemini_client = GeminiClient()
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|