7 Commits
v0.0.3 ... main

Author SHA1 Message Date
5b9f3696a0 Tag v0.0.6 2025-07-17 22:42:11 -05:00
825788eec3 build(deps): update @google/gemini-cli-core to ^0.1.12
- Update gemini-cli-core dependency to latest version
- Refactor imports in chatwrapper.ts to use direct module imports
- Change auth type to use string literal 'oauth-personal'
2025-07-17 22:41:03 -05:00
f83a1b3957 Tag v0.0.5 2025-06-30 18:31:19 -05:00
af3a52bac6 feat(api): add model field and root endpoint
Add a model field to the gemini request mapping and implement a new
root endpoint that returns a plain text status message.
2025-06-30 18:30:51 -05:00
e7eb40ba4e Added donation section to README 2025-06-30 16:53:44 -05:00
0932a9a3e5 Tag v0.0.4 2025-06-30 16:46:43 -05:00
f286ab3d38 feat(auth): add auto-generation of oauth credentials
Implement functionality to create the oauth_creds.json file from
environment variables (ACCESS_TOKEN, REFRESH_TOKEN, EXPIRY_DATE)
if the file is missing. Also update documentation, docker-compose,
and build scripts to support this new feature.
2025-06-30 16:45:49 -05:00
12 changed files with 706 additions and 304 deletions

31
.dockerignore Normal file
View File

@@ -0,0 +1,31 @@
# Dependencies
node_modules/
npm-debug.log
yarn-debug.log*
yarn-error.log*
# Environment variables
.env
.env.example
# Build output
dist/
build/
coverage/
# Development
profile/
*.test.ts
*.spec.ts
# Version control
.git/
.gitignore
# IDE
.vscode/
.idea/
# Docker
Dockerfile
docker-compose.yml

View File

@@ -1,3 +1,10 @@
PORT=11434
VERBOSE=false
API_KEY=MY0P3NA1K3Y
API_KEY=MY0P3NA1K3Y
ACCESS_TOKEN=MYACC3SS_T0K3N
REFRESH_TOKEN=MYR3FR3SH_T0K3N
EXPIRY_DATE=1234567890
# Docker
DOCKER_REGISTRY=
DOCKER_REGISTRY_USER=
DOCKER_HUB_USER=

26
Dockerfile Normal file
View File

@@ -0,0 +1,26 @@
# Use an official Node.js runtime as a parent image
FROM node:22.15-slim
# Set the working directory in the container
WORKDIR /usr/src/app
# Create directory for oauth credentials
RUN mkdir -p /root/.gemini
# Copy package.json and package-lock.json to the working directory
COPY package*.json ./
# Install any needed packages specified in package.json
RUN npm install
# Bundle app source
COPY . .
# Build the typescript code
RUN npm run build
# Make port 4343 available to the world outside this container
EXPOSE 4343
# Define the command to run the app
CMD [ "npm", "start" ]

215
README.md
View File

@@ -1,76 +1,163 @@
# Gemini ↔︎ OpenAI Proxy
# Gemini CLI OpenAI API Proxy
Serve **Google Gemini 2.5 Pro** (or Flash) through an **OpenAI-compatible API**.
Plug-and-play with clients that already speak OpenAI—SillyTavern, llama.cpp, LangChain, the VS Code *Cline* extension, etc.
This project provides a lightweight proxy server that translates OpenAI API requests to the Google Gemini API, utilizing the `@google/gemini-cli` for authentication and request handling.
---
## Features
## ✨ Features
* **OpenAI API Compatibility:** Acts as a drop-in replacement for services that use the OpenAI API format.
* **Google Gemini Integration:** Leverages the power of Google's Gemini models.
* **Authentication:** Uses `gemini-cli` for secure OAuth2 authentication with Google.
* **Docker Support:** Includes `Dockerfile` and `docker-compose.yml` for easy containerized deployment.
* **Hugging Face Spaces Ready:** Can be easily deployed as a Hugging Face Space.
| ✔ | Feature | Notes |
|---|---------|-------|
| `/v1/chat/completions` | Non-stream & stream (SSE) | Works with curl, ST, LangChain… |
| Vision support | `image_url` → Gemini `inlineData` | |
| Function / Tool calling | OpenAI “functions” → Gemini Tool Registry | |
| Reasoning / chain-of-thought | Sends `enable_thoughts:true`, streams `<think>` chunks | ST shows grey bubbles |
| 1 M-token context | Proxy auto-lifts Gemini CLIs default 200 k cap | |
| CORS | Enabled (`*`) by default | Ready for browser apps |
| Zero external deps | Node 22 + TypeScript only | No Express |
## Support the Project
---
If you find this project useful, consider supporting its development:
## 🚀 Quick start (local)
[![Donate using Liberapay][liberapay-logo]][liberapay-link]
[liberapay-logo]: https://liberapay.com/assets/widgets/donate.svg "Liberapay Logo"
[liberapay-link]: https://liberapay.com/sfiorini/donate
## Prerequisites
Before you begin, ensure you have the following installed:
* [Node.js](https://nodejs.org/) (v18 or higher)
* [npm](https://www.npmjs.com/)
* [Docker](https://www.docker.com/) (for containerized deployment)
* [Git](https://git-scm.com/)
## Local Installation and Setup
1. **Clone the repository:**
```bash
git clone https://github.com/your-username/gemini-cli-openai-api.git
cd gemini-cli-openai-api
```
2. **Install project dependencies:**
```bash
npm install
```
3. **Install the Gemini CLI and Authenticate:**
This is a crucial step to authenticate with your Google account and generate the necessary credentials.
```bash
npm install -g @google/gemini-cli
gemini auth login
```
Follow the on-screen instructions to log in with your Google account. This will create a file at `~/.gemini/oauth_creds.json` containing your authentication tokens.
4. **Configure Environment Variables:**
Create a `.env` file by copying the example file:
```bash
cp .env.example .env
```
Open the `.env` file and set the following variables:
* `PORT`: The port the server will run on (default: `11434`).
* `API_KEY`: A secret key to protect your API endpoint. You can generate a strong random string for this.
## Running the Project
### Development Mode
To run the server in development mode with hot-reloading:
```bash
git clone https://huggingface.co/engineofperplexity/gemini-openai-proxy
cd gemini-openai-proxy
npm ci # install deps & ts-node
npm run dev
```
# launch on port 11434
npx ts-node src/server.ts
Optional env vars
PORT=3000change listen port
GEMINI_API_KEY=<key>use your own key
The server will be accessible at `http://localhost:11434` (or the port you specified).
Minimal curl test
bash
Copy
Edit
curl -X POST http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "gemini-2.5-pro-latest",
"messages":[{"role":"user","content":"Hello Gemini!"}]
}'
SillyTavern settings
Field Value
API Base URL http://127.0.0.1:11434/v1
Model gemini-2.5-pro-latest
Streaming On
Reasoning On → grey <think> lines appear
### Production Mode
🐳 Docker
bash
Copy
Edit
# build once
docker build -t gemini-openai-proxy .
To build and run the server in production mode:
# run
docker run -p 11434:11434 \
-e GEMINI_API_KEY=$GEMINI_API_KEY \
gemini-openai-proxy
🗂 Project layout
pgsql
Copy
Edit
src/
server.ts minimalist HTTP server
mapper.ts OpenAI ⇄ Gemini transforms
chatwrapper.ts thin wrapper around @google/genai
remoteimage.ts fetch + base64 for vision
package.json deps & scripts
Dockerfile
README.md
📜 License
MIT free for personal & commercial use.
```bash
npm run build
npm start
```
## Docker Deployment
### Using Docker Compose
The easiest way to deploy the project with Docker is by using the provided `docker-compose.yml` file.
1. **Authentication:**
The Docker container needs access to your OAuth credentials. You have two options:
* **Option A (Recommended): Mount the credentials file.**
Uncomment the `volumes` section in `docker-compose.yml` to mount your local `oauth_creds.json` file into the container.
```yaml
volumes:
- ~/.gemini/oauth_creds.json:/root/.gemini/oauth_creds.json
```
* **Option B: Use environment variables.**
If you cannot mount the file, you can set the `ACCESS_TOKEN`, `REFRESH_TOKEN`, and `EXPIRY_DATE` environment variables in the `docker-compose.yml` file. You can get these values from your `~/.gemini/oauth_creds.json` file.
2. **Configure `docker-compose.yml`:**
Open `docker-compose.yml` and set the `API_KEY` and other environment variables as needed.
3. **Start the container:**
```bash
docker-compose up -d
```
The server will be running on the port specified in the `ports` section of the `docker-compose.yml` file (e.g., `4343`).
### Building the Docker Image Manually
If you need to build the Docker image yourself:
```bash
docker build -t gemini-cli-openai-api .
```
Then you can run the container with the appropriate environment variables and volume mounts.
## Hugging Face Spaces Deployment
You can deploy this project as a Docker Space on Hugging Face.
1. **Create a new Space:**
* Go to [huggingface.co/new-space](https://huggingface.co/new-space).
* Choose a name for your space.
* Select "Docker" as the Space SDK.
* Choose "From scratch".
* Create the space.
2. **Upload the project files:**
* Upload all the project files (including the `Dockerfile`) to your new Hugging Face Space repository. You can do this via the web interface or by cloning the space's repository and pushing the files.
3. **Configure Secrets:**
* In your Space's settings, go to the "Secrets" section.
* Add the following secrets. You can get the values for the first three from your `~/.gemini/oauth_creds.json` file.
* `ACCESS_TOKEN`: Your Google OAuth access token.
* `REFRESH_TOKEN`: Your Google OAuth refresh token.
* `EXPIRY_DATE`: The expiry date of your access token.
* `API_KEY`: The secret API key you want to use to protect your endpoint.
* `PORT`: The port the application should run on inside the container (e.g., `7860`, which is a common default for Hugging Face Spaces).
4. **Update Dockerfile (if necessary):**
* The provided `Dockerfile` exposes port `4343`. If Hugging Face requires a different port (like `7860`), you may need to update the `EXPOSE` instruction in the `Dockerfile`.
5. **Deploy:**
* Hugging Face Spaces will automatically build and deploy your Docker container when you push changes to the repository. Check the "Logs" to monitor the build and deployment process.
Your Gemini-powered OpenAI proxy will now be running on your Hugging Face Space!

20
docker-compose.yml Normal file
View File

@@ -0,0 +1,20 @@
version: '3.8'
services:
gemini-cli-openai-api:
container_name: gemini-cli-openai-api
image: sfiorini/gemini-cli-openai-api:latest
ports:
- "4343:4343"
# Enable sharing a pre existing OAuth credentials file
# to avoid the need to set environment variables.
# volumes:
# - ~/.gemini/oauth_creds.json:/root/.gemini/oauth_creds.json
environment:
- TZ=America/Chicago
- PORT=4343
- VERBOSE=false
- API_KEY=MY0P3NA1K3Y
- ACCESS_TOKEN=MYACC3SS_T0K3N
- REFRESH_TOKEN=MYR3FR3SH_T0K3N
- EXPIRY_DATE=1234567890
restart: unless-stopped

435
package-lock.json generated
View File

@@ -1,15 +1,15 @@
{
"name": "gemini-cli-openai-api",
"version": "0.0.3",
"version": "0.0.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gemini-cli-openai-api",
"version": "0.0.3",
"version": "0.0.6",
"license": "MIT",
"dependencies": {
"@google/gemini-cli-core": "^0.1.7",
"@google/gemini-cli-core": "^0.1.12",
"consola": "^3.4.2",
"dotenv": "^17.0.0",
"zod": "^3.25.67"
@@ -769,11 +769,11 @@
}
},
"node_modules/@google/gemini-cli-core": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/@google/gemini-cli-core/-/gemini-cli-core-0.1.7.tgz",
"integrity": "sha512-V3KYamCruqhBSoWNvWm5MJn6EwwZVv/129h0f2SFVfgJP759QVAvcnT4nGq18Jf5nNqDkq01Uug3yR/NfGJN+g==",
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/@google/gemini-cli-core/-/gemini-cli-core-0.1.12.tgz",
"integrity": "sha512-oI6DYfzHztROW65b0kzIBP9Lu3jgP9LCE203A60tQY8JRBWtLyStYa7Wn0RQNl8v/Ym1C6xYcuFcJVEs1tFUIQ==",
"dependencies": {
"@google/genai": "^1.4.0",
"@google/genai": "1.8.0",
"@modelcontextprotocol/sdk": "^1.11.0",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/exporter-logs-otlp-grpc": "^0.52.0",
@@ -783,41 +783,52 @@
"@opentelemetry/sdk-node": "^0.52.0",
"@types/glob": "^8.1.0",
"@types/html-to-text": "^9.0.4",
"ajv": "^8.17.1",
"diff": "^7.0.0",
"dotenv": "^16.4.7",
"gaxios": "^6.1.1",
"dotenv": "^17.1.0",
"gaxios": "^7.1.1",
"glob": "^10.4.5",
"google-auth-library": "^9.11.0",
"html-to-text": "^9.0.5",
"ignore": "^7.0.0",
"micromatch": "^4.0.8",
"open": "^10.1.2",
"shell-quote": "^1.8.2",
"shell-quote": "^1.8.3",
"simple-git": "^3.28.0",
"strip-ansi": "^7.1.0",
"undici": "^7.10.0",
"ws": "^8.18.0"
},
"engines": {
"node": ">=18"
"node": ">=20"
}
},
"node_modules/@google/gemini-cli-core/node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
"node_modules/@google/gemini-cli-core/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"url": "https://dotenvx.com"
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@google/gemini-cli-core/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
"node_modules/@google/genai": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.7.0.tgz",
"integrity": "sha512-s/OZLkrIfBwc+SFFaZoKdEogkw4in0YRTGc4Q483jnfchNBWzrNe560eZEfGJHQRPn6YfzJgECCx0sqEOMWvYw==",
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.8.0.tgz",
"integrity": "sha512-n3KiMFesQCy2R9iSdBIuJ0JWYQ1HZBJJkmt4PPZMGZKvlgHhBAGw1kUMyX+vsAIzprN3lK45DI755lm70wPOOg==",
"license": "Apache-2.0",
"dependencies": {
"google-auth-library": "^9.14.2",
@@ -1053,9 +1064,9 @@
"license": "MIT"
},
"node_modules/@modelcontextprotocol/sdk": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.13.2.tgz",
"integrity": "sha512-Vx7qOcmoKkR3qhaQ9qf3GxiVKCEu+zfJddHv6x3dY/9P6+uIwJnmuAur5aB+4FDXf41rRrDnOEGkviX5oYZ67w==",
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.16.0.tgz",
"integrity": "sha512-8ofX7gkZcLj9H9rSd50mCgm3SSF8C7XoclxJuLoV0Cz3rEQ1tv9MZRYYvJtm9n1BiEQQMzSmE/w2AEkNacLYfg==",
"license": "MIT",
"dependencies": {
"ajv": "^6.12.6",
@@ -1063,6 +1074,7 @@
"cors": "^2.8.5",
"cross-spawn": "^7.0.5",
"eventsource": "^3.0.2",
"eventsource-parser": "^3.0.0",
"express": "^5.0.1",
"express-rate-limit": "^7.5.0",
"pkce-challenge": "^5.0.0",
@@ -2371,27 +2383,6 @@
"node": ">= 0.6"
}
},
"node_modules/accepts/node_modules/mime-db": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/accepts/node_modules/mime-types": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
"license": "MIT",
"dependencies": {
"mime-db": "^1.54.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -2424,9 +2415,9 @@
}
},
"node_modules/agent-base": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
"license": "MIT",
"engines": {
"node": ">= 14"
@@ -2540,9 +2531,9 @@
"license": "MIT"
},
"node_modules/bignumber.js": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz",
"integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==",
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
"integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
"license": "MIT",
"engines": {
"node": "*"
@@ -3014,6 +3005,15 @@
"node": ">= 8"
}
},
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
@@ -3175,9 +3175,9 @@
}
},
"node_modules/dotenv": {
"version": "17.0.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.0.0.tgz",
"integrity": "sha512-A0BJ5lrpJVSfnMMXjmeO0xUnoxqsBHWCoqqTnGwGYVdnctqXXUEhJOO7LxmgxJon9tEZFGpe0xPRX0h2v3AANQ==",
"version": "17.2.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.0.tgz",
"integrity": "sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
@@ -3803,27 +3803,6 @@
"express": ">= 4.11"
}
},
"node_modules/express/node_modules/mime-db": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express/node_modules/mime-types": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
"license": "MIT",
"dependencies": {
"mime-db": "^1.54.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/exsolve": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz",
@@ -3873,6 +3852,22 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "BSD-3-Clause"
},
"node_modules/fastq": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
@@ -3893,6 +3888,29 @@
"walk-up-path": "^4.0.0"
}
},
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
}
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -4005,6 +4023,18 @@
"node": ">=18.3.0"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"license": "MIT",
"dependencies": {
"fetch-blob": "^3.1.2"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -4048,6 +4078,34 @@
}
},
"node_modules/gaxios": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.1.tgz",
"integrity": "sha512-Odju3uBUJyVCkW64nLD4wKLhbh93bh6vIg/ZIXkWiLPBrdgtc65+tls/qml+un3pr6JqYVFDZbbmLDQT68rTOQ==",
"license": "Apache-2.0",
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"node-fetch": "^3.3.2"
},
"engines": {
"node": ">=18"
}
},
"node_modules/gcp-metadata": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
"integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
"license": "Apache-2.0",
"dependencies": {
"gaxios": "^6.1.1",
"google-logging-utils": "^0.0.2",
"json-bigint": "^1.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/gcp-metadata/node_modules/gaxios": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
"integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
@@ -4063,18 +4121,24 @@
"node": ">=14"
}
},
"node_modules/gcp-metadata": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
"integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
"license": "Apache-2.0",
"node_modules/gcp-metadata/node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"gaxios": "^6.1.1",
"google-logging-utils": "^0.0.2",
"json-bigint": "^1.0.0"
"whatwg-url": "^5.0.0"
},
"engines": {
"node": ">=14"
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/get-caller-file": {
@@ -4217,6 +4281,42 @@
"node": ">=14"
}
},
"node_modules/google-auth-library/node_modules/gaxios": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
"integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
"license": "Apache-2.0",
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"is-stream": "^2.0.0",
"node-fetch": "^2.6.9",
"uuid": "^9.0.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/google-auth-library/node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/google-logging-utils": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
@@ -4265,6 +4365,42 @@
"node": ">=14.0.0"
}
},
"node_modules/gtoken/node_modules/gaxios": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
"integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
"license": "Apache-2.0",
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"is-stream": "^2.0.0",
"node-fetch": "^2.6.9",
"uuid": "^9.0.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/gtoken/node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -4880,6 +5016,27 @@
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
"license": "MIT",
"dependencies": {
"mime-db": "^1.54.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@@ -4942,24 +5099,42 @@
"node": ">= 0.6"
}
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"deprecated": "Use your platform's native DOMException instead",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "4.x || >=6.0.0"
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/node-fetch-native": {
@@ -5466,6 +5641,15 @@
"node": ">=0.10.0"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-in-the-middle": {
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz",
@@ -5725,27 +5909,6 @@
"node": ">= 18"
}
},
"node_modules/send/node_modules/mime-db": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/send/node_modules/mime-types": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
"license": "MIT",
"dependencies": {
"mime-db": "^1.54.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/serve-static": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
@@ -6311,27 +6474,6 @@
"node": ">= 0.6"
}
},
"node_modules/type-is/node_modules/mime-db": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/type-is/node_modules/mime-types": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
"license": "MIT",
"dependencies": {
"mime-db": "^1.54.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
@@ -6450,6 +6592,15 @@
"node": "20 || >=22"
}
},
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
@@ -6621,9 +6772,9 @@
"license": "ISC"
},
"node_modules/ws": {
"version": "8.18.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"

View File

@@ -1,18 +1,22 @@
{
"name": "gemini-cli-openai-api",
"version": "0.0.3",
"version": "0.0.6",
"main": "server.ts",
"scripts": {
"build": "tsdown",
"bump-release": "bumpp",
"dev": "tsx watch ./src/server.ts",
"docker": "npm run docker:build && npm run docker:push",
"docker:build": "npm run docker:build:version && npm run docker:tag:latest",
"docker:build": "npm run docker:build:version && npm run docker:tag:latest && npm run docker:build:du:version && npm run docker:tag:du:latest",
"docker:build:version": "dotenv -- bash -c 'docker build -t $DOCKER_REGISTRY/$DOCKER_REGISTRY_USER/$npm_package_name:v$npm_package_version .'",
"docker:push": "npm run docker:push:version && npm run docker:push:latest",
"docker:build:du:version": "dotenv -- bash -c 'docker build -t $DOCKER_HUB_USER/$npm_package_name:v$npm_package_version .'",
"docker:push": "npm run docker:push:version && npm run docker:push:latest && npm run docker:push:du:version && npm run docker:push:du:latest",
"docker:push:latest": "dotenv -- bash -c 'docker push $DOCKER_REGISTRY/$DOCKER_REGISTRY_USER/$npm_package_name:latest'",
"docker:push:du:latest": "dotenv -- bash -c 'docker push $DOCKER_HUB_USER/$npm_package_name:latest'",
"docker:push:version": "dotenv -- bash -c 'docker push $DOCKER_REGISTRY/$DOCKER_REGISTRY_USER/$npm_package_name:v$npm_package_version'",
"docker:push:du:version": "dotenv -- bash -c 'docker push $DOCKER_HUB_USER/$npm_package_name:v$npm_package_version'",
"docker:tag:latest": "dotenv -- bash -c 'docker tag $DOCKER_REGISTRY/$DOCKER_REGISTRY_USER/$npm_package_name:v$npm_package_version $DOCKER_REGISTRY/$DOCKER_REGISTRY_USER/$npm_package_name:latest'",
"docker:tag:du:latest": "dotenv -- bash -c 'docker tag $DOCKER_HUB_USER/$npm_package_name:v$npm_package_version $DOCKER_HUB_USER/$npm_package_name:latest'",
"start": "node ./dist/server.js",
"knip": "knip",
"lint": "eslint --fix ."
@@ -22,7 +26,7 @@
"license": "MIT",
"description": "",
"dependencies": {
"@google/gemini-cli-core": "^0.1.7",
"@google/gemini-cli-core": "^0.1.12",
"consola": "^3.4.2",
"dotenv": "^17.0.0",
"zod": "^3.25.67"

View File

@@ -3,6 +3,45 @@
*/
import http from 'http';
import { config } from './config';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import consola from 'consola';
/**
* Ensures that the OAuth credentials file exists if the required environment
* variables are present.
*/
export async function ensureOAuthCredentials(): Promise<void> {
const geminiDir = path.join(os.homedir(), '.gemini');
const credsPath = path.join(geminiDir, 'oauth_creds.json');
try {
await fs.access(credsPath);
consola.info(`OAuth credentials file already exists at ${credsPath}`);
} catch {
consola.info(`OAuth credentials file not found at ${credsPath}.`);
if (config.ACCESS_TOKEN && config.REFRESH_TOKEN && config.EXPIRY_DATE) {
consola.info('Creating OAuth credentials file' +
' from environment variables.');
await fs.mkdir(geminiDir, { recursive: true });
const creds = {
access_token: config.ACCESS_TOKEN,
refresh_token: config.REFRESH_TOKEN,
token_type: 'Bearer',
expiry_date: config.EXPIRY_DATE,
};
await fs.writeFile(credsPath, JSON.stringify(creds, null, 2));
consola.info(`Successfully created ${credsPath}`);
} else {
consola.error(
'OAuth credentials file is missing and one or more required ' +
'environment variables: ACCESS_TOKEN, REFRESH_TOKEN, EXPIRY_DATE.',
);
throw new Error('Missing OAuth credentials or environment variables.');
}
}
}
/**
* Checks for API key authentication.

View File

@@ -2,17 +2,14 @@
* @fileoverview This file provides a wrapper around the Gemini API, handling
* content generation, model management, and retry logic.
*/
import {
import {
AuthType,
createContentGeneratorConfig,
createContentGenerator,
ContentGenerator,
} from '@google/gemini-cli-core/dist/src/core/contentGenerator.js';
import {
Config,
DEFAULT_GEMINI_MODEL,
DEFAULT_GEMINI_FLASH_MODEL,
} from '@google/gemini-cli-core/dist/src/config/models.js';
DEFAULT_GEMINI_FLASH_MODEL } from '@google/gemini-cli-core';
import { Content, GeminiResponse, Model } from './types.js';
import consola from 'consola';
@@ -64,9 +61,11 @@ function getGenerator(
const generatorPromise = (async () => {
const cfg = await createContentGeneratorConfig(
modelToUse,
AuthType.LOGIN_WITH_GOOGLE_PERSONAL,
'oauth-personal' as AuthType, // Use OAuth for personal access
);
const generator = await createContentGenerator(cfg);
// Using core's createContentGenerator with minimal valid arguments
const generator =
await createContentGenerator(cfg, {} as unknown as Config);
return { generator, model: cfg.model };
})();
@@ -206,3 +205,4 @@ export function listModels(): Model[] {
// throw new Error('Embeddings endpoint not implemented yet.');
// }

View File

@@ -29,4 +29,21 @@ export const config = {
* @type {string | undefined}
*/
API_KEY: process.env.API_KEY,
/**
* The access token for OAuth.
* @type {string | undefined}
*/
ACCESS_TOKEN: process.env.ACCESS_TOKEN,
/**
* The refresh token for OAuth.
* @type {string | undefined}
*/
REFRESH_TOKEN: process.env.REFRESH_TOKEN,
/**
* The expiry date for the access token.
* @type {number | undefined}
*/
EXPIRY_DATE: process.env.EXPIRY_DATE
? Number(process.env.EXPIRY_DATE)
: undefined,
};

View File

@@ -118,6 +118,7 @@ export async function mapRequest(body: RequestBody) {
return {
geminiReq: {
model: body.model,
contents,
generationConfig,
stream: body.stream,

View File

@@ -8,7 +8,7 @@ import { listModels, sendChat, sendChatStream } from './chatwrapper';
import { mapRequest, mapResponse, mapStreamChunk } from './mapper.js';
import { RequestBody, GeminiResponse, GeminiStreamChunk, Part } from './types';
import { config } from './config';
import { isAuthorized } from './auth';
import { isAuthorized, ensureOAuthCredentials } from './auth';
// ==================================================================
// Server Configuration
@@ -24,7 +24,7 @@ if (VERBOSE) {
consola.info('Verbose logging enabled');
}
consola.info('Google CLI OpenAI proxy');
consola.info('Google CLI OpenAI API');
// ==================================================================
// HTTP Server Helpers
@@ -85,56 +85,66 @@ function readJSON(
// ==================================================================
// Main Server Logic
// ==================================================================
http
.createServer(async (req, res) => {
allowCors(res);
const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
const pathname = url.pathname.replace(/\/$/, '') || '/';
consola.info(`${req.method} ${url.pathname}`);
// Handle pre-flight CORS requests.
if (req.method === 'OPTIONS') {
res.writeHead(204).end();
return;
}
ensureOAuthCredentials()
.then(() => {
http
.createServer(async (req, res) => {
allowCors(res);
const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
const pathname = url.pathname.replace(/\/$/, '') || '/';
consola.info(`${req.method} ${url.pathname}`);
if (!isAuthorized(req, res)) {
return;
}
// Handle pre-flight CORS requests.
if (req.method === 'OPTIONS') {
res.writeHead(204).end();
return;
}
// Route for listing available models.
if (pathname === '/v1/models' || pathname === '/models') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
data: listModels(),
}),
);
return;
}
if (pathname === '/') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Google CLI OpenAI API server is running......');
return;
}
// Route for chat completions.
if (
(pathname === '/chat/completions' ||
if (!isAuthorized(req, res)) {
return;
}
// Route for listing available models.
if (pathname === '/v1/models' || pathname === '/models') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
data: listModels(),
}),
);
return;
}
// Route for chat completions.
if (
(pathname === '/chat/completions' ||
pathname === '/v1/chat/completions') &&
req.method === 'POST'
) {
const body = await readJSON(req, res);
if (!body) return;
) {
const body = await readJSON(req, res);
if (!body) return;
try {
const { geminiReq, tools } = await mapRequest(body);
try {
const { geminiReq, tools } = await mapRequest(body);
if (body.stream) {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
if (body.stream) {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
for await (const chunk of sendChatStream({ ...geminiReq, tools })) {
// Transform the chunk to match the expected stream format.
const transformedParts =
for await (
const chunk of sendChatStream({ ...geminiReq, tools })) {
// Transform the chunk to match the expected stream format.
const transformedParts =
chunk.candidates?.[0]?.content?.parts?.map((part) => {
const transformedPart: Part = {
text: part.text,
@@ -151,50 +161,59 @@ http
return transformedPart;
}) ?? [];
const streamChunk: GeminiStreamChunk = {
candidates: [
{
content: {
parts: transformedParts,
},
},
],
};
const streamChunk: GeminiStreamChunk = {
candidates: [
{
content: {
parts: transformedParts,
},
},
],
};
res.write(
`data: ${JSON.stringify(mapStreamChunk(streamChunk))}\n\n`,
);
}
res.end('data: [DONE]\n\n');
} else {
const gResp: GeminiResponse = await sendChat({ ...geminiReq, tools });
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(mapResponse(gResp, body)));
}
} catch (err) {
const error = err as Error;
consola.error('Proxy error ➜', error);
res.write(
`data: ${JSON.stringify(mapStreamChunk(streamChunk))}\n\n`,
);
}
res.end('data: [DONE]\n\n');
} else {
const gResp: GeminiResponse =
await sendChat({ ...geminiReq, tools });
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(mapResponse(gResp, body)));
}
} catch (err) {
const error = err as Error;
consola.error('Proxy error ➜', error);
// Handle errors, sending them in the appropriate format for streaming
// or non-streaming responses.
if (body.stream && res.headersSent) {
res.write(
`data: ${JSON.stringify({
error: {
message: error.message,
type: 'error',
},
})}\n\n`,
);
res.end('data: [DONE]\n\n');
return;
} else {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: { message: error.message } }));
// Handle errors, sending them in the appropriate
// format for streaming or non-streaming responses.
if (body.stream && res.headersSent) {
res.write(
`data: ${JSON.stringify({
error: {
message: error.message,
type: 'error',
},
})}\n\n`,
);
res.end('data: [DONE]\n\n');
return;
} else {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: { message: error.message } }));
}
}
}
}
}
})
.listen(PORT, () => {
consola.info(`Listening on port :${PORT}`);
});
})
.listen(PORT, () => {
consola.info(`Listening on port :${PORT}`);
.catch((err: unknown) => {
if (err instanceof Error) {
consola.error(err.message);
} else {
consola.error('An unknown error occurred during startup.');
}
});