diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/README-EN.md b/README-EN.md new file mode 100644 index 0000000..38d21b9 --- /dev/null +++ b/README-EN.md @@ -0,0 +1,167 @@ +# GeminiCli2API +
+This project contains two Node.js HTTP servers. They act as local proxies for the Google Cloud Code Assist API. One of the servers also provides an OpenAI API compatible interface. This allows you to bypass the terminal interface and integrate with any client through an API. + +[中文](./README.md) / [English](./README-EN.md) +
+ +## Project Overview + +- `gemini-api-server.js`: This is a standalone Node.js HTTP server. It acts as a local proxy for the Google Cloud Code Assist API. This server provides all features and bug fixes. It is designed to be robust and flexible. With a fully controllable logging system, it is easy to monitor. +- `openai-api-server.js`: This script is based on `gemini-api-server.js`. It creates a standalone Node.js HTTP server. This server is also a local proxy for the Google Cloud Code Assist API. However, it provides an OpenAI API compatible interface. Therefore, any client that supports the OpenAI API can use it directly. +- `gemini-core.js`: This file contains the core logic shared by both servers. For example, authentication, API calls, request/response handling, and logging functions. + +## Feature Description + +### Problems Solved + +- Solves the problem of reduced free quotas for the official Gemini API. Now, users can authorize with their Gemini CLI account to make 1000 requests per day. +- Provides compatibility with the OpenAI API, making it convenient for existing OpenAI clients to use. + +### Current Limitations + +- Cannot use the built-in functions of the original Gemini CLI. +- Does not support multimodal capabilities. (todo) + +## Main Features + +### Gemini API Server (`gemini-api-server.js`) + +- **Automatic Authentication and Token Renewal**: On the first run, the script guides the user to complete Google account manual authorization through a browser. It will obtain an OAuth token. This token is securely stored locally and automatically refreshed before it expires. This ensures the continuous operation of the service without manual intervention. +- **Manual Authorization Flow**: + 1. **Copy Authorization Link**: The terminal will output a Google authorization URL. Please copy this URL. + 2. **Open the Link in a Browser**: Open the URL in a browser on any device with a graphical interface (e.g., your local computer). + 3. **Complete Authorization**: Log in to your Google account and grant permissions. + 4. **Copy the Redirected URL**: After authorization, the browser will attempt to redirect to a URL, extract the authorization code, complete authentication, and start the service normally. +- **Flexible API Key Validation**: Users can provide the key in the URL query parameter (`?key=...`) or the `x-goog-api-key` request header. As long as the key is correct, the request can be authorized. The key can be set via the `--api-key` startup parameter. +- **Role Normalization Fix**: The server automatically adds the necessary 'user'/'model' roles to the request body. At the same time, it correctly preserves `systemInstruction` (or `system_instruction`). +- **Fixed Model List**: The server specifically provides and uses the `gemini-1.5-pro-latest` and `gemini-1.5-flash-latest` models. +- **Full Gemini API Endpoint Support**: Implements `listModels`, `generateContent`, `streamGenerateContent`. +- **Fully Controllable Logging System**: Includes the remaining validity period of the token. It can output timestamped prompt logs to the console or a file. Supports log printing. + +### OpenAI Compatible API Server (`openai-api-server.js`) + +- **OpenAI API Compatibility**: Implements the `/v1/models` and `/v1/chat/completions` endpoints. +- **Format Conversion**: Automatically converts requests/responses between OpenAI format and the internal Gemini format. +- **Streaming Support**: Fully supports OpenAI's streaming responses (`"stream": true`). +- **Flexible Authentication**: Supports API key validation through the `Authorization: Bearer ` request header, URL query parameters (`?key=...`), or the `x-goog-api-key` request header. +- **Configurability**: The listening address, port, API key, and prompt logging mode can be configured via command-line arguments. +- **Reuses Core Logic**: The underlying communication with Google services still uses `gemini-core.js`. + +## Installation + +1. **Environment Setup**: + Create a `package.json` file in the project root directory with the content: `{"type": "module"}`. This is to avoid module type warnings. + (This project already provides a `package.json` file, no need to create it manually) + +2. **Install Dependencies**: + ```bash + npm install + ``` + This will install `google-auth-library` and `uuid`. + +## Usage Instructions and Command Line Examples + +### 1. Gemini API Server (`gemini-api-server.js`) + +**Start the service** (the following parameters can be combined): + +- **Default start**: Listen on `localhost:3000`, do not print prompts + ```bash + node gemini-api-server.js + ``` +- **Specify listening IP**: Listen on all network interfaces (e.g., for Docker or LAN access) + ```bash + node gemini-api-server.js 0.0.0.0 + ``` +- **Print prompts to console**: Listen on `localhost` and output prompt details to the console + ```bash + node gemini-api-server.js --log-prompts console + ``` +- **Print prompts to file**: Listen on `localhost` and save prompt details to a new file with a startup timestamp (e.g., `prompts-20231027-153055.log`) + ```bash + node gemini-api-server.js --log-prompts file + ``` +- **Combine parameters** (parameter order does not matter): + - Run on a specified IP and print prompts to the console + ```bash + node gemini-api-server.js 192.168.1.100 --log-prompts console + ``` + - Run on all network interfaces and print prompts to a file + ```bash + node gemini-api-server.js --log-prompts file 0.0.0.0 + ``` + - Specify API Key and port + ```bash + node gemini-api-server.js --api-key your_secret_key --port 3001 + ``` + +**Call API Endpoints** (Default API Key: `123456`): + +- **a) List available models** (GET request, key in URL parameter) + ```bash + curl "http://localhost:3000/v1beta/models?key=123456" + ``` +- **b) Generate content - single turn conversation** (POST request, key in request header) + ```bash + curl "http://localhost:3000/v1beta/models/gemini-1.5-pro-latest:generateContent" \ + -H "Content-Type: application/json" \ + -H "x-goog-api-key: 123456" \ + -d '{"contents":[{"parts":[{"text":"Explain what a proxy server is in one sentence"}]}]}' + ``` +- **c) Generate content - with system prompt** (POST request, key in request header, note `system_instruction`) + ```bash + curl "http://localhost:3000/v1beta/models/gemini-1.5-pro-latest:generateContent" \ + -H "Content-Type: application/json" \ + -H "x-goog-api-key: 123456" \ + -d '{ + "system_instruction": { "parts": [{ "text": "You are a cat named Neko." }] }, + "contents": [{ "parts": [{ "text": "Hello, what is your name?" }] }] + }' + ``` +- **d) Stream generate content** (POST request, key in URL parameter) + ```bash + curl "http://localhost:3000/v1beta/models/gemini-1.5-flash-latest:streamGenerateContent?key=123456" \ + -H "Content-Type: application/json" \ + -d '{"contents":[{"parts":[{"text":"Write a five-line poem about the universe"}]}]}' + ``` + +### 2. OpenAI Compatible API Server (`openai-api-server.js`) + +**Start the service**: + +- **Same as Gemini API Server**: + + +**Call API Endpoints** (Assuming API Key: `your_secret_key`, service running on `localhost:8000`): + +- **a) List available models**: + ```bash + curl http://localhost:8000/v1/models \ + -H "Authorization: Bearer your_secret_key" + ``` +- **b) Generate content - with system prompt (non-streaming)**: + ```bash + curl http://localhost:8000/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your_secret_key" \ + -d '{ + "model": "gemini-1.5-pro-latest", + "messages": [ + {"role": "system", "content": "You are a cat named Neko."}, + {"role": "user", "content": "Hello, what is your name?"} + ] + }' + ``` +- **c) Generate content - streaming**: + ```bash + curl http://localhost:8000/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your_secret_key" \ + -d '{ + "model": "gemini-1.5-flash-latest", + "messages": [ + {"role": "user", "content": "Write a five-line poem about the universe"} + ], + "stream": true + }' diff --git a/README.md b/README.md new file mode 100644 index 0000000..082f9cf --- /dev/null +++ b/README.md @@ -0,0 +1,167 @@ +# GeminiCli2API +
+该项目包含两个 Node.js HTTP 服务器。它们作为 Google Cloud Code Assist API 的本地代理。其中一个服务器还提供了与 OpenAI API 兼容的接口。可以抛弃终端界面,通过API的形式接入到任意客户端。 + +[中文](./README.md) / [English](./README-EN.md) +
+ +## 项目概述 + +- `gemini-api-server.js`: 这是一个独立的 Node.js HTTP 服务器。它作为 Google Cloud Code Assist API 的本地代理。该服务器提供所有功能和错误修复。它的设计稳健、灵活。通过全面可控的日志系统,可以方便地进行监控。 +- `openai-api-server.js`: 这个脚本基于 `gemini-api-server.js`。它创建了一个独立的 Node.js HTTP 服务器。此服务器也是 Google Cloud Code Assist API 的本地代理。但是,它对外提供了与 OpenAI API 兼容的接口。因此,任何支持 OpenAI API 的客户端可以直接使用它。 +- `gemini-core.js`: 这个文件包含两个服务器共享的核心逻辑。例如,认证、API 调用、请求/响应处理以及日志记录功能。 + +## 功能说明 + +### 解决的问题 + +- 解决了 Gemini 官方免费额度减少的问题。现在,用户可以使用 Gemini CLI 账号授权,每天进行 1000 次请求。 +- 提供了与 OpenAI API 的兼容性,方便现有 OpenAI 客户端使用。 + +### 目前的局限 + +- 不能使用原版 Gemini CLI 的内置功能。 +- 不支持多模态能力。(todo) + +## 主要功能 + +### Gemini API Server (`gemini-api-server.js`) + +- **自动认证与令牌续期**: 首次运行,脚本会引导用户通过浏览器完成 Google 账号手动授权。它将获取 OAuth 令牌。此令牌会安全地存储在本地,并在过期前自动刷新。这确保了服务的持续运行,无需手动干预。 +- **手动授权流程**: + 1. **复制授权链接**: 终端会输出一个 Google 授权 URL。请复制此 URL。 + 2. **在浏览器中打开链接**: 在任何有图形界面的设备上 (例如你的本地电脑) 的浏览器中打开该 URL。 + 3. **完成授权**: 登录你的 Google 账号并授予权限。 + 4. **复制重定向后的 URL**: 授权后,浏览器会尝试重定向到一个URL,提取授权码,完成认证,并正常启动服务。 +- **灵活的 API 密钥校验**: 用户可以在 URL 查询参数 (`?key=...`) 或 `x-goog-api-key` 请求头中提供密钥。只要密钥正确,请求即可通过授权。密钥可以通过 `--api-key` 启动参数设置。 +- **角色规范化修复**: 服务器会自动为请求体添加必要的 'user'/'model' 角色。同时,它会正确保留 `systemInstruction` (或 `system_instruction`)。 +- **固定的模型列表**: 服务器专门提供并使用 `gemini-2.5-pro` 和 `gemini-2.5-flash` 模型。 +- **完整的 Gemini API 端点支持**: 实现了 `listModels`, `generateContent`, `streamGenerateContent`。 +- **全面可控的日志系统**: 包括令牌剩余有效期。它可以输出到控制台或文件的带时间戳的提示词日志。支持日志打印。 + +### OpenAI 兼容 API Server (`openai-api-server.js`) + +- **OpenAI API 兼容性**: 实现了 `/v1/models` 和 `/v1/chat/completions` 端点。 +- **格式转换**: 自动将 OpenAI 格式的请求/响应与内部 Gemini 格式进行转换。 +- **流式传输支持**: 完全支持 OpenAI 的流式响应 (`"stream": true`)。 +- **灵活的认证**: 支持通过 `Authorization: Bearer ` 请求头、URL 查询参数 (`?key=...`) 或 `x-goog-api-key` 请求头进行 API 密钥校验。 +- **可配置性**: 可以通过命令行参数配置监听地址、端口、API 密钥和提示词日志模式。 +- **重用核心逻辑**: 底层仍使用 `gemini-core.js` 与 Google 服务通信。 + +## 安装 + +1. **环境设置**: + 在项目根目录创建一个 `package.json` 文件,内容为: `{"type": "module"}`。这是为了避免模块类型警告。 + (本项目已提供 `package.json` 文件,无需手动创建) + +2. **安装依赖**: + ```bash + npm install + ``` + 这将安装 `google-auth-library` 和 `uuid`。 + +## 使用说明 与 命令行示例 + +### 1. Gemini API Server (`gemini-api-server.js`) + +**启动服务** (可以组合使用以下参数): + +- **默认启动**: 监听 `localhost:3000`,不打印提示词 + ```bash + node gemini-api-server.js + ``` +- **指定监听 IP**: 监听所有网络接口 (例如,用于 Docker 或局域网访问) + ```bash + node gemini-api-server.js 0.0.0.0 + ``` +- **打印提示词到控制台**: 监听 `localhost`,并在控制台输出提示词详情 + ```bash + node gemini-api-server.js --log-prompts console + ``` +- **打印提示词到文件**: 监听 `localhost`,并将提示词详情保存到一个带启动时间戳的新文件中 (例如: `prompts-20231027-153055.log`) + ```bash + node gemini-api-server.js --log-prompts file + ``` +- **组合使用参数** (参数顺序无关): + - 在指定 IP 上运行,并打印提示词到控制台 + ```bash + node gemini-api-server.js 192.168.1.100 --log-prompts console + ``` + - 在所有网络接口上运行,并打印提示词到文件 + ```bash + node gemini-api-server.js --log-prompts file 0.0.0.0 + ``` + - 指定 API Key 和端口 + ```bash + node gemini-api-server.js --api-key your_secret_key --port 3001 + ``` + +**调用 API 接口** (默认 API Key: `123456`): + +- **a) 列出可用模型** (GET 请求,密钥在 URL 参数中) + ```bash + curl "http://localhost:3000/v1beta/models?key=123456" + ``` +- **b) 生成内容 - 单轮对话** (POST 请求,密钥在请求头中) + ```bash + curl "http://localhost:3000/v1beta/models/gemini-2.5-pro:generateContent" \ + -H "Content-Type: application/json" \ + -H "x-goog-api-key: 123456" \ + -d '{"contents":[{"parts":[{"text":"用一句话解释什么是代理服务器"}]}]}' + ``` +- **c) 生成内容 - 带系统提示词** (POST 请求,密钥在请求头中,注意 `system_instruction`) + ```bash + curl "http://localhost:3000/v1beta/models/gemini-2.5-pro:generateContent" \ + -H "Content-Type: application/json" \ + -H "x-goog-api-key: 123456" \ + -d '{ + "system_instruction": { "parts": [{ "text": "你是一只名叫 Neko 的猫。" }] }, + "contents": [{ "parts": [{ "text": "你好,你叫什么名字?" }] }] + }' + ``` +- **d) 流式生成内容** (POST 请求,密钥在 URL 参数中) + ```bash + curl "http://localhost:3000/v1beta/models/gemini-2.5-flash:streamGenerateContent?key=123456" \ + -H "Content-Type: application/json" \ + -d '{"contents":[{"parts":[{"text":"写一首关于宇宙的五行短诗"}]}]}' + ``` + +### 2. OpenAI 兼容 API Server (`openai-api-server.js`) + +**启动服务** : + +- **同Gemini API Server 保持一致**: + + +**调用 API 接口** (假设 API Key: `your_secret_key`, 服务运行在 `localhost:8000`): + +- **a) 列出可用模型**: + ```bash + curl http://localhost:8000/v1/models \ + -H "Authorization: Bearer your_secret_key" + ``` +- **b) 生成内容 - 带系统提示词 (非流式)**: + ```bash + curl http://localhost:8000/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your_secret_key" \ + -d '{ + "model": "gemini-2.5-pro", + "messages": [ + {"role": "system", "content": "你是一只名叫 Neko 的猫。"}, + {"role": "user", "content": "你好,你叫什么名字?"} + ] + }' + ``` +- **c) 生成内容 - 流式**: + ```bash + curl http://localhost:8000/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your_secret_key" \ + -d '{ + "model": "gemini-2.5-flash", + "messages": [ + {"role": "user", "content": "写一首关于宇宙的五行短诗"} + ], + "stream": true + }' diff --git a/gemini-api-server.js b/gemini-api-server.js new file mode 100644 index 0000000..cf42cd9 --- /dev/null +++ b/gemini-api-server.js @@ -0,0 +1,273 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + * + * 描述: + * (最终生产可用版) + * 该脚本创建了一个独立的 Node.js HTTP 服务器,作为 Google Cloud Code Assist API 的本地代理。 + * 此版本包含了所有功能和错误修复,设计稳健、灵活,并通过全面且可控的日志系统使其易于监控。 + * + * 主要功能: + * - 灵活的 API 密钥校验: 只要在 URL 查询参数 (`?key=...`) 或 `x-goog-api-key` 请求头中提供了正确的密钥,请求即可通过授权。密钥可通过 `--api-key` 启动参数设置。 + * - 角色规范化修复: 自动为请求体添加必需的 'user'/'model' 角色,并正确保留 `systemInstruction` (或 `system_instruction`)。 + * - 固定的模型列表: 服务器现在专门提供并使用 `gemini-2.5-pro` 和 `gemini-2.5-flash` 模型。 + * - 完整的 Gemini API 端点支持: 实现了 `listModels`, `generateContent`, `streamGenerateContent`。 + * - 全面且可控的日志系统: 包括令牌剩余有效期、可输出到控制台或文件的带时间戳的提示词日志等。 + * + * ----------------------------------------------------------------------------- + * 使用说明 & 命令行示例 + * ----------------------------------------------------------------------------- + * + * 1. 环境设置: + * // 在脚本所在目录创建一个 `package.json` 文件,内容为: {"type": "module"} + * // 以避免模块类型警告。 + * + * // 安装依赖: + * npm install google-auth-library + * + * 2. 启动服务 (根据需要组合使用以下参数): + * + * // 默认启动: 监听 localhost,不打印提示词 + * node gemini-api-server-final.js + * + * // 指定监听IP: 监听所有网络接口 (例如,用于 Docker 或局域网访问) + * node gemini-api-server-final.js 0.0.0.0 + * + * // 打印提示词到控制台: 监听 localhost,并在控制台输出提示词详情 + * node gemini-api-server-final.js --log-prompts console + * + * // 打印提示词到文件: 监听 localhost,并将提示词详情保存到一个带启动时间戳的新文件中 + * // (例如: prompts-20231027-153055.log) + * node gemini-api-server-final.js --log-prompts file + * + * // 组合使用参数 (参数顺序无关): + * // 在指定 IP 上运行,并打印提示词到控制台 + * node gemini-api-server-final.js 192.168.1.100 --log-prompts console + * + * // 在所有网络接口上运行,并打印提示词到文件 + * node gemini-api-server-final.js --log-prompts file 0.0.0.0 + * + * // 指定 API Key 和端口 (参数顺序无关) + * node gemini-api-server-final.js --api-key your_secret_key --port 3001 + * + * 3. 调用 API 接口 (默认 API Key: 123456): + * + * // a) 列出可用模型 (GET 请求,密钥在 URL 参数中) + * curl "http://localhost:3000/v1beta/models?key=123456" + * + * // b) 生成内容 - 单轮对话 (POST 请求,密钥在请求头中) + * curl "http://localhost:3000/v1beta/models/gemini-2.5-pro:generateContent" \ + * -H "Content-Type: application/json" \ + * -H "x-goog-api-key: 123456" \ + * -d '{"contents":[{"parts":[{"text":"用一句话解释什么是代理服务器"}]}]}' + * + * // c) 生成内容 - 带系统提示词 (POST 请求,密钥在请求头中,注意 system_instruction) + * curl "http://localhost:3000/v1beta/models/gemini-2.5-pro:generateContent" \ + * -H "Content-Type: application/json" \ + * -H "x-goog-api-key: 123456" \ + * -d '{ + * "system_instruction": { "parts": [{ "text": "你是一只名叫 Neko 的猫。" }] }, + * "contents": [{ "parts": [{ "text": "你好,你叫什么名字?" }] }] + * }' + * + * // d) 流式生成内容 (POST 请求,密钥在 URL 参数中) + * curl "http://localhost:3000/v1beta/models/gemini-2.5-flash:streamGenerateContent?key=123456" \ + * -H "Content-Type: application/json" \ + * -d '{"contents":[{"parts":[{"text":"写一首关于宇宙的五行短诗"}]}]}' + * + */ + + + +import * as http from 'http'; +import { + GeminiApiService, + API_ACTIONS, + formatExpiryTime, + logPrompt, + extractPromptText, + extractResponseText, + getRequestBody +} from './gemini-core.js'; + +// --- Configuration Parsing --- +let HOST = 'localhost'; +let PROMPT_LOG_MODE = 'none'; // 'none', 'console', 'file' +const PROMPT_LOG_BASE_NAME = 'prompts'; +let PROMPT_LOG_FILENAME = ''; +let REQUIRED_API_KEY = '123456'; // Default API Key +let SERVER_PORT = 3000; // Default Port + +const args = process.argv.slice(2); +const remainingArgs = []; + +for (let i = 0; i < args.length; i++) { + if (args[i] === '--api-key') { + if (i + 1 < args.length) { + REQUIRED_API_KEY = args[i + 1]; + i++; // Skip the value + } else { + console.warn(`[Config Warning] --api-key flag requires a value.`); + } + } else if (args[i] === '--log-prompts') { + if (i + 1 < args.length) { + const mode = args[i + 1]; + if (mode === 'console' || mode === 'file') { + PROMPT_LOG_MODE = mode; + } else { + console.warn(`[Config Warning] Invalid mode for --log-prompts. Expected 'console' or 'file'. Prompt logging is disabled.`); + } + i++; // Skip the value + } else { + console.warn(`[Config Warning] --log-prompts flag requires a value.`); + } + } else if (args[i] === '--port') { + if (i + 1 < args.length) { + SERVER_PORT = parseInt(args[i + 1], 10); + i++; // Skip the value + } else { + console.warn(`[Config Warning] --port flag requires a value.`); + } + } else { + remainingArgs.push(args[i]); + } +} + +if (remainingArgs.length > 0) { + HOST = remainingArgs[0]; +} + +if (PROMPT_LOG_MODE === 'file') { + const now = new Date(); + const pad = (num) => num.toString().padStart(2, '0'); + const timestamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`; + PROMPT_LOG_FILENAME = `${PROMPT_LOG_BASE_NAME}-${timestamp}.log`; +} + +// --- Constants --- +// SERVER_PORT is now a configurable variable + +function isAuthorized(req, requestUrl) { + const queryKey = requestUrl.searchParams.get('key'); + const headerKey = req.headers['x-goog-api-key']; + + if (queryKey === REQUIRED_API_KEY || headerKey === REQUIRED_API_KEY) { + return true; + } + + console.log(`[Auth] Unauthorized request denied. Query key: "${queryKey}", Header key: "${headerKey}"`); + return false; +} + +// --- Singleton Instance & HTTP Server Handlers --- +let apiServiceInstance = null; +async function getApiService() { + if (!apiServiceInstance) { + apiServiceInstance = new GeminiApiService(HOST); + await apiServiceInstance.initialize(); + } else if (!apiServiceInstance.isInitialized) { + await apiServiceInstance.initialize(); + } + return apiServiceInstance; +} +async function handleStreamRequest(res, service, model, requestBody) { + res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "Transfer-Encoding": "chunked" }); + const stream = service.generateContentStream(model, requestBody); + console.log('[Server Response Stream]'); + process.stdout.write('> '); + for await (const chunk of stream) { + const chunkText = extractResponseText(chunk); + if (chunkText) process.stdout.write(chunkText); + const chunkString = JSON.stringify(chunk); + res.write(`data: ${chunkString}\n\n`); + } + process.stdout.write('\n'); + res.end(); +} +async function handleUnaryRequest(res, service, model, requestBody) { + const response = await service.generateContent(model, requestBody); + console.log('[Server Response Unary]'); + process.stdout.write('> '); + const responseText = extractResponseText(response); + process.stdout.write(responseText); + process.stdout.write('\n'); + const responseString = JSON.stringify(response); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(responseString); +} +function handleError(res, error) { + console.error('\n[Server] Request failed:', error.stack); + if (!res.headersSent) { + const statusCode = error.response?.status || 500; + res.writeHead(statusCode, { 'Content-Type': 'application/json' }); + } + const errorPayload = { error: { message: error.message, details: error.response?.data } }; + res.end(JSON.stringify(errorPayload)); +} + +async function requestHandler(req, res) { + console.log(`\n[Server] Received request: ${req.method} http://${req.headers.host}${req.url}`); + + const requestUrl = new URL(req.url, `http://${req.headers.host}`); + + if (!isAuthorized(req, requestUrl)) { + res.writeHead(401, { 'Content-Type': 'application/json' }); + return res.end(JSON.stringify({ error: { message: 'Unauthorized: API key is invalid or missing. Provide it in the `x-goog-api-key` header or as a `key` query parameter.' } })); + } + + try { + const service = await getApiService(); + const expiryDate = service.authClient.credentials.expiry_date; + console.log(`[Auth Token] Time until expiry: ${formatExpiryTime(expiryDate)}`); + + if (req.method === 'GET' && requestUrl.pathname === '/v1beta/models') { + const models = await service.listModels(); + res.writeHead(200, { 'Content-Type': 'application/json' }); + return res.end(JSON.stringify(models)); + } + + const urlPattern = new RegExp(`/v1beta/models/(.+?):(${API_ACTIONS.GENERATE_CONTENT}|${API_ACTIONS.STREAM_GENERATE_CONTENT})`); + const urlMatch = requestUrl.pathname.match(urlPattern); + + if (req.method === 'POST' && urlMatch) { + const [, model, action] = urlMatch; + const requestBody = await getRequestBody(req); + + if (PROMPT_LOG_MODE !== 'none') { + const promptText = extractPromptText(requestBody); + await logPrompt(promptText, PROMPT_LOG_MODE, PROMPT_LOG_FILENAME); + } + + if (action === API_ACTIONS.STREAM_GENERATE_CONTENT) { + await handleStreamRequest(res, service, model, requestBody); + } else { + await handleUnaryRequest(res, service, model, requestBody); + } + return; + } + + res.writeHead(404, { 'Content-Type': 'application/json' }); + return res.end(JSON.stringify({ error: { message: 'Not Found' } })); + + } catch (error) { + handleError(res, error); + } +} + +// --- Server Initialization --- +const server = http.createServer(requestHandler); + +server.listen(SERVER_PORT, HOST, () => { + console.log(`--- Server Configuration ---`); + console.log(` Host: ${HOST}`); + console.log(` Port: ${SERVER_PORT}`); + console.log(` Required API Key: ${REQUIRED_API_KEY}`); + console.log(` Prompt Logging: ${PROMPT_LOG_MODE}${PROMPT_LOG_MODE === 'file' ? ` (to ${PROMPT_LOG_FILENAME})` : ''}`); + console.log(`--------------------------`); + console.log(`\nGemini API Server (Final) running on http://${HOST}:${SERVER_PORT}`); + console.log('Initializing service... This may take a moment.'); + getApiService().catch(err => { + console.error("[Server] Pre-warming failed.", err.message); + }); +}); diff --git a/openai-api-server.js b/openai-api-server.js new file mode 100644 index 0000000..150b668 --- /dev/null +++ b/openai-api-server.js @@ -0,0 +1,487 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + * + * 功能: + * 该脚本创建一个独立的 Node.js HTTP 服务器,作为 Google Cloud Code Assist API 的本地代理, + * 但它暴露了与 OpenAI API 兼容的接口,使其可以被任何支持 OpenAI API 的客户端直接使用。 + * + * 主要特性: + * - **OpenAI API 兼容性**: 实现了 `/v1/models` 和 `/v1/chat/completions` 端点。 + * - **格式转换**: 自动将 OpenAI 格式的请求/响应与内部 Gemini 格式进行转换。 + * - **流式传输支持**: 完全支持 OpenAI 的流式响应 (`"stream": true`)。 + * - **灵活的认证**: 支持通过 `Authorization: Bearer ` 请求头、URL 查询参数 (`?key=...`) 或 `x-goog-api-key` 请求头进行 API 密钥校验。 + * - **全面且可控的日志系统**: 包括令牌剩余有效期、可输出到控制台或文件的带时间戳的提示词日志等。 + * - **可配置性**: 可以通过命令行参数配置监听地址、端口、API 密钥和提示词日志模式。 + * - **重用核心逻辑**: 底层依然使用 `gemini-core.js` 与 Google 服务通信。 + * + * ----------------------------------------------------------------------------- + * 使用说明 & 命令行示例 + * ----------------------------------------------------------------------------- + * + * 1. 环境设置: + * 在项目根目录创建一个 `package.json` 文件,内容为: `{"type": "module"}`,以避免模块类型警告。 + * (此项目已提供 `package.json` 文件,无需手动创建) + * + * 2. 安装依赖: + * ```bash + * npm install + * ``` + * 这将安装 `google-auth-library` 和 `uuid`。 + * + * 3. 启动服务 (根据需要组合使用以下参数): + * + * - **默认启动**: 监听 `localhost:8000` + * ```bash + * node openai-api-server.js + * ``` + * - **指定监听 IP** (位置参数): + * ```bash + * node openai-api-server.js 0.0.0.0 + * ``` + * - **使用命名参数指定端口**: + * ```bash + * node openai-api-server.js --port 8081 + * ``` + * - **使用命名参数指定 API Key**: + * ```bash + * node openai-api-server.js --api-key your_secret_key + * ``` + * - **打印提示词到控制台**: 监听 `localhost`,并在控制台输出提示词详情 + * ```bash + * node openai-api-server.js --log-prompts console + * ``` + * - **打印提示词到文件**: 监听 `localhost`,并将提示词详情保存到一个带启动时间戳的新文件中 (例如: `prompts-20231027-153055.log`) + * ```bash + * node openai-api-server.js --log-prompts file + * ``` + * - **组合使用参数** (参数顺序无关): + * ```bash + * node openai-api-server.js --port 8088 --api-key your_secret_key 0.0.0.0 + * ``` + * + * 4. 调用 API 接口 (假设 API Key: `your_secret_key`, 服务运行在 `localhost:8000`): + * + * - **a) 列出可用模型** + * ```bash + * curl http://localhost:8000/v1/models \ + * -H "Authorization: Bearer your_secret_key" + * ``` + * - **b) 生成内容 - 带系统提示词 (非流式)** + * ```bash + * curl http://localhost:8000/v1/chat/completions \ + * -H "Content-Type: application/json" \ + * -H "Authorization: Bearer your_secret_key" \ + * -d '{ + * "model": "gemini-2.5-pro", + * "messages": [ + * {"role": "system", "content": "你是一只名叫 Neko 的猫。"}, + * {"role": "user", "content": "你好,你叫什么名字?"} + * ] + * }' + * ``` + * - **c) 生成内容 - 流式** + * ```bash + * curl http://localhost:8000/v1/chat/completions \ + * -H "Content-Type: application/json" \ + * -H "Authorization: Bearer your_secret_key" \ + * -d '{ + * "model": "gemini-2.5-flash", + * "messages": [ + * {"role": "user", "content": "写一首关于宇宙的五行短诗"} + * ], + * "stream": true + * }' + * + */ + +import * as http from 'http'; +import { v4 as uuidv4 } from 'uuid'; +import { + GeminiApiService, + API_ACTIONS, + formatExpiryTime, + logPrompt, + extractPromptText, + getRequestBody, + extractResponseText +} from './gemini-core.js'; + +// --- Configuration Parsing --- +let HOST = 'localhost'; +let PROMPT_LOG_MODE = 'none'; // 'none', 'console', 'file' +const PROMPT_LOG_BASE_NAME = 'prompts'; +let PROMPT_LOG_FILENAME = ''; +let REQUIRED_API_KEY = '123456'; // Default API Key +let SERVER_PORT = 8000; // Default Port + +const args = process.argv.slice(2); +const remainingArgs = []; + +for (let i = 0; i < args.length; i++) { + if (args[i] === '--api-key') { + if (i + 1 < args.length) { + REQUIRED_API_KEY = args[i + 1]; + i++; // Skip the value + } else { + console.warn(`[Config Warning] --api-key flag requires a value.`); + } + } else if (args[i] === '--port') { + if (i + 1 < args.length) { + SERVER_PORT = parseInt(args[i + 1], 10); + i++; // Skip the value + } else { + console.warn(`[Config Warning] --port flag requires a value.`); + } + } else if (args[i] === '--log-prompts') { + if (i + 1 < args.length) { + const mode = args[i + 1]; + if (mode === 'console' || mode === 'file') { + PROMPT_LOG_MODE = mode; + } else { + console.warn(`[Config Warning] Invalid mode for --log-prompts. Expected 'console' or 'file'. Prompt logging is disabled.`); + } + i++; // Skip the value + } else { + console.warn(`[Config Warning] --log-prompts flag requires a value.`); + } + } else { + remainingArgs.push(args[i]); + } +} + +if (remainingArgs.length > 0) HOST = remainingArgs[0]; + +if (PROMPT_LOG_MODE === 'file') { + const now = new Date(); + const pad = (num) => num.toString().padStart(2, '0'); + const timestamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`; + PROMPT_LOG_FILENAME = `${PROMPT_LOG_BASE_NAME}-${timestamp}.log`; +} + +// --- Constants --- +// SERVER_PORT is now a configurable variable + +// --- Format Conversion Functions --- + +/** + * Extracts text from the 'content' field of an OpenAI message, + * which can be a string or an array of content parts (for multimodal input). + * @param {string|Array} content The content field from a message. + * @returns {string} The extracted text content. + */ +function extractTextFromMessageContent(content) { + if (typeof content === 'string') { + return content; + } + if (Array.isArray(content)) { + // Filter for text parts and join them. This gracefully handles multimodal inputs + // by only extracting the text, which is what the Gemini text models expect. + return content + .filter(part => part.type === 'text' && typeof part.text === 'string') + .map(part => part.text) + .join('\n'); + } + // Return an empty string if content is not in a recognized format. + return ''; +} + + +function toGeminiRequest(openaiRequest) { + const geminiRequest = { + contents: [] + }; + + let systemContent = []; + const messages = openaiRequest.messages || []; + + // 1. Extract and combine all system messages + const otherMessages = messages.filter(m => { + if (m.role === 'system') { + // Use the helper function to safely extract text from system messages + systemContent.push(extractTextFromMessageContent(m.content)); + return false; + } + return true; + }); + + if (systemContent.length > 0) { + console.log('[Debug] systemContent before join:', systemContent); + geminiRequest.systemInstruction = { + parts: [{ + // Now systemContent is an array of strings, so join is safe + text: systemContent.join('\n') + }] + }; + } + + // 2. Process the remaining messages, merging consecutive messages of the same role. + if (otherMessages.length > 0) { + let currentRole = null; + let currentContentParts = []; + + for (const message of otherMessages) { + const role = message.role === 'assistant' ? 'model' : message.role; + + if (role !== 'user' && role !== 'model') continue; // Ignore other roles + + const messageText = extractTextFromMessageContent(message.content); + + if (role === currentRole) { + // If the role is the same, append the content. + currentContentParts.push(messageText); + } else { + // If the role changes, push the previously accumulated content. + if (currentRole) { + console.log('[Debug] currentContentParts before join (in loop):', currentContentParts); + geminiRequest.contents.push({ + role: currentRole, + parts: [{ + text: currentContentParts.join('\n') + }] + }); + } + // Start a new content block for the new role. + currentRole = role; + currentContentParts = [messageText]; + } + } + + // Push the last accumulated content block. + if (currentRole) { + console.log('[Debug] currentContentParts before join (at end):', currentContentParts); + geminiRequest.contents.push({ + role: currentRole, + parts: [{ + text: currentContentParts.join('\n') + }] + }); + } + } + + // 3. Basic validation and logging (the API will do the final validation) + if (geminiRequest.contents.length > 0) { + if (geminiRequest.contents[0].role !== 'user') { + console.warn("[Request Conversion] Warning: Conversation doesn't start with a 'user' role. The API will likely reject this request."); + } + if (geminiRequest.contents.length > 0 && geminiRequest.contents[geminiRequest.contents.length - 1].role !== 'user') { + console.warn("[Request Conversion] Warning: The last message in the conversation is not from the 'user'. The API may reject this request."); + } + } + + + console.log('[Server] Converted Gemini Request (before core processing):', JSON.stringify(geminiRequest, null, 2)); + + return geminiRequest; +} + +function toOpenAIModelList(geminiModels) { + return { + object: "list", + data: geminiModels.map(modelId => ({ + id: modelId, + object: "model", + created: Math.floor(Date.now() / 1000), + owned_by: "google", + })), + }; +} + +function toOpenAIChatCompletion(geminiResponse, model) { + const text = extractResponseText(geminiResponse); + return { + id: `chatcmpl-${uuidv4()}`, + object: "chat.completion", + created: Math.floor(Date.now() / 1000), + model: model, + choices: [{ + index: 0, + message: { + role: "assistant", + content: text, + }, + finish_reason: "stop", + }], + usage: { + prompt_tokens: 0, // Note: Gemini API doesn't provide token counts + completion_tokens: 0, + total_tokens: 0, + }, + }; +} + +function toOpenAIStreamChunk(geminiChunk, model) { + const text = extractResponseText(geminiChunk); + return { + id: `chatcmpl-${uuidv4()}`, + object: "chat.completion.chunk", + created: Math.floor(Date.now() / 1000), + model: model, + choices: [{ + index: 0, + delta: { content: text }, + finish_reason: null, + }], + }; +} + +// --- Authorization --- +function isAuthorized(req, requestUrl) { + const authHeader = req.headers['authorization']; + const queryKey = requestUrl.searchParams.get('key'); + const headerKey = req.headers['x-goog-api-key']; + + // Check for Bearer token in Authorization header (OpenAI style) + if (authHeader && authHeader.startsWith('Bearer ')) { + const token = authHeader.substring(7); + if (token === REQUIRED_API_KEY) { + return true; + } + } + + // Check for API key in URL query parameter (Gemini style) + if (queryKey === REQUIRED_API_KEY) { + return true; + } + + // Check for API key in x-goog-api-key header (Gemini style) + if (headerKey === REQUIRED_API_KEY) { + return true; + } + + console.log(`[Auth] Unauthorized request denied. Bearer token: "${authHeader ? authHeader.substring(7) : 'N/A'}", Query key: "${queryKey}", Header key: "${headerKey}"`); + return false; +} + + +// --- Singleton Instance & HTTP Server Handlers --- +let apiServiceInstance = null; +async function getApiService() { + if (!apiServiceInstance) { + apiServiceInstance = new GeminiApiService(HOST); + await apiServiceInstance.initialize(); + } else if (!apiServiceInstance.isInitialized) { // Ensure re-initialization if not already initialized + await apiServiceInstance.initialize(); + } + return apiServiceInstance; +} + +async function handleStreamRequest(res, service, model, requestBody) { + res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive" }); + const stream = service.generateContentStream(model, requestBody); + console.log('[Server Response Stream]'); + process.stdout.write('> '); + try { + for await (const chunk of stream) { + const openAIChunk = toOpenAIStreamChunk(chunk, model); + const chunkText = openAIChunk.choices[0].delta.content || ""; + if (chunkText) { + process.stdout.write(chunkText); + } + res.write(`data: ${JSON.stringify(openAIChunk)}\n\n`); + } + // Send the final [DONE] message according to OpenAI spec + res.write('data: [DONE]\n\n'); + } catch (error) { + console.error('\n[Server] Error during stream processing:', error.stack); + if (!res.writableEnded) { + // We may not be able to write headers, but we can try to send an error payload. + const errorPayload = { error: { message: "An error occurred during streaming.", details: error.message } }; + res.end(JSON.stringify(errorPayload)); // End the response with an error + } + } finally { + process.stdout.write('\n'); + if (!res.writableEnded) { + res.end(); + } + } +} + +async function handleUnaryRequest(res, service, model, requestBody) { + const geminiResponse = await service.generateContent(model, requestBody); + console.log('[Server] Raw Gemini Unary Response:', JSON.stringify(geminiResponse, null, 2)); // Add this line + const openAIResponse = toOpenAIChatCompletion(geminiResponse, model); + console.log('[Server Response Unary]'); + process.stdout.write('> '); + const responseText = extractResponseText(geminiResponse); + process.stdout.write(responseText); + process.stdout.write('\n'); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(openAIResponse)); +} + +function handleError(res, error) { + console.error('\n[Server] Request failed:', error.stack); + if (!res.headersSent) { + const statusCode = error.response?.status || 500; + res.writeHead(statusCode, { 'Content-Type': 'application/json' }); + } + const errorPayload = { error: { message: error.message, details: error.response?.data } }; + res.end(JSON.stringify(errorPayload)); +} + +async function requestHandler(req, res) { + console.log(`\n[Server] Received request: ${req.method} http://${req.headers.host}${req.url}`); + + const requestUrl = new URL(req.url, `http://${req.headers.host}`); + + if (!isAuthorized(req, requestUrl)) { + res.writeHead(401, { 'Content-Type': 'application/json' }); + return res.end(JSON.stringify({ error: { message: 'Unauthorized: API key is invalid or missing. Provide it in the `Authorization: Bearer ` header, as a `key` query parameter, or in the `x-goog-api-key` header.' } })); + } + + try { + const service = await getApiService(); + const expiryDate = service.authClient.credentials.expiry_date; + console.log(`[Auth Token] Time until expiry: ${formatExpiryTime(expiryDate)}`); + + if (req.method === 'GET' && requestUrl.pathname === '/v1/models') { + const models = await service.listModels(); + const openAIModels = toOpenAIModelList(models.models.map(m => m.name.replace('models/', ''))); + res.writeHead(200, { 'Content-Type': 'application/json' }); + return res.end(JSON.stringify(openAIModels)); + } + + if (req.method === 'POST' && requestUrl.pathname === '/v1/chat/completions') { + const openaiRequest = await getRequestBody(req); + const model = openaiRequest.model; + const geminiRequest = toGeminiRequest(openaiRequest); + + if (PROMPT_LOG_MODE !== 'none') { + const promptText = extractPromptText(geminiRequest); // Use geminiRequest for logging + await logPrompt(promptText, PROMPT_LOG_MODE, PROMPT_LOG_FILENAME); + } + + if (openaiRequest.stream) { + await handleStreamRequest(res, service, model, geminiRequest); + } else { + await handleUnaryRequest(res, service, model, geminiRequest); + } + return; + } + + res.writeHead(404, { 'Content-Type': 'application/json' }); + return res.end(JSON.stringify({ error: { message: 'Not Found' } })); + + } catch (error) { + handleError(res, error); + } +} + +// --- Server Initialization --- +const server = http.createServer(requestHandler); + +server.listen(SERVER_PORT, HOST, () => { + console.log(`--- OpenAI-Compatible Server Configuration ---`); + console.log(` Host: ${HOST}`); + console.log(` Port: ${SERVER_PORT}`); + console.log(` Required API Key: ${REQUIRED_API_KEY}`); + console.log(` Prompt Logging: ${PROMPT_LOG_MODE}${PROMPT_LOG_MODE === 'file' ? ` (to ${PROMPT_LOG_FILENAME})` : ''}`); + console.log(`---------------------------------------------`); + console.log(`\nServer running on http://${HOST}:${SERVER_PORT}`); + console.log('Initializing backend service... This may take a moment.'); + getApiService().catch(err => { + console.error("[Server] Pre-warming failed.", err.message); + }); +}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3cd60c4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,330 @@ +{ + "name": "GeminiCli2API", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "google-auth-library": "^10.1.0", + "uuid": "^11.1.0" + } + }, + "node_modules/agent-base": { + "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" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "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": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "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", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "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/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/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": "7.0.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-7.0.1.tgz", + "integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.1.0.tgz", + "integrity": "sha512-GspVjZj1RbyRWpQ9FbAXMKjFGzZwDKnUHi66JJ+tcjcu5/xYAP1pdlWotCuIkMwjfVsxxDvsGZXGLzRt72D0sQ==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^7.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.1.tgz", + "integrity": "sha512-rcX58I7nqpu4mbKztFeOAObbomBbHU2oIb/d3tJfF3dizGSApqtSwYJigGCooHdnMyQBIw8BrWyK96w3YXgr6A==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "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": "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": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "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" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..cadb4ba --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "type": "module", + "dependencies": { + "google-auth-library": "^10.1.0", + "uuid": "^11.1.0" + } +}