gemini-calo 0.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.
@@ -0,0 +1,190 @@
1
+ Metadata-Version: 2.1
2
+ Name: gemini-calo
3
+ Version: 0.1.0
4
+ Summary: A Fastapi Based Proxy for Gemini API
5
+ Home-page: https://github.com/state-alchemists/gemini-calo
6
+ License: AGPL-3.0-or-later
7
+ Author: Go Frendi Gunawan
8
+ Author-email: gofrendiasgard@gmail.com
9
+ Requires-Python: >=3.10,<4.0.0
10
+ Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Dist: fastapi (>=0.116.1,<0.117.0)
16
+ Requires-Dist: uvicorn (>=0.35.0,<0.36.0)
17
+ Project-URL: Documentation, https://github.com/state-alchemists/gemini-calo
18
+ Project-URL: Repository, https://github.com/state-alchemists/gemini-calo
19
+ Description-Content-Type: text/markdown
20
+
21
+ # Gemini Calo
22
+
23
+ **Gemini Calo** is a powerful, yet simple, FastAPI-based proxy server for Google's Gemini API. It provides a seamless way to add a layer of authentication, logging, and monitoring to your Gemini API requests. It's designed to be run as a standalone server or integrated into your existing FastAPI applications.
24
+
25
+ One of its key features is providing an OpenAI-compatible endpoint, allowing you to use Gemini models with tools and libraries that are built for the OpenAI API.
26
+
27
+ ## Key Features
28
+
29
+ * **Authentication:** Secure your Gemini API access with an additional layer of API key authentication.
30
+ * **Request Logging:** Detailed logging of all incoming requests and outgoing responses.
31
+ * **OpenAI Compatibility:** Use Gemini models through an OpenAI-compatible `/v1/chat/completions` endpoint.
32
+ * **Round-Robin API Keys:** Distribute your requests across multiple Gemini API keys.
33
+ * **Easy Integration:** Use it as a standalone server or mount it into your existing FastAPI project.
34
+ * **Extensible:** Easily add your own custom middleware to suit your needs.
35
+
36
+ ## How It's Useful
37
+
38
+ - **Centralized API Key Management:** Instead of hardcoding your Gemini API keys in various clients, you can manage them in one place.
39
+ - **Security:** Protect your expensive Gemini API keys by exposing only a proxy key to your users or client applications.
40
+ - **Monitoring & Observability:** The logging middleware gives you insight into how your API is being used, helping you debug issues and monitor usage patterns.
41
+ - **Seamless Migration:** If you have existing tools that use the OpenAI API, you can switch to using Google's Gemini models without significant code changes.
42
+
43
+ ## Running the Built-in Server
44
+
45
+ You can quickly get the proxy server up and running with just a few steps.
46
+
47
+ ### 1. Installation
48
+
49
+ Install the package using pip:
50
+
51
+ ```bash
52
+ pip install gemini-calo
53
+ ```
54
+
55
+ ### 2. Configuration
56
+
57
+ The server is configured through environment variables. You can create a `.env` file in your working directory to store them.
58
+
59
+ * `GEMINI_CALO_API_KEYS`: A comma-separated list of your Google Gemini API keys. The proxy will rotate through these keys for outgoing requests.
60
+ * `GEMINI_CALO_PROXY_API_KEYS`: (Optional) A comma-separated list of API keys that clients must provide to use the proxy. If not set, the proxy will be open to anyone.
61
+ * `GEMINI_CALO_HTTP_PORT`: (Optional) The port on which the server will run. Defaults to `8000`.
62
+
63
+ **Example `.env` file:**
64
+
65
+ ```
66
+ GEMINI_CALO_API_KEYS=your_gemini_key_1,your_gemini_key_2
67
+ GEMINI_CALO_PROXY_API_KEYS=my_secret_proxy_key_1,my_secret_proxy_key_2
68
+ GEMINI_CALO_HTTP_PORT=8080
69
+ ```
70
+
71
+ ### 3. Running the Server
72
+
73
+ Once configured, you can start the server with the `gemini-calo` command:
74
+
75
+ ```bash
76
+ gemini-calo
77
+ ```
78
+
79
+ The server will start on the configured port (e.g., `http://0.0.0.0:8080`).
80
+
81
+ ## Integrating with an Existing FastAPI Application
82
+
83
+ If you have an existing FastAPI application, you can easily integrate Gemini Calo's proxy functionality into it.
84
+
85
+ ```python
86
+ from fastapi import FastAPI
87
+ from gemini_calo.proxy import GeminiProxyService
88
+ from gemini_calo.middlewares.auth import auth_middleware
89
+ from gemini_calo.middlewares.logging import logging_middleware
90
+ from functools import partial
91
+ import os
92
+
93
+ # Your existing FastAPI app
94
+ app = FastAPI()
95
+
96
+ # 1. Initialize the GeminiProxyService
97
+ gemini_api_keys = os.getenv("GEMINI_CALO_API_KEYS", "").split(",")
98
+ proxy_service = GeminiProxyService(gemini_api_keys=gemini_api_keys)
99
+
100
+ # 2. (Optional) Add Authentication Middleware
101
+ proxy_api_keys = os.getenv("GEMINI_CALO_PROXY_API_KEYS", "").split(",")
102
+ if proxy_api_keys:
103
+ auth_middleware_with_keys = partial(auth_middleware, user_api_key_checker=proxy_api_keys)
104
+ app.middleware("http")(auth_middleware_with_keys)
105
+
106
+ # 3. (Optional) Add Logging Middleware
107
+ app.middleware("http")(logging_middleware)
108
+
109
+ # 4. Mount the Gemini and OpenAI routers
110
+ app.include_router(proxy_service.gemini_router)
111
+ app.include_router(proxy_service.openai_router)
112
+
113
+ @app.get("/health")
114
+ def health_check():
115
+ return {"status": "ok"}
116
+
117
+ # Now you can run your app as usual with uvicorn
118
+ # uvicorn your_app_file:app --reload
119
+ ```
120
+
121
+ ## How the Middleware Works
122
+
123
+ Middleware in FastAPI are functions that process every request before it reaches the specific path operation and every response before it is sent back to the client. Gemini Calo includes two useful middlewares by default.
124
+
125
+ ### Logging Middleware (`logging_middleware`)
126
+
127
+ This middleware logs the details of every incoming request and outgoing response, including headers and body content. This is invaluable for debugging and monitoring. It's designed to handle both standard and streaming responses correctly.
128
+
129
+ ### Authentication Middleware (`auth_middleware`)
130
+
131
+ This middleware protects your proxy endpoints. It checks for an API key in the `Authorization` header (as a Bearer token) or the `x-goog-api-key` header. It then validates this key against the list of keys you provided in the `GEMINI_CALO_PROXY_API_KEYS` environment variable. If the key is missing or invalid, it returns a `401 Unauthorized` error.
132
+
133
+ ### Adding Your Own Middleware
134
+
135
+ Because Gemini Calo is built on FastAPI, you can easily add your own custom middleware. For example, you could add a middleware for rate limiting, CORS, or custom header injection.
136
+
137
+ #### Advanced Middleware: Modifying Request Body and Headers
138
+
139
+ Here is a more advanced example that intercepts a request, modifies its JSON body, adds a new header, and then forwards it to the actual endpoint. This can be useful for injecting default values, adding metadata, or transforming request payloads.
140
+
141
+ **Important:** Reading the request body consumes it. To allow the endpoint to read the body again, we must reconstruct the request with the modified body.
142
+
143
+ ```python
144
+ from fastapi import FastAPI, Request
145
+ from starlette.datastructures import MutableHeaders
146
+ import json
147
+
148
+ app = FastAPI()
149
+
150
+ # This middleware will add a 'user_id' to the request body
151
+ # and a 'X-Request-ID' to the headers.
152
+ async def modify_request_middleware(request: Request, call_next):
153
+ # Get the original request body
154
+ body = await request.body()
155
+
156
+ # Modify headers
157
+ request_headers = MutableHeaders(request.headers)
158
+ request_headers["X-Request-ID"] = "some-unique-id"
159
+
160
+ # Modify body (if it's JSON)
161
+ new_body = body
162
+ if body and request.headers.get("content-type") == "application/json":
163
+ try:
164
+ json_body = json.loads(body)
165
+ # Add or modify a key
166
+ json_body["user_id"] = "injected-user-123"
167
+ new_body = json.dumps(json_body).encode()
168
+ except json.JSONDecodeError:
169
+ # Body is not valid JSON, pass it through
170
+ pass
171
+
172
+ # To pass the modified body and headers, we need to create a new Request object.
173
+ # We do this by defining a new 'receive' channel.
174
+ async def receive():
175
+ return {"type": "http.request", "body": new_body, "more_body": False}
176
+
177
+ # We replace the original request's scope with the modified headers
178
+ request.scope["headers"] = request_headers.raw
179
+
180
+ # Create the new request object and pass it to the next middleware/endpoint
181
+ new_request = Request(request.scope, receive)
182
+ response = await call_next(new_request)
183
+
184
+ return response
185
+
186
+ app.middleware("http")(modify_request_middleware)
187
+
188
+ # ... then add the Gemini Calo proxy and routers as shown above
189
+ ```
190
+
@@ -0,0 +1,169 @@
1
+ # Gemini Calo
2
+
3
+ **Gemini Calo** is a powerful, yet simple, FastAPI-based proxy server for Google's Gemini API. It provides a seamless way to add a layer of authentication, logging, and monitoring to your Gemini API requests. It's designed to be run as a standalone server or integrated into your existing FastAPI applications.
4
+
5
+ One of its key features is providing an OpenAI-compatible endpoint, allowing you to use Gemini models with tools and libraries that are built for the OpenAI API.
6
+
7
+ ## Key Features
8
+
9
+ * **Authentication:** Secure your Gemini API access with an additional layer of API key authentication.
10
+ * **Request Logging:** Detailed logging of all incoming requests and outgoing responses.
11
+ * **OpenAI Compatibility:** Use Gemini models through an OpenAI-compatible `/v1/chat/completions` endpoint.
12
+ * **Round-Robin API Keys:** Distribute your requests across multiple Gemini API keys.
13
+ * **Easy Integration:** Use it as a standalone server or mount it into your existing FastAPI project.
14
+ * **Extensible:** Easily add your own custom middleware to suit your needs.
15
+
16
+ ## How It's Useful
17
+
18
+ - **Centralized API Key Management:** Instead of hardcoding your Gemini API keys in various clients, you can manage them in one place.
19
+ - **Security:** Protect your expensive Gemini API keys by exposing only a proxy key to your users or client applications.
20
+ - **Monitoring & Observability:** The logging middleware gives you insight into how your API is being used, helping you debug issues and monitor usage patterns.
21
+ - **Seamless Migration:** If you have existing tools that use the OpenAI API, you can switch to using Google's Gemini models without significant code changes.
22
+
23
+ ## Running the Built-in Server
24
+
25
+ You can quickly get the proxy server up and running with just a few steps.
26
+
27
+ ### 1. Installation
28
+
29
+ Install the package using pip:
30
+
31
+ ```bash
32
+ pip install gemini-calo
33
+ ```
34
+
35
+ ### 2. Configuration
36
+
37
+ The server is configured through environment variables. You can create a `.env` file in your working directory to store them.
38
+
39
+ * `GEMINI_CALO_API_KEYS`: A comma-separated list of your Google Gemini API keys. The proxy will rotate through these keys for outgoing requests.
40
+ * `GEMINI_CALO_PROXY_API_KEYS`: (Optional) A comma-separated list of API keys that clients must provide to use the proxy. If not set, the proxy will be open to anyone.
41
+ * `GEMINI_CALO_HTTP_PORT`: (Optional) The port on which the server will run. Defaults to `8000`.
42
+
43
+ **Example `.env` file:**
44
+
45
+ ```
46
+ GEMINI_CALO_API_KEYS=your_gemini_key_1,your_gemini_key_2
47
+ GEMINI_CALO_PROXY_API_KEYS=my_secret_proxy_key_1,my_secret_proxy_key_2
48
+ GEMINI_CALO_HTTP_PORT=8080
49
+ ```
50
+
51
+ ### 3. Running the Server
52
+
53
+ Once configured, you can start the server with the `gemini-calo` command:
54
+
55
+ ```bash
56
+ gemini-calo
57
+ ```
58
+
59
+ The server will start on the configured port (e.g., `http://0.0.0.0:8080`).
60
+
61
+ ## Integrating with an Existing FastAPI Application
62
+
63
+ If you have an existing FastAPI application, you can easily integrate Gemini Calo's proxy functionality into it.
64
+
65
+ ```python
66
+ from fastapi import FastAPI
67
+ from gemini_calo.proxy import GeminiProxyService
68
+ from gemini_calo.middlewares.auth import auth_middleware
69
+ from gemini_calo.middlewares.logging import logging_middleware
70
+ from functools import partial
71
+ import os
72
+
73
+ # Your existing FastAPI app
74
+ app = FastAPI()
75
+
76
+ # 1. Initialize the GeminiProxyService
77
+ gemini_api_keys = os.getenv("GEMINI_CALO_API_KEYS", "").split(",")
78
+ proxy_service = GeminiProxyService(gemini_api_keys=gemini_api_keys)
79
+
80
+ # 2. (Optional) Add Authentication Middleware
81
+ proxy_api_keys = os.getenv("GEMINI_CALO_PROXY_API_KEYS", "").split(",")
82
+ if proxy_api_keys:
83
+ auth_middleware_with_keys = partial(auth_middleware, user_api_key_checker=proxy_api_keys)
84
+ app.middleware("http")(auth_middleware_with_keys)
85
+
86
+ # 3. (Optional) Add Logging Middleware
87
+ app.middleware("http")(logging_middleware)
88
+
89
+ # 4. Mount the Gemini and OpenAI routers
90
+ app.include_router(proxy_service.gemini_router)
91
+ app.include_router(proxy_service.openai_router)
92
+
93
+ @app.get("/health")
94
+ def health_check():
95
+ return {"status": "ok"}
96
+
97
+ # Now you can run your app as usual with uvicorn
98
+ # uvicorn your_app_file:app --reload
99
+ ```
100
+
101
+ ## How the Middleware Works
102
+
103
+ Middleware in FastAPI are functions that process every request before it reaches the specific path operation and every response before it is sent back to the client. Gemini Calo includes two useful middlewares by default.
104
+
105
+ ### Logging Middleware (`logging_middleware`)
106
+
107
+ This middleware logs the details of every incoming request and outgoing response, including headers and body content. This is invaluable for debugging and monitoring. It's designed to handle both standard and streaming responses correctly.
108
+
109
+ ### Authentication Middleware (`auth_middleware`)
110
+
111
+ This middleware protects your proxy endpoints. It checks for an API key in the `Authorization` header (as a Bearer token) or the `x-goog-api-key` header. It then validates this key against the list of keys you provided in the `GEMINI_CALO_PROXY_API_KEYS` environment variable. If the key is missing or invalid, it returns a `401 Unauthorized` error.
112
+
113
+ ### Adding Your Own Middleware
114
+
115
+ Because Gemini Calo is built on FastAPI, you can easily add your own custom middleware. For example, you could add a middleware for rate limiting, CORS, or custom header injection.
116
+
117
+ #### Advanced Middleware: Modifying Request Body and Headers
118
+
119
+ Here is a more advanced example that intercepts a request, modifies its JSON body, adds a new header, and then forwards it to the actual endpoint. This can be useful for injecting default values, adding metadata, or transforming request payloads.
120
+
121
+ **Important:** Reading the request body consumes it. To allow the endpoint to read the body again, we must reconstruct the request with the modified body.
122
+
123
+ ```python
124
+ from fastapi import FastAPI, Request
125
+ from starlette.datastructures import MutableHeaders
126
+ import json
127
+
128
+ app = FastAPI()
129
+
130
+ # This middleware will add a 'user_id' to the request body
131
+ # and a 'X-Request-ID' to the headers.
132
+ async def modify_request_middleware(request: Request, call_next):
133
+ # Get the original request body
134
+ body = await request.body()
135
+
136
+ # Modify headers
137
+ request_headers = MutableHeaders(request.headers)
138
+ request_headers["X-Request-ID"] = "some-unique-id"
139
+
140
+ # Modify body (if it's JSON)
141
+ new_body = body
142
+ if body and request.headers.get("content-type") == "application/json":
143
+ try:
144
+ json_body = json.loads(body)
145
+ # Add or modify a key
146
+ json_body["user_id"] = "injected-user-123"
147
+ new_body = json.dumps(json_body).encode()
148
+ except json.JSONDecodeError:
149
+ # Body is not valid JSON, pass it through
150
+ pass
151
+
152
+ # To pass the modified body and headers, we need to create a new Request object.
153
+ # We do this by defining a new 'receive' channel.
154
+ async def receive():
155
+ return {"type": "http.request", "body": new_body, "more_body": False}
156
+
157
+ # We replace the original request's scope with the modified headers
158
+ request.scope["headers"] = request_headers.raw
159
+
160
+ # Create the new request object and pass it to the next middleware/endpoint
161
+ new_request = Request(request.scope, receive)
162
+ response = await call_next(new_request)
163
+
164
+ return response
165
+
166
+ app.middleware("http")(modify_request_middleware)
167
+
168
+ # ... then add the Gemini Calo proxy and routers as shown above
169
+ ```
@@ -0,0 +1,7 @@
1
+ from gemini_calo.proxy import GeminiProxyService
2
+ from gemini_calo.middlewares.auth import auth_middleware
3
+ from gemini_calo.middlewares.logging import logging_middleware
4
+
5
+ assert GeminiProxyService
6
+ assert auth_middleware
7
+ assert logging_middleware
@@ -0,0 +1,34 @@
1
+ import os
2
+ import uvicorn
3
+ from fastapi import FastAPI
4
+ from gemini_calo.proxy import GeminiProxyService
5
+ from gemini_calo.middlewares.logging import logging_middleware
6
+ from gemini_calo.middlewares.auth import auth_middleware
7
+ from functools import partial
8
+
9
+
10
+ def start_server():
11
+ api_keys = os.getenv("GEMINI_CALO_API_KEYS", "").split(",")
12
+ if not api_keys or api_keys == [""]:
13
+ raise ValueError("GEMINI_CALO_API_KEYS not found or empty in .env file")
14
+
15
+ app = FastAPI()
16
+ proxy = GeminiProxyService(gemini_api_keys=api_keys)
17
+
18
+ proxy_api_key = os.getenv("GEMINI_CALO_PROXY_API_KEYS", "")
19
+ if proxy_api_key != "":
20
+ proxy_api_keys = proxy_api_key.split(",")
21
+ auth_middleware_with_key = partial(auth_middleware, user_api_key_checker=proxy_api_keys)
22
+ app.middleware("http")(auth_middleware_with_key)
23
+
24
+ app.middleware("http")(logging_middleware)
25
+ app.include_router(proxy.gemini_router)
26
+ app.include_router(proxy.openai_router)
27
+
28
+ @app.get("/")
29
+ def read_root():
30
+ return {"Hello": "Proxy"}
31
+
32
+ uvicorn.run(
33
+ app, host="0.0.0.0", port=int(os.getenv("GEMINI_CALO_HTTP_PORT", "8000"))
34
+ )
@@ -0,0 +1,12 @@
1
+ import logging
2
+
3
+ logging.basicConfig(
4
+ level=logging.INFO,
5
+ format="%(asctime)s [%(levelname)s] %(message)s",
6
+ handlers=[
7
+ logging.FileHandler("app.log"),
8
+ logging.StreamHandler()
9
+ ]
10
+ )
11
+
12
+ logger = logging.getLogger(__name__)
File without changes
@@ -0,0 +1,66 @@
1
+ import inspect
2
+ from typing import Any, Callable, Coroutine
3
+ from fastapi import Request, Response
4
+ from fastapi.responses import JSONResponse
5
+ from gemini_calo.proxy import GeminiProxyService, REQUEST_TYPE
6
+
7
+
8
+ async def auth_middleware(
9
+ request: Request,
10
+ call_next: Callable[[Request], Coroutine[Any, Any, Response]],
11
+ user_api_key_checker: str | list[str] | Callable[[str], bool | Coroutine[Any, Any, bool]] | None = None
12
+ ) -> Response:
13
+ # If no checker is set, skip this
14
+ if user_api_key_checker is None:
15
+ return await call_next(request)
16
+ # Only apply check for specifics URLs
17
+ request_type = GeminiProxyService.get_request_type(request)
18
+ if request_type == REQUEST_TYPE.OTHER:
19
+ return await call_next(request)
20
+ # Get User API Key from request header
21
+ api_key = _get_request_api_key(request)
22
+ if not api_key:
23
+ return JSONResponse(
24
+ status_code=401,
25
+ content={
26
+ "error": (
27
+ "API key is missing. Provide the API key in "
28
+ "'Authorization: Bearer <key>' or 'x-goog-api-key' header."
29
+ )
30
+ },
31
+ )
32
+ # Check whether User API Key is valid
33
+ is_authorized = await _check_is_authorized(user_api_key_checker, api_key)
34
+ if not is_authorized:
35
+ return JSONResponse(
36
+ status_code=401,
37
+ content={"error": "The provided API Key is not valid."},
38
+ )
39
+ return await call_next(request)
40
+
41
+
42
+ def _get_request_api_key(request: Request) -> str | None:
43
+ api_key = None
44
+ auth_header = request.headers.get("Authorization")
45
+ if auth_header:
46
+ try:
47
+ scheme, _, api_key = auth_header.partition(" ")
48
+ if scheme.lower() == "bearer":
49
+ return api_key
50
+ return None
51
+ except ValueError:
52
+ return None
53
+ return request.headers.get("x-goog-api-key")
54
+
55
+
56
+ async def _check_is_authorized(
57
+ user_api_key_checker: str | list[str] | Callable[[str], bool | Coroutine[Any, Any, bool]] | None,
58
+ api_key: str
59
+ ) -> bool:
60
+ if inspect.iscoroutinefunction(user_api_key_checker):
61
+ return await user_api_key_checker(api_key)
62
+ if callable(user_api_key_checker):
63
+ return user_api_key_checker(api_key)
64
+ if isinstance(user_api_key_checker, str):
65
+ return api_key == user_api_key_checker
66
+ return api_key in user_api_key_checker
@@ -0,0 +1,65 @@
1
+ from fastapi import Request, Response
2
+ from fastapi.responses import StreamingResponse
3
+ from gemini_calo.logger import logger
4
+ import json
5
+
6
+
7
+ async def logging_middleware(request: Request, call_next):
8
+ """
9
+ Logs incoming request and outgoing response details.
10
+ Handles both regular and streaming responses.
11
+ """
12
+ logger.info(f"Incoming request: {request.method} {request.url}")
13
+ logger.debug(f"Request headers: {request.headers}")
14
+
15
+ # Read the body and re-create the request to make the body available again
16
+ body = await request.body()
17
+ if body:
18
+ try:
19
+ logger.debug(f"Request body: {json.loads(body)}")
20
+ except json.JSONDecodeError:
21
+ logger.debug(f"Request body (non-JSON): {body.decode(errors='ignore')}")
22
+
23
+ async def receive():
24
+ return {"type": "http.request", "body": body}
25
+
26
+ request = Request(request.scope, receive)
27
+
28
+ response = await call_next(request)
29
+
30
+ logger.info(f"Outgoing response: Status {response.status_code}")
31
+ logger.debug(f"Response headers: {response.headers}")
32
+
33
+ if isinstance(response, StreamingResponse):
34
+ # For streaming responses, we need to wrap the iterator to log chunks
35
+ original_iterator = response.body_iterator
36
+
37
+ async def logging_iterator():
38
+ full_body = b""
39
+ async for chunk in original_iterator:
40
+ full_body += chunk
41
+ yield chunk
42
+ try:
43
+ logger.debug(f"Response body (stream): {json.loads(full_body)}")
44
+ except json.JSONDecodeError:
45
+ logger.debug(f"Response body (stream, non-JSON): {full_body.decode(errors='ignore')}")
46
+
47
+ return StreamingResponse(
48
+ logging_iterator(),
49
+ status_code=response.status_code,
50
+ headers=dict(response.headers),
51
+ media_type=response.media_type,
52
+ )
53
+
54
+ # For non-streaming responses
55
+ response_body = b""
56
+ if hasattr(response, "body"):
57
+ response_body = response.body
58
+
59
+ if response_body:
60
+ try:
61
+ logger.debug(f"Response body: {json.loads(response_body)}")
62
+ except json.JSONDecodeError:
63
+ logger.debug(f"Response body (non-JSON): {response_body.decode(errors='ignore')}")
64
+
65
+ return response
@@ -0,0 +1,156 @@
1
+ from typing import Any
2
+ from fastapi import Request, Response, APIRouter
3
+ from fastapi.responses import StreamingResponse
4
+ from gemini_calo.logger import logger
5
+ from enum import Enum
6
+ import httpx
7
+
8
+
9
+ class REQUEST_TYPE(Enum):
10
+ OPENAI_COMPLETION: str = "openai-completion"
11
+ OPENAI_EMBEDDING: str = "openai-embedding"
12
+ GEMINI_COMPLETION: str = "gemini-completion"
13
+ GEMINI_STREAMING_COMPLETION: str = "gemini-streaming-completion"
14
+ GEMINI_EMBEDDING: str = "gemini-embedding"
15
+ OTHER: str = "other"
16
+
17
+
18
+ class GeminiProxyService:
19
+ def __init__(
20
+ self,
21
+ base_url: str = "https://generativelanguage.googleapis.com",
22
+ gemini_api_keys: list[str] = [],
23
+ ):
24
+ self._gemini_api_keys = gemini_api_keys
25
+ self._current_gemini_api_key_index = 0
26
+ self._base_url = base_url
27
+ self.openai_router = APIRouter()
28
+ self.gemini_router = APIRouter()
29
+ self._add_routes()
30
+
31
+ def _add_routes(self):
32
+ self.openai_router.add_api_route(
33
+ "/v1/chat/completions",
34
+ self.forward_openai_request,
35
+ methods=["POST"],
36
+ response_model=Any,
37
+ # response_class=Response,
38
+ )
39
+ self.openai_router.add_api_route(
40
+ "/v1/embeddings",
41
+ self.forward_openai_request,
42
+ methods=["POST"],
43
+ response_model=Any,
44
+ # response_class=Response,
45
+ )
46
+ self.gemini_router.add_api_route(
47
+ "/v1beta/models/{model_name:path}:generateContent",
48
+ self.forward_gemini_request,
49
+ methods=["POST"],
50
+ response_model=Any,
51
+ # response_class=Response,
52
+ )
53
+ self.gemini_router.add_api_route(
54
+ "/v1beta/models/{model_name:path}:streamGenerateContent",
55
+ self.forward_gemini_request,
56
+ methods=["POST"],
57
+ response_model=Any,
58
+ # response_class=Response,
59
+ )
60
+ self.gemini_router.add_api_route(
61
+ "/v1beta/models/{model_name:path}:embedContent",
62
+ self.forward_gemini_request,
63
+ methods=["POST"],
64
+ response_model=Any,
65
+ # response_class=Response,
66
+ )
67
+
68
+ @classmethod
69
+ def get_request_type(cls, request: Request) -> str:
70
+ if request.url.path == "/v1/chat/completions":
71
+ return REQUEST_TYPE.OPENAI_COMPLETION
72
+ if request.url.path == "/v1/embeddings":
73
+ return REQUEST_TYPE.OPENAI_EMBEDDING
74
+ if request.url.path.startswith("/v1beta/models/"):
75
+ if request.url.path.endswith(":generateContent"):
76
+ return REQUEST_TYPE.GEMINI_COMPLETION
77
+ if request.url.path.endswith(":streamGenerateContent"):
78
+ return REQUEST_TYPE.GEMINI_STREAMING_COMPLETION
79
+ if request.url.path.endswith(":embedContent"):
80
+ return REQUEST_TYPE.GEMINI_EMBEDDING
81
+ return REQUEST_TYPE.OTHER
82
+
83
+ def get_gemini_api_key(self):
84
+ key = self._gemini_api_keys[self._current_gemini_api_key_index]
85
+ self._current_gemini_api_key_index += 1
86
+ self._current_gemini_api_key_index %= len(self._gemini_api_keys)
87
+ return key
88
+
89
+ def get_httpx_client(self):
90
+ return httpx.AsyncClient(base_url=self._base_url)
91
+
92
+ async def forward_openai_request(self, request: Request) -> Response:
93
+ """Forward openai request"""
94
+ api_key = self.get_gemini_api_key()
95
+ client = self.get_httpx_client()
96
+ url = httpx.URL(
97
+ scheme="https",
98
+ path=request.url.path,
99
+ query=request.url.query.encode("utf-8"),
100
+ )
101
+ headers = {
102
+ "Content-Type": "application/json",
103
+ "Authorization": f"Bearer {api_key}",
104
+ }
105
+ logger.info(f"Forwarding OpenAI request to: {url}")
106
+ req = client.build_request(
107
+ request.method,
108
+ url,
109
+ headers=headers,
110
+ content=request.stream(),
111
+ timeout=300.0,
112
+ )
113
+ response = await client.send(req, stream=True)
114
+ if hasattr(response, "aiter_raw"):
115
+ return StreamingResponse(
116
+ response.aiter_raw(),
117
+ status_code=response.status_code,
118
+ headers=dict(response.headers),
119
+ )
120
+ response_body = await response.aread()
121
+ return Response(
122
+ content=response_body,
123
+ status_code=response.status_code,
124
+ headers=dict(response.headers),
125
+ )
126
+
127
+ async def forward_gemini_request(self, request: Request) -> Response:
128
+ """Forward gemini request"""
129
+ api_key = self.get_gemini_api_key()
130
+ client = self.get_httpx_client()
131
+ url = httpx.URL(path=request.url.path, query=request.url.query.encode("utf-8"))
132
+ headers = {
133
+ "Content-Type": "application/json",
134
+ "x-goog-api-key": api_key,
135
+ }
136
+ logger.info(f"Forwarding request to: {url}")
137
+ req = client.build_request(
138
+ request.method,
139
+ url,
140
+ headers=headers,
141
+ content=request.stream(),
142
+ timeout=300.0,
143
+ )
144
+ response = await client.send(req, stream=True)
145
+ if hasattr(response, "aiter_raw"):
146
+ return StreamingResponse(
147
+ response.aiter_raw(),
148
+ status_code=response.status_code,
149
+ headers=dict(response.headers),
150
+ )
151
+ response_body = await response.aread()
152
+ return Response(
153
+ content=response_body,
154
+ status_code=response.status_code,
155
+ headers=dict(response.headers),
156
+ )
@@ -0,0 +1,30 @@
1
+ [tool.poetry]
2
+ name = "gemini-calo"
3
+ version = "0.1.0"
4
+ description = "A Fastapi Based Proxy for Gemini API"
5
+ authors = ["Go Frendi Gunawan <gofrendiasgard@gmail.com>"]
6
+ license = "AGPL-3.0-or-later"
7
+ readme = "README.md"
8
+ homepage = "https://github.com/state-alchemists/gemini-calo"
9
+ repository = "https://github.com/state-alchemists/gemini-calo"
10
+ documentation = "https://github.com/state-alchemists/gemini-calo"
11
+
12
+ [tool.poetry.dependencies]
13
+ python = ">=3.10,<4.0.0"
14
+ fastapi = "^0.116.1"
15
+ uvicorn = "^0.35.0"
16
+
17
+ [tool.poetry.group.dev.dependencies]
18
+ pytest = "~8.3.5"
19
+ httpx = "~0.28.1"
20
+ pytest-httpx = "~0.35.0"
21
+
22
+ [tool.pytest.ini_options]
23
+ asyncio_default_fixture_loop_scope = "function"
24
+
25
+ [tool.poetry.scripts]
26
+ gemini-calo = "gemini_calo.__main__:start_server"
27
+
28
+ [build-system]
29
+ requires = ["poetry-core>=1.0.0"]
30
+ build-backend = "poetry.core.masonry.api"