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.
- gemini_calo-0.1.0/PKG-INFO +190 -0
- gemini_calo-0.1.0/README.md +169 -0
- gemini_calo-0.1.0/gemini_calo/__init__.py +7 -0
- gemini_calo-0.1.0/gemini_calo/__main__.py +34 -0
- gemini_calo-0.1.0/gemini_calo/logger.py +12 -0
- gemini_calo-0.1.0/gemini_calo/middlewares/__init__.py +0 -0
- gemini_calo-0.1.0/gemini_calo/middlewares/auth.py +66 -0
- gemini_calo-0.1.0/gemini_calo/middlewares/logging.py +65 -0
- gemini_calo-0.1.0/gemini_calo/proxy.py +156 -0
- gemini_calo-0.1.0/pyproject.toml +30 -0
|
@@ -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,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
|
+
)
|
|
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"
|