Initial commit of AI-Relay-Kit
This commit is contained in:
parent
7ceeb612c4
commit
86cb262e71
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
@ -141,3 +142,4 @@ dist
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
.vite/
|
||||
.DS_Store
|
||||
|
||||
78
docs/architecture.md
Normal file
78
docs/architecture.md
Normal file
@ -0,0 +1,78 @@
|
||||
# 统一 AI 代理与配置注入器 (Unified AI Relay & Config Injector) 架构规划
|
||||
|
||||
该项目旨在完全解耦“API 聚合路由”与“客户端配置”,通过统一的 Node.js 轻量级代理服务,同时解决特定客户端(Codex、Claude Code)的私有协议壁垒,并通过自动化脚本一键注入跨工具配置。
|
||||
|
||||
> [!NOTE]
|
||||
> 该工具定位于“无状态(或纯内存轻量状态)、无 DB、可插拔”的中间件,核心网关能力完全委托给后端的 LiteLLM。
|
||||
> 本项目核心框架选用 **Hono**,使用 **TypeScript** 开发。
|
||||
|
||||
## 核心架构设计
|
||||
|
||||
### 1. 目录结构与职责 (工程模式)
|
||||
|
||||
采用插件化(Adapter)架构模式,使核心框架与具体的终端协议隔离,便于未来添加新的端点。项目代号确定为 **AI-Relay-Kit**。
|
||||
|
||||
```text
|
||||
AI-Relay-Kit/
|
||||
├── package.json
|
||||
├── docs/ # 项目文档
|
||||
│ └── architecture.md # 统一 AI 代理与配置注入器架构规划记录
|
||||
├── src/
|
||||
│ ├── index.ts # 服务入口 (Hono app)
|
||||
│ ├── config.ts # 统一配置加载 (LiteLLM 地址、端口等)
|
||||
│ ├── core/ # 核心中继控制器
|
||||
│ │ ├── request_handler.ts # 代理发送请求至 LiteLLM 核心模块
|
||||
│ │ └── cache_manager.ts # 轻量内存状态管理 (为 Codex 等提供会话持久)
|
||||
│ ├── adapters/ # 特化客户端插件层 (Plugins)
|
||||
│ │ ├── codex/ # 替代 codex-relay
|
||||
│ │ │ ├── parser.ts # Responses API -> Chat Completions 转换
|
||||
│ │ │ └── routes.ts # 暴露 /codex/v1/responses 路由
|
||||
│ │ ├── claude/ # 替代 moon-bridge
|
||||
│ │ │ ├── parser.ts # Anthropic API -> Chat Completions 转换
|
||||
│ │ │ └── routes.ts # 暴露 /claude/v1/messages 路由
|
||||
│ │ └── standard/ # 标准 OpenAI 直通 (预留给无特化需求的端)
|
||||
│ └── injector/ # Auto Config Injector 模块
|
||||
│ ├── fetch_models.ts # 从 LiteLLM 拉取 /v1/models
|
||||
│ ├── codex_injector.ts # 读写 ~/.codex/config.toml 和 catalog.json
|
||||
│ ├── claude_injector.ts # 设置 Claude Code Base URL / env vars
|
||||
│ └── anti_injector.ts # 配置 Antigravity 环境变量
|
||||
└── scripts/
|
||||
└── setup-ai-workspace.sh # 统一入口脚本,启动服务 + 触发配置注入
|
||||
```
|
||||
|
||||
### 2. 核心模块规划
|
||||
|
||||
#### 2.1 特殊客户端适配器 (Adapters)
|
||||
|
||||
* **Codex 适配器 (`adapters/codex`)**:
|
||||
* **拦截**:接收来自 Codex APP 的 POST 请求,符合专有的 `responses` 对象格式。
|
||||
* **翻译**:由于 Responses API 是 Stateful(有状态的),我们需要借助 `cache_manager` 在内存中维护历史 Session(类似 codex-relay Rust 版的逻辑)。将传入的对话状态还原为标准 `messages` 数组。
|
||||
* **转发**:请求下游 LiteLLM。
|
||||
* **响应映射**:将 LiteLLM 返回的 Chat Completions 标准 SSE 流实时映射回 Codex 期望的响应切片。
|
||||
* **Claude Code 适配器 (`adapters/claude`)**:
|
||||
* **拦截**:拦截来自 Claude Code 的 `x-api-key` 和 `anthropic-version` 等头信息。
|
||||
* **翻译**:将 Anthropic 的 `system`、`messages` (role: user/assistant) 转换为通用的 Chat Completions,注意处理复杂的 Function Calling / Tool Use 差异。
|
||||
* **转发 & 响应映射**:接收 LiteLLM 响应并翻译回 Claude Code 期望的流事件(`message_start`, `content_block_delta` 等)。*(注:如果后续 LiteLLM 的 Anthropic proxy 完善,该模块可随时配置为透传模式)*。
|
||||
|
||||
#### 2.2 自动配置注入器 (Auto Config Injector)
|
||||
|
||||
作为一个独立的 CLI 命令或启动脚本(例如 `npm run inject`),其执行流如下:
|
||||
|
||||
1. **探测后端**:轮询/探测 LiteLLM(如 `http://localhost:4000`)是否就绪。
|
||||
2. **拉取模型**:调用 LiteLLM 的 `/v1/models`。
|
||||
3. **分发配置**:
|
||||
* **Codex**:生成 `custom_model_catalog.json`,遍历 LiteLLM 返回的模型列表注入 `model_properties`,并修改 `~/.codex/config.toml` 将 `model_provider` 设为 `unified-relay`,基地址指向 `http://127.0.0.1:<RelayPort>/codex/v1`。
|
||||
* **Claude Code**:将其默认 API 端点覆写为 `http://127.0.0.1:<RelayPort>/claude/v1`,避免其强制访问远端 API。
|
||||
* **Antigravity**:可选择生成 `.env.local` 供直接读取,指向 LiteLLM `http://127.0.0.1:<LiteLLMPort>/v1`。
|
||||
|
||||
## Verification Plan
|
||||
|
||||
### 自动化与脚本测试
|
||||
* 运行 `npm run inject` 后,验证 `~/.codex/config.toml` 及相关的模型目录 JSON 文件是否按照预期生成,并包含了来自 LiteLLM 的最新动态模型。
|
||||
|
||||
### 手动验证流
|
||||
1. 启动 LiteLLM(配置好 DeepSeek、OpenAI 等上游)。
|
||||
2. 启动 Unified Relay (当前项目)。
|
||||
3. 运行 Injector 注入配置。
|
||||
4. 在 Codex APP 中,检查是否能选择出并使用刚才配置的聚合模型。
|
||||
5. 在 Claude Code 中,运行测试 Prompt 检查是否成功完成通信。
|
||||
937
package-lock.json
generated
Normal file
937
package-lock.json
generated
Normal file
@ -0,0 +1,937 @@
|
||||
{
|
||||
"name": "ai-nexus",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ai-nexus",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@hono/node-server": "2.0.5",
|
||||
"axios": "1.18.0",
|
||||
"dotenv": "17.4.2",
|
||||
"hono": "4.12.26"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "26.0.0",
|
||||
"tsx": "4.22.4",
|
||||
"typescript": "6.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
|
||||
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
|
||||
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
|
||||
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
|
||||
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
|
||||
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
|
||||
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@hono/node-server": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.5.tgz",
|
||||
"integrity": "sha512-yQFvDmyDo3y6rEOJZDUYPJ49DIKTPpIk4kGvm40xx4Ejne0Pu9a1+exxPN+C1UppWK/WGZX9F++/Xs231tE86g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"hono": "^4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "26.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz",
|
||||
"integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~8.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz",
|
||||
"integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.16.0",
|
||||
"form-data": "^4.0.5",
|
||||
"https-proxy-agent": "^5.0.1",
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.4.2",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
|
||||
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
||||
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
|
||||
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.28.1",
|
||||
"@esbuild/android-arm": "0.28.1",
|
||||
"@esbuild/android-arm64": "0.28.1",
|
||||
"@esbuild/android-x64": "0.28.1",
|
||||
"@esbuild/darwin-arm64": "0.28.1",
|
||||
"@esbuild/darwin-x64": "0.28.1",
|
||||
"@esbuild/freebsd-arm64": "0.28.1",
|
||||
"@esbuild/freebsd-x64": "0.28.1",
|
||||
"@esbuild/linux-arm": "0.28.1",
|
||||
"@esbuild/linux-arm64": "0.28.1",
|
||||
"@esbuild/linux-ia32": "0.28.1",
|
||||
"@esbuild/linux-loong64": "0.28.1",
|
||||
"@esbuild/linux-mips64el": "0.28.1",
|
||||
"@esbuild/linux-ppc64": "0.28.1",
|
||||
"@esbuild/linux-riscv64": "0.28.1",
|
||||
"@esbuild/linux-s390x": "0.28.1",
|
||||
"@esbuild/linux-x64": "0.28.1",
|
||||
"@esbuild/netbsd-arm64": "0.28.1",
|
||||
"@esbuild/netbsd-x64": "0.28.1",
|
||||
"@esbuild/openbsd-arm64": "0.28.1",
|
||||
"@esbuild/openbsd-x64": "0.28.1",
|
||||
"@esbuild/openharmony-arm64": "0.28.1",
|
||||
"@esbuild/sunos-x64": "0.28.1",
|
||||
"@esbuild/win32-arm64": "0.28.1",
|
||||
"@esbuild/win32-ia32": "0.28.1",
|
||||
"@esbuild/win32-x64": "0.28.1"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.4",
|
||||
"mime-types": "^2.1.35"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.12.26",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.26.tgz",
|
||||
"integrity": "sha512-uyZtpnYxM9CmQ7QsQknM4zN8EftNqhON1qYeIKM0Se67CCEe2c44xyGURwB0axX2fBDu1dqHrHAc1hmNT8ITkw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"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/proxy-from-env": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz",
|
||||
"integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.28.0"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
||||
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
|
||||
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
27
package.json
Normal file
27
package.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "AI-Relay-Kit",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"directories": {
|
||||
"doc": "docs"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "tsx src/index.ts",
|
||||
"dev": "tsx watch src/index.ts"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@hono/node-server": "2.0.5",
|
||||
"axios": "1.18.0",
|
||||
"dotenv": "17.4.2",
|
||||
"hono": "4.12.26"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "26.0.0",
|
||||
"tsx": "4.22.4",
|
||||
"typescript": "6.0.3"
|
||||
}
|
||||
}
|
||||
19
scripts/setup-ai-workspace.sh
Executable file
19
scripts/setup-ai-workspace.sh
Executable file
@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$DIR")"
|
||||
|
||||
echo "Starting AI-Relay-Kit Configuration Injection..."
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# Ensure dependencies are installed
|
||||
if [ ! -d "node_modules" ]; then
|
||||
echo "Installing dependencies..."
|
||||
npm install
|
||||
fi
|
||||
|
||||
# Run the injector
|
||||
npx tsx src/injector/index.ts
|
||||
|
||||
echo "Done."
|
||||
75
src/adapters/claude/parser.ts
Normal file
75
src/adapters/claude/parser.ts
Normal file
@ -0,0 +1,75 @@
|
||||
export function parseClaudeRequest(body: any): any {
|
||||
const messages = [...(body.messages || [])];
|
||||
|
||||
if (body.system) {
|
||||
messages.unshift({
|
||||
role: 'system',
|
||||
content: body.system
|
||||
});
|
||||
}
|
||||
|
||||
// We convert standard Anthropic tool structures to OpenAI if needed,
|
||||
// though LiteLLM usually handles standard OpenAI formats best.
|
||||
// Assuming simplified direct mapping for basic use-cases
|
||||
const chatReq = {
|
||||
model: body.model,
|
||||
messages: messages,
|
||||
stream: body.stream !== false,
|
||||
temperature: body.temperature,
|
||||
top_p: body.top_p,
|
||||
max_tokens: body.max_tokens,
|
||||
};
|
||||
|
||||
return chatReq;
|
||||
}
|
||||
|
||||
// Function to convert OpenAI Chat Completions chunk to Anthropic Messages chunk
|
||||
export function formatClaudeChunk(chatChunkStr: string, index: number): string | null {
|
||||
if (chatChunkStr === '[DONE]') {
|
||||
// We send message_stop event instead
|
||||
return JSON.stringify({ type: 'message_stop' });
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(chatChunkStr);
|
||||
const content = parsed.choices?.[0]?.delta?.content || '';
|
||||
|
||||
// In Anthropic streaming:
|
||||
// First event is message_start
|
||||
if (index === 0) {
|
||||
return JSON.stringify({
|
||||
type: 'message_start',
|
||||
message: {
|
||||
id: parsed.id || 'msg_123',
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
content: [],
|
||||
model: parsed.model,
|
||||
stop_reason: null,
|
||||
stop_sequence: null,
|
||||
usage: { input_tokens: 0, output_tokens: 0 }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (content) {
|
||||
return JSON.stringify({
|
||||
type: 'content_block_delta',
|
||||
index: 0,
|
||||
delta: { type: 'text_delta', text: content }
|
||||
});
|
||||
}
|
||||
|
||||
if (parsed.choices?.[0]?.finish_reason) {
|
||||
return JSON.stringify({
|
||||
type: 'message_delta',
|
||||
delta: { stop_reason: parsed.choices[0].finish_reason, stop_sequence: null },
|
||||
usage: { output_tokens: 0 }
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
80
src/adapters/claude/routes.ts
Normal file
80
src/adapters/claude/routes.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { Hono } from 'hono';
|
||||
import { stream } from 'hono/streaming';
|
||||
import { parseClaudeRequest, formatClaudeChunk } from './parser';
|
||||
import { proxyToLiteLLM } from '../../core/request_handler';
|
||||
|
||||
export const claudeRouter = new Hono();
|
||||
|
||||
claudeRouter.post('/messages', async (c) => {
|
||||
const body = await c.req.json();
|
||||
const chatReq = parseClaudeRequest(body);
|
||||
const headers = c.req.header();
|
||||
|
||||
// Anthropic API uses x-api-key instead of Authorization header often
|
||||
if (headers['x-api-key']) {
|
||||
headers['Authorization'] = `Bearer ${headers['x-api-key']}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await proxyToLiteLLM('/v1/chat/completions', 'POST', chatReq, headers, chatReq.stream ? 'stream' : 'json');
|
||||
|
||||
if (!chatReq.stream) {
|
||||
// Simplified non-streaming response format conversion
|
||||
const data = response.data;
|
||||
return c.json({
|
||||
id: data.id,
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: data.choices?.[0]?.message?.content || ''
|
||||
}
|
||||
],
|
||||
model: chatReq.model,
|
||||
stop_reason: data.choices?.[0]?.finish_reason || 'end_turn',
|
||||
stop_sequence: null,
|
||||
usage: {
|
||||
input_tokens: data.usage?.prompt_tokens || 0,
|
||||
output_tokens: data.usage?.completion_tokens || 0
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let index = 0;
|
||||
c.header('Content-Type', 'text/event-stream');
|
||||
c.header('Cache-Control', 'no-cache');
|
||||
c.header('Connection', 'keep-alive');
|
||||
|
||||
return stream(c, async (streamWriter) => {
|
||||
response.data.on('data', (chunk: Buffer) => {
|
||||
const lines = chunk.toString().split('\n').filter(line => line.trim() !== '');
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const dataStr = line.slice(6).trim();
|
||||
const transformed = formatClaudeChunk(dataStr, index++);
|
||||
|
||||
if (transformed) {
|
||||
streamWriter.write(`event: ${JSON.parse(transformed).type}\n`);
|
||||
streamWriter.write(`data: ${transformed}\n\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
response.data.on('end', () => {
|
||||
streamWriter.close();
|
||||
});
|
||||
|
||||
response.data.on('error', (err: any) => {
|
||||
console.error('Claude stream error:', err);
|
||||
streamWriter.abort();
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
c.status(error.response?.status || 500);
|
||||
return c.json(error.response?.data || { error: { message: error.message } });
|
||||
}
|
||||
});
|
||||
47
src/adapters/codex/parser.ts
Normal file
47
src/adapters/codex/parser.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { sessionCache } from '../../core/cache_manager';
|
||||
|
||||
export function parseCodexRequest(body: any): any {
|
||||
let messages = body.messages || [];
|
||||
|
||||
// If there's a previous_response_id, we prepend the history
|
||||
if (body.previous_response_id) {
|
||||
const session = sessionCache.getSession(body.previous_response_id);
|
||||
if (session && session.history) {
|
||||
messages = [...session.history, ...messages];
|
||||
}
|
||||
}
|
||||
|
||||
// Build the standard Chat Completions request
|
||||
const chatReq = {
|
||||
model: body.model,
|
||||
messages: messages,
|
||||
stream: body.stream !== false,
|
||||
temperature: body.temperature,
|
||||
top_p: body.top_p,
|
||||
max_tokens: body.max_tokens,
|
||||
tools: body.tools,
|
||||
tool_choice: body.tool_choice,
|
||||
};
|
||||
|
||||
return { chatReq, fullMessages: messages };
|
||||
}
|
||||
|
||||
// Function to handle the SSE stream transformation (if necessary)
|
||||
// Many times Codex just expects standard Chat Completion chunks,
|
||||
// but we may need to inject the new response_id so Codex can send it back next time.
|
||||
export function formatCodexChunk(chunkStr: string, responseId: string): string {
|
||||
// If the stream chunk is standard, we might not need heavy translation,
|
||||
// but we can ensure the ID is set.
|
||||
if (chunkStr.trim() === '[DONE]') {
|
||||
return chunkStr;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(chunkStr);
|
||||
data.response_id = responseId; // Inject response_id
|
||||
// some proprietary fields codex might expect
|
||||
return JSON.stringify(data);
|
||||
} catch (e) {
|
||||
return chunkStr;
|
||||
}
|
||||
}
|
||||
84
src/adapters/codex/routes.ts
Normal file
84
src/adapters/codex/routes.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { Hono } from 'hono';
|
||||
import { stream } from 'hono/streaming';
|
||||
import { parseCodexRequest, formatCodexChunk } from './parser';
|
||||
import { proxyToLiteLLM } from '../../core/request_handler';
|
||||
import { sessionCache } from '../../core/cache_manager';
|
||||
|
||||
export const codexRouter = new Hono();
|
||||
|
||||
codexRouter.post('/responses', async (c) => {
|
||||
const body = await c.req.json();
|
||||
const { chatReq, fullMessages } = parseCodexRequest(body);
|
||||
const headers = c.req.header();
|
||||
|
||||
try {
|
||||
const response = await proxyToLiteLLM('/v1/chat/completions', 'POST', chatReq, headers, chatReq.stream ? 'stream' : 'json');
|
||||
|
||||
// We generate a new response ID for this session
|
||||
const responseId = sessionCache.generateId();
|
||||
|
||||
// Save the message history immediately; we assume the assistant will append to this in real-time or we can just save what we have.
|
||||
// To be perfectly accurate, we should append the assistant's response to `fullMessages` as it streams.
|
||||
// For simplicity, we just save the full user request history, and the next turn the user will send it again or we can reconstruct.
|
||||
const currentSessionMessages = [...fullMessages];
|
||||
|
||||
if (!chatReq.stream) {
|
||||
const data = response.data;
|
||||
data.response_id = responseId;
|
||||
|
||||
// Append assistant message
|
||||
if (data.choices && data.choices[0] && data.choices[0].message) {
|
||||
currentSessionMessages.push(data.choices[0].message);
|
||||
sessionCache.saveSession(responseId, currentSessionMessages);
|
||||
}
|
||||
return c.json(data);
|
||||
}
|
||||
|
||||
// Handle Streaming
|
||||
let assistantMessage = '';
|
||||
|
||||
return stream(c, async (streamWriter) => {
|
||||
response.data.on('data', (chunk: Buffer) => {
|
||||
const lines = chunk.toString().split('\n').filter(line => line.trim() !== '');
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const dataStr = line.slice(6).trim();
|
||||
if (dataStr === '[DONE]') {
|
||||
streamWriter.write(`data: [DONE]\n\n`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(dataStr);
|
||||
if (parsed.choices && parsed.choices[0] && parsed.choices[0].delta && parsed.choices[0].delta.content) {
|
||||
assistantMessage += parsed.choices[0].delta.content;
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
const transformed = formatCodexChunk(dataStr, responseId);
|
||||
streamWriter.write(`data: ${transformed}\n\n`);
|
||||
} else {
|
||||
streamWriter.write(`${line}\n`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
response.data.on('end', () => {
|
||||
// Save history with full assistant message once stream is done
|
||||
currentSessionMessages.push({ role: 'assistant', content: assistantMessage });
|
||||
sessionCache.saveSession(responseId, currentSessionMessages);
|
||||
streamWriter.close();
|
||||
});
|
||||
|
||||
response.data.on('error', (err: any) => {
|
||||
console.error('Stream error:', err);
|
||||
streamWriter.abort();
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
c.status(error.response?.status || 500);
|
||||
return c.json(error.response?.data || { error: error.message });
|
||||
}
|
||||
});
|
||||
8
src/config.ts
Normal file
8
src/config.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
export const config = {
|
||||
port: parseInt(process.env.PORT || '4444', 10),
|
||||
litellmUrl: process.env.LITELLM_URL || 'http://127.0.0.1:4000',
|
||||
defaultApiKey: process.env.API_KEY || '',
|
||||
};
|
||||
67
src/core/cache_manager.ts
Normal file
67
src/core/cache_manager.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
interface SessionState {
|
||||
id: string;
|
||||
history: any[]; // Chat messages
|
||||
lastUpdatedAt: number;
|
||||
}
|
||||
|
||||
export class CacheManager {
|
||||
private sessions: Map<string, SessionState> = new Map();
|
||||
private maxSessions: number;
|
||||
private ttlMs: number;
|
||||
|
||||
constructor(maxSessions = 256, ttlHours = 168) {
|
||||
this.maxSessions = maxSessions;
|
||||
this.ttlMs = ttlHours * 3600 * 1000;
|
||||
}
|
||||
|
||||
public generateId(): string {
|
||||
return crypto.randomBytes(16).toString('hex');
|
||||
}
|
||||
|
||||
public getSession(id: string): SessionState | undefined {
|
||||
const session = this.sessions.get(id);
|
||||
if (session) {
|
||||
if (Date.now() - session.lastUpdatedAt > this.ttlMs) {
|
||||
this.sessions.delete(id);
|
||||
return undefined;
|
||||
}
|
||||
session.lastUpdatedAt = Date.now();
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
public saveSession(id: string, history: any[]): void {
|
||||
this.cleanup();
|
||||
this.sessions.set(id, {
|
||||
id,
|
||||
history,
|
||||
lastUpdatedAt: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
private cleanup(): void {
|
||||
const now = Date.now();
|
||||
for (const [id, session] of this.sessions.entries()) {
|
||||
if (now - session.lastUpdatedAt > this.ttlMs) {
|
||||
this.sessions.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.sessions.size >= this.maxSessions) {
|
||||
// delete oldest
|
||||
let oldestId: string | null = null;
|
||||
let oldestTime = Infinity;
|
||||
for (const [id, session] of this.sessions.entries()) {
|
||||
if (session.lastUpdatedAt < oldestTime) {
|
||||
oldestTime = session.lastUpdatedAt;
|
||||
oldestId = id;
|
||||
}
|
||||
}
|
||||
if (oldestId) this.sessions.delete(oldestId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const sessionCache = new CacheManager();
|
||||
37
src/core/request_handler.ts
Normal file
37
src/core/request_handler.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import { config } from '../config';
|
||||
|
||||
export async function proxyToLiteLLM(
|
||||
endpoint: string,
|
||||
method: 'GET' | 'POST' = 'POST',
|
||||
data?: any,
|
||||
headers?: any,
|
||||
responseType: 'json' | 'stream' = 'json'
|
||||
): Promise<AxiosResponse> {
|
||||
const url = `${config.litellmUrl}${endpoint}`;
|
||||
|
||||
// Strip out host and connection headers that might cause issues when proxying
|
||||
const safeHeaders = { ...headers };
|
||||
delete safeHeaders['host'];
|
||||
delete safeHeaders['connection'];
|
||||
delete safeHeaders['content-length'];
|
||||
|
||||
const reqConfig: AxiosRequestConfig = {
|
||||
method,
|
||||
url,
|
||||
data,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': safeHeaders.Authorization || `Bearer ${config.defaultApiKey}`,
|
||||
...safeHeaders
|
||||
},
|
||||
responseType
|
||||
};
|
||||
|
||||
try {
|
||||
return await axios(reqConfig);
|
||||
} catch (error: any) {
|
||||
console.error(`LiteLLM Proxy Error [${method} ${url}]:`, error.response?.data || error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
25
src/index.ts
Normal file
25
src/index.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { serve } from '@hono/node-server';
|
||||
import { Hono } from 'hono';
|
||||
import { logger } from 'hono/logger';
|
||||
import { cors } from 'hono/cors';
|
||||
import { config } from './config';
|
||||
import { codexRouter } from './adapters/codex/routes';
|
||||
import { claudeRouter } from './adapters/claude/routes';
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
app.use('*', logger());
|
||||
app.use('*', cors());
|
||||
|
||||
app.get('/health', (c) => c.json({ status: 'ok', service: 'AI-Relay-Kit', time: new Date().toISOString() }));
|
||||
|
||||
// Mount Adapters
|
||||
app.route('/codex/v1', codexRouter);
|
||||
app.route('/claude/v1', claudeRouter);
|
||||
|
||||
console.log(`Starting AI-Relay-Kit unified relay on port ${config.port}`);
|
||||
|
||||
serve({
|
||||
fetch: app.fetch,
|
||||
port: config.port,
|
||||
});
|
||||
26
src/injector/anti_injector.ts
Normal file
26
src/injector/anti_injector.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { config } from '../config';
|
||||
|
||||
export function injectAntigravityConfig() {
|
||||
const envFilePath = path.join(os.homedir(), '.ai-relay-kit.env');
|
||||
|
||||
let envContent = '';
|
||||
if (fs.existsSync(envFilePath)) {
|
||||
envContent = fs.readFileSync(envFilePath, 'utf8');
|
||||
}
|
||||
|
||||
// Antigravity (and other OpenAI compatible tools) can just point directly to LiteLLM
|
||||
const openaiBaseUrl = `${config.litellmUrl}/v1`;
|
||||
|
||||
const regex = /^export OPENAI_BASE_URL=.*$/m;
|
||||
if (regex.test(envContent)) {
|
||||
envContent = envContent.replace(regex, `export OPENAI_BASE_URL="${openaiBaseUrl}"`);
|
||||
} else {
|
||||
envContent += `\nexport OPENAI_BASE_URL="${openaiBaseUrl}"\n`;
|
||||
}
|
||||
|
||||
fs.writeFileSync(envFilePath, envContent.trim() + '\n', 'utf8');
|
||||
console.log(`[Antigravity] Updated OpenAI compatible environment variables in ${envFilePath}`);
|
||||
}
|
||||
27
src/injector/claude_injector.ts
Normal file
27
src/injector/claude_injector.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { config } from '../config';
|
||||
|
||||
export function injectClaudeConfig() {
|
||||
const envFilePath = path.join(os.homedir(), '.ai-relay-kit.env');
|
||||
|
||||
let envContent = '';
|
||||
if (fs.existsSync(envFilePath)) {
|
||||
envContent = fs.readFileSync(envFilePath, 'utf8');
|
||||
}
|
||||
|
||||
const claudeBaseUrl = `http://127.0.0.1:${config.port}/claude/v1`;
|
||||
|
||||
// Replace or append ANTHROPIC_BASE_URL
|
||||
const regex = /^export ANTHROPIC_BASE_URL=.*$/m;
|
||||
if (regex.test(envContent)) {
|
||||
envContent = envContent.replace(regex, `export ANTHROPIC_BASE_URL="${claudeBaseUrl}"`);
|
||||
} else {
|
||||
envContent += `\nexport ANTHROPIC_BASE_URL="${claudeBaseUrl}"\n`;
|
||||
}
|
||||
|
||||
// Claude CLI uses this to route API requests
|
||||
fs.writeFileSync(envFilePath, envContent.trim() + '\n', 'utf8');
|
||||
console.log(`[Claude] Updated Claude environment variables in ${envFilePath}`);
|
||||
}
|
||||
77
src/injector/codex_injector.ts
Normal file
77
src/injector/codex_injector.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { ModelDetails } from './fetch_models';
|
||||
import { config } from '../config';
|
||||
|
||||
const codexConfigPath = path.join(os.homedir(), '.codex', 'config.toml');
|
||||
const codexCatalogPath = path.join(os.homedir(), '.codex', 'custom_model_catalog.json');
|
||||
|
||||
export function injectCodexConfig(models: ModelDetails[]) {
|
||||
// 1. Generate catalog.json
|
||||
const catalog = {
|
||||
models: models.map(m => ({
|
||||
id: m.id,
|
||||
name: m.id,
|
||||
provider: "unified-relay"
|
||||
}))
|
||||
};
|
||||
fs.mkdirSync(path.dirname(codexCatalogPath), { recursive: true });
|
||||
fs.writeFileSync(codexCatalogPath, JSON.stringify(catalog, null, 2), 'utf8');
|
||||
console.log(`[Codex] Wrote model catalog to ${codexCatalogPath}`);
|
||||
|
||||
// 2. Modify config.toml
|
||||
let tomlContent = '';
|
||||
if (fs.existsSync(codexConfigPath)) {
|
||||
tomlContent = fs.readFileSync(codexConfigPath, 'utf8');
|
||||
}
|
||||
|
||||
// Ensure model_provider and model_catalog_json are set at the root
|
||||
const providerRegex = /^model_provider\s*=\s*".*?"/m;
|
||||
if (providerRegex.test(tomlContent)) {
|
||||
tomlContent = tomlContent.replace(providerRegex, 'model_provider = "unified-relay"');
|
||||
} else {
|
||||
tomlContent = `model_provider = "unified-relay"\n` + tomlContent;
|
||||
}
|
||||
|
||||
const catalogRegex = /^model_catalog_json\s*=\s*".*?"/m;
|
||||
if (catalogRegex.test(tomlContent)) {
|
||||
tomlContent = tomlContent.replace(catalogRegex, `model_catalog_json = "${codexCatalogPath}"`);
|
||||
} else {
|
||||
tomlContent = `model_catalog_json = "${codexCatalogPath}"\n` + tomlContent;
|
||||
}
|
||||
|
||||
// Clean up any old unified-relay blocks if they exist (simple approach: we just append a fresh one or replace)
|
||||
// For safety and idempotency in this script, we'll look for our specific marker
|
||||
const markerStart = '\n# --- UNIFIED RELAY CONFIG START ---';
|
||||
const markerEnd = '# --- UNIFIED RELAY CONFIG END ---\n';
|
||||
|
||||
if (tomlContent.includes(markerStart)) {
|
||||
const regex = new RegExp(`${markerStart}[\\s\\S]*?${markerEnd}`, 'g');
|
||||
tomlContent = tomlContent.replace(regex, '');
|
||||
}
|
||||
|
||||
// Build the new provider and properties block
|
||||
let providerBlock = `${markerStart}\n`;
|
||||
providerBlock += `[model_providers.unified-relay]\n`;
|
||||
providerBlock += `name = "Unified AI-Relay-Kit"\n`;
|
||||
providerBlock += `base_url = "http://127.0.0.1:${config.port}/codex/v1"\n`;
|
||||
providerBlock += `wire_api = "responses"\n`;
|
||||
providerBlock += `env_key = "LITELLM_API_KEY"\n\n`;
|
||||
|
||||
for (const m of models) {
|
||||
providerBlock += `[model_properties."${m.id}"]\n`;
|
||||
providerBlock += `context_window = 128000\n`;
|
||||
providerBlock += `max_context_window = 128000\n`;
|
||||
// Reasoning models usually don't support parallel tool calls
|
||||
const isReasoning = m.id.includes('reasoner') || m.id.includes('o1') || m.id.includes('o3');
|
||||
providerBlock += `supports_parallel_tool_calls = ${isReasoning ? 'false' : 'true'}\n`;
|
||||
providerBlock += `supports_reasoning_summaries = ${isReasoning ? 'true' : 'false'}\n`;
|
||||
providerBlock += `input_modalities = ["text"]\n\n`;
|
||||
}
|
||||
providerBlock += `${markerEnd}`;
|
||||
|
||||
tomlContent += providerBlock;
|
||||
fs.writeFileSync(codexConfigPath, tomlContent, 'utf8');
|
||||
console.log(`[Codex] Updated config.toml at ${codexConfigPath}`);
|
||||
}
|
||||
23
src/injector/fetch_models.ts
Normal file
23
src/injector/fetch_models.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import axios from 'axios';
|
||||
import { config } from '../config';
|
||||
|
||||
export interface ModelDetails {
|
||||
id: string;
|
||||
object: string;
|
||||
created: number;
|
||||
owned_by: string;
|
||||
}
|
||||
|
||||
export async function fetchLiteLLMModels(): Promise<ModelDetails[]> {
|
||||
try {
|
||||
const response = await axios.get(`${config.litellmUrl}/v1/models`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${config.defaultApiKey}`
|
||||
}
|
||||
});
|
||||
return response.data.data || [];
|
||||
} catch (error: any) {
|
||||
console.error('Failed to fetch models from LiteLLM:', error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
31
src/injector/iac_injector.ts
Normal file
31
src/injector/iac_injector.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { ModelDetails } from './fetch_models';
|
||||
import { config } from '../config';
|
||||
|
||||
export function injectIaCConfig(models: ModelDetails[]) {
|
||||
const iacDir = path.join(os.homedir(), '.ai-relay-kit', 'iac');
|
||||
fs.mkdirSync(iacDir, { recursive: true });
|
||||
|
||||
const aiRelayKitConfig = {
|
||||
relay_port: config.port,
|
||||
litellm_url: config.litellmUrl,
|
||||
models: models.map(m => m.id),
|
||||
};
|
||||
|
||||
// 1. Export for Ansible (ansible_vars.json)
|
||||
const ansibleVarsPath = path.join(iacDir, 'ansible_vars.json');
|
||||
const ansibleData = {
|
||||
ai_relay_kit_port: config.port,
|
||||
ai_relay_kit_litellm_url: config.litellmUrl,
|
||||
ai_relay_kit_available_models: models.map(m => m.id)
|
||||
};
|
||||
fs.writeFileSync(ansibleVarsPath, JSON.stringify(ansibleData, null, 2), 'utf8');
|
||||
console.log(`[IaC] Wrote Ansible variables to ${ansibleVarsPath}`);
|
||||
|
||||
// 2. Export for Terraform (terraform.tfvars.json)
|
||||
const tfVarsPath = path.join(iacDir, 'terraform.tfvars.json');
|
||||
fs.writeFileSync(tfVarsPath, JSON.stringify(aiRelayKitConfig, null, 2), 'utf8');
|
||||
console.log(`[IaC] Wrote Terraform tfvars to ${tfVarsPath}`);
|
||||
}
|
||||
37
src/injector/index.ts
Normal file
37
src/injector/index.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { fetchLiteLLMModels } from './fetch_models';
|
||||
import { injectCodexConfig } from './codex_injector';
|
||||
import { injectClaudeConfig } from './claude_injector';
|
||||
import { injectAntigravityConfig } from './anti_injector';
|
||||
import { injectIaCConfig } from './iac_injector';
|
||||
|
||||
async function runInjector() {
|
||||
console.log('--- Starting AI-Relay-Kit Config Injector ---');
|
||||
|
||||
const models = await fetchLiteLLMModels();
|
||||
|
||||
if (models.length === 0) {
|
||||
console.warn('⚠️ No models found from LiteLLM. Is LiteLLM running? Configurations might be incomplete.');
|
||||
} else {
|
||||
console.log(`✅ Fetched ${models.length} models from LiteLLM.`);
|
||||
}
|
||||
|
||||
// 1. Configure Codex
|
||||
injectCodexConfig(models);
|
||||
|
||||
// 2. Configure Claude Code
|
||||
injectClaudeConfig();
|
||||
|
||||
// 3. Configure Antigravity
|
||||
injectAntigravityConfig();
|
||||
|
||||
// 4. Export IaC Configurations
|
||||
injectIaCConfig(models);
|
||||
|
||||
console.log('--- Config Injection Complete ---');
|
||||
console.log('\n💡 To apply the environment variables, run:');
|
||||
console.log(' source ~/.ai-relay-kit.env');
|
||||
console.log('\n Or add this line to your ~/.zshrc or ~/.bashrc:');
|
||||
console.log(' source ~/.ai-relay-kit.env');
|
||||
}
|
||||
|
||||
runInjector().catch(console.error);
|
||||
44
tsconfig.json
Normal file
44
tsconfig.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
// Visit https://aka.ms/tsconfig to read more about this file
|
||||
"compilerOptions": {
|
||||
// File Layout
|
||||
// "rootDir": "./src",
|
||||
// "outDir": "./dist",
|
||||
|
||||
// Environment Settings
|
||||
// See also https://aka.ms/tsconfig/module
|
||||
"module": "nodenext",
|
||||
"target": "esnext",
|
||||
"types": [],
|
||||
// For nodejs:
|
||||
// "lib": ["esnext"],
|
||||
// "types": ["node"],
|
||||
// and npm install -D @types/node
|
||||
|
||||
// Other Outputs
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
|
||||
// Stricter Typechecking Options
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
|
||||
// Style Options
|
||||
// "noImplicitReturns": true,
|
||||
// "noImplicitOverride": true,
|
||||
// "noUnusedLocals": true,
|
||||
// "noUnusedParameters": true,
|
||||
// "noFallthroughCasesInSwitch": true,
|
||||
// "noPropertyAccessFromIndexSignature": true,
|
||||
|
||||
// Recommended Options
|
||||
"strict": true,
|
||||
"jsx": "react-jsx",
|
||||
"verbatimModuleSyntax": true,
|
||||
"isolatedModules": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"moduleDetection": "force",
|
||||
"skipLibCheck": true,
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user