gemini-cli-proxy 1.0.3__py3-none-any.whl → 1.1.0__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.
gemini_cli_proxy/cli.py CHANGED
@@ -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 = 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
- self.log_level: str = "info"
16
- self.debug: bool = False
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 = 30.0
23
+ self.timeout: float = 120.0
21
24
 
22
25
  # Limit configuration
23
26
  self.rate_limit: int = 60 # Requests per minute
@@ -6,11 +6,16 @@ Handles interaction with Gemini CLI tool
6
6
 
7
7
  import asyncio
8
8
  import logging
9
- from typing import List, Optional, AsyncGenerator
9
+ import os
10
+ import tempfile
11
+ import uuid
12
+ import base64
13
+ import re
14
+ from typing import List, Optional, AsyncGenerator, Tuple
10
15
  from .models import ChatMessage
11
16
  from .config import config
12
17
 
13
- logger = logging.getLogger(__name__)
18
+ logger = logging.getLogger('gemini_cli_proxy')
14
19
 
15
20
 
16
21
  class GeminiClient:
@@ -19,9 +24,43 @@ class GeminiClient:
19
24
  def __init__(self):
20
25
  self.semaphore = asyncio.Semaphore(config.max_concurrency)
21
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
+
22
60
  async def chat_completion(
23
61
  self,
24
62
  messages: List[ChatMessage],
63
+ model: str,
25
64
  temperature: Optional[float] = None,
26
65
  max_tokens: Optional[int] = None,
27
66
  **kwargs
@@ -31,6 +70,7 @@ class GeminiClient:
31
70
 
32
71
  Args:
33
72
  messages: List of chat messages
73
+ model: Model name to use
34
74
  temperature: Temperature parameter
35
75
  max_tokens: Maximum number of tokens
36
76
  **kwargs: Other parameters
@@ -44,12 +84,13 @@ class GeminiClient:
44
84
  """
45
85
  async with self.semaphore:
46
86
  return await self._execute_gemini_command(
47
- messages, temperature, max_tokens, **kwargs
87
+ messages, model, temperature, max_tokens, **kwargs
48
88
  )
49
89
 
50
90
  async def chat_completion_stream(
51
91
  self,
52
92
  messages: List[ChatMessage],
93
+ model: str,
53
94
  temperature: Optional[float] = None,
54
95
  max_tokens: Optional[int] = None,
55
96
  **kwargs
@@ -59,6 +100,7 @@ class GeminiClient:
59
100
 
60
101
  Args:
61
102
  messages: List of chat messages
103
+ model: Model name to use
62
104
  temperature: Temperature parameter
63
105
  max_tokens: Maximum number of tokens
64
106
  **kwargs: Other parameters
@@ -68,7 +110,7 @@ class GeminiClient:
68
110
  """
69
111
  # First get complete response
70
112
  full_response = await self.chat_completion(
71
- messages, temperature, max_tokens, **kwargs
113
+ messages, model, temperature, max_tokens, **kwargs
72
114
  )
73
115
 
74
116
  # Split by lines and yield one by one
@@ -82,6 +124,7 @@ class GeminiClient:
82
124
  async def _execute_gemini_command(
83
125
  self,
84
126
  messages: List[ChatMessage],
127
+ model: str,
85
128
  temperature: Optional[float] = None,
86
129
  max_tokens: Optional[int] = None,
87
130
  **kwargs
@@ -91,6 +134,7 @@ class GeminiClient:
91
134
 
92
135
  Args:
93
136
  messages: List of chat messages
137
+ model: Model name to use
94
138
  temperature: Temperature parameter
95
139
  max_tokens: Maximum number of tokens
96
140
  **kwargs: Other parameters
@@ -98,14 +142,12 @@ class GeminiClient:
98
142
  Returns:
99
143
  Command output result
100
144
  """
101
- # Build command arguments
102
- cmd_args = [config.gemini_command]
145
+ # Build command arguments and get temporary files
146
+ prompt, temp_files = self._build_prompt_with_images(messages)
103
147
 
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])
148
+ cmd_args = [config.gemini_command]
149
+ cmd_args.extend(["-m", model])
150
+ cmd_args.extend(["-p", prompt])
109
151
 
110
152
  # Note: Real gemini CLI doesn't support temperature and max_tokens parameters
111
153
  # We ignore these parameters here but log them
@@ -133,42 +175,161 @@ class GeminiClient:
133
175
  # Check return code
134
176
  if process.returncode != 0:
135
177
  error_msg = stderr.decode('utf-8').strip()
136
- raise RuntimeError(f"Gemini CLI execution failed (exit code: {process.returncode}): {error_msg}")
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}")
137
187
 
138
188
  # Return standard output
139
189
  result = stdout.decode('utf-8').strip()
140
- logger.debug(f"Command executed successfully, output length: {len(result)}")
190
+ logger.debug(f"Gemini CLI response: {result}")
141
191
  return result
142
192
 
143
193
  except asyncio.TimeoutError:
144
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
145
198
  raise
146
199
  except Exception as e:
147
200
  logger.error(f"Error executing Gemini CLI command: {e}")
148
- raise
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}")
149
211
 
150
- def _build_prompt(self, messages: List[ChatMessage]) -> str:
212
+ def _build_prompt_with_images(self, messages: List[ChatMessage]) -> Tuple[str, List[str]]:
151
213
  """
152
- Build prompt text
214
+ Build prompt text with image processing
153
215
 
154
216
  Args:
155
217
  messages: List of chat messages
156
218
 
157
219
  Returns:
158
- Formatted prompt text
220
+ Tuple of (formatted prompt text, list of temporary file paths)
159
221
  """
160
- # Simplified implementation: format all messages by role
161
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}")
162
263
 
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}")
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
170
269
 
171
- return "\n".join(prompt_parts)
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
172
333
 
173
334
 
174
335
  # Global client instance
@@ -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(__name__)
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=getattr(logging, config.log_level.upper()),
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(__name__)
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemini-cli-proxy
3
- Version: 1.0.3
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
 
@@ -0,0 +1,12 @@
1
+ gemini_cli_proxy/__init__.py,sha256=fOI3EtGmmggiMc3uGV8lGLsbzXmM3ADfnFbaHnwrKtg,257
2
+ gemini_cli_proxy/cli.py,sha256=3_ZgcvEDzRtY6AVA0-8VbQtUWOcpn3waoAsH-soOQ18,1863
3
+ gemini_cli_proxy/config.py,sha256=Ly7_eGTssitIbSVhxq7Tq-bQao83oDWzIyI-t1PJpvM,1043
4
+ gemini_cli_proxy/gemini_client.py,sha256=wWIZ4gaT1CqFbZaFsUKRiDf0RHWpou1AZGYwGeuw_1o,12180
5
+ gemini_cli_proxy/models.py,sha256=SiyesskO4J3rLb4nayxHc4cpbTKQQ6j7osTOsTo7Ems,3033
6
+ gemini_cli_proxy/openai_adapter.py,sha256=oi4W4SjuFjii-v42KVWqh1UGzaTXI_KvrUIhBTcOiFM,5482
7
+ gemini_cli_proxy/server.py,sha256=a25eSsTUWyTLxNjFfcOFiL2TUgykaQRc3dBgfp6pC-8,5696
8
+ gemini_cli_proxy-1.1.0.dist-info/METADATA,sha256=ujEj4r8IPPIfqHnA2HfsWOYjfOj6XCBhh9aWAnKzl3E,4085
9
+ gemini_cli_proxy-1.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
+ gemini_cli_proxy-1.1.0.dist-info/entry_points.txt,sha256=wDLl4ePzvEWNQMSxoE7rKV5k8_MpK6yQwpYdiaXjcWI,63
11
+ gemini_cli_proxy-1.1.0.dist-info/licenses/LICENSE,sha256=-LKYkZXXzjCmYRVwR74fDmMHP3gNlKIW_UUuEbY9hq8,1068
12
+ gemini_cli_proxy-1.1.0.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- gemini_cli_proxy/__init__.py,sha256=fOI3EtGmmggiMc3uGV8lGLsbzXmM3ADfnFbaHnwrKtg,257
2
- gemini_cli_proxy/cli.py,sha256=hc83w1AUobLN_-ONITforwdyQBx_0jwGyn-M_zbh4LQ,1555
3
- gemini_cli_proxy/config.py,sha256=5erCL5v5sb2Kz-Kje-luCw7EJUB8oq5wU3fdBnMS3H0,854
4
- gemini_cli_proxy/gemini_client.py,sha256=nfRbgzTHvh7w4QsLzG-s5B964JIBzwt--PE3jvQT0R4,5735
5
- gemini_cli_proxy/models.py,sha256=3FNvKk4CuLUU7MrFM0X12HeEN5paRPrRoJO0083KLfQ,2779
6
- gemini_cli_proxy/openai_adapter.py,sha256=x_8dUcob1DOLnKbTLQBsmN_e1dO6mX0DFaXqUmzcBzY,5394
7
- gemini_cli_proxy/server.py,sha256=6u6vEc4nqrh6eocflmx8JuHTZuWoH4uKJdfdblMnJfo,5383
8
- gemini_cli_proxy-1.0.3.dist-info/METADATA,sha256=vi5AG8BOBCWU25lJyIUXmKybadk4GzwOcZ8mJzhSzrg,4006
9
- gemini_cli_proxy-1.0.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
- gemini_cli_proxy-1.0.3.dist-info/entry_points.txt,sha256=wDLl4ePzvEWNQMSxoE7rKV5k8_MpK6yQwpYdiaXjcWI,63
11
- gemini_cli_proxy-1.0.3.dist-info/licenses/LICENSE,sha256=-LKYkZXXzjCmYRVwR74fDmMHP3gNlKIW_UUuEbY9hq8,1068
12
- gemini_cli_proxy-1.0.3.dist-info/RECORD,,