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.
Files changed (23) hide show
  1. {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/.gitignore +3 -0
  2. {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/PKG-INFO +8 -3
  3. {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/README.md +7 -2
  4. {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/README_zh.md +7 -2
  5. {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/pyproject.toml +1 -1
  6. {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/src/gemini_cli_proxy/cli.py +14 -9
  7. {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/src/gemini_cli_proxy/config.py +6 -3
  8. gemini_cli_proxy-1.1.0/src/gemini_cli_proxy/gemini_client.py +336 -0
  9. {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/src/gemini_cli_proxy/models.py +8 -1
  10. {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/src/gemini_cli_proxy/openai_adapter.py +3 -1
  11. {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/src/gemini_cli_proxy/server.py +8 -3
  12. {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/uv.lock +1 -1
  13. gemini_cli_proxy-1.0.3/src/gemini_cli_proxy/gemini_client.py +0 -175
  14. {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/.cursor/rules/coding-conventions.mdc +0 -0
  15. {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/.cursor/rules/project-overview.mdc +0 -0
  16. {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/.python-version +0 -0
  17. {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/LICENSE +0 -0
  18. {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/img/cherry-studio-1.jpg +0 -0
  19. {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/img/cherry-studio-2.jpg +0 -0
  20. {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/src/gemini_cli_proxy/__init__.py +0 -0
  21. {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/tests/__init__.py +0 -0
  22. {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/tests/integration/__init__.py +0 -0
  23. {gemini_cli_proxy-1.0.3 → gemini_cli_proxy-1.1.0}/tests/unit/__init__.py +0 -0
@@ -195,3 +195,6 @@ cython_debug/
195
195
 
196
196
  /local-docs/
197
197
  /local-scripts/
198
+
199
+ # Temporary image files for Gemini CLI
200
+ .gemini-cli-proxy/
@@ -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
 
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gemini-cli-proxy"
3
- version = "1.0.3"
3
+ version = "1.1.0"
4
4
  description = "OpenAI-compatible API wrapper for Gemini CLI"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -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
@@ -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(__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
 
@@ -379,7 +379,7 @@ wheels = [
379
379
 
380
380
  [[package]]
381
381
  name = "gemini-cli-proxy"
382
- version = "1.0.3"
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()