├── .prettierrc ├── .dockerignore ├── server.ts ├── .editorconfig ├── .gitignore ├── package.json ├── tsconfig.json ├── Dockerfile ├── LICENSE ├── README.md ├── bun.lock ├── .github └── workflows │ ├── claude.yml │ └── claude-code-review.yml └── main.ts /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false 3 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | fly.toml 2 | node_modules 3 | dist 4 | .env 5 | Dockerfile 6 | .dockerignore 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | .git 11 | .gitignore 12 | -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | import {serve} from '@hono/node-server' 2 | import app from './main' 3 | 4 | const port= Number(process.env.PORT || '4000') 5 | serve({ 6 | fetch: app.fetch, 7 | port 8 | }) 9 | 10 | console.log(`http://localhost:${port}`) -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = false 12 | insert_final_newline = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies (bun install) 2 | node_modules 3 | 4 | # output 5 | out 6 | dist 7 | *.tgz 8 | 9 | # code coverage 10 | coverage 11 | *.lcov 12 | 13 | # logs 14 | logs 15 | _.log 16 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 17 | 18 | # dotenv environment variable files 19 | .env 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | .env.local 24 | 25 | # caches 26 | .eslintcache 27 | .cache 28 | *.tsbuildinfo 29 | 30 | # IntelliJ based IDEs 31 | .idea 32 | 33 | # Finder (MacOS) folder config 34 | .DS_Store 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ai-proxy", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "start": "node ./dist/server.js", 7 | "dev": "bunx tsx --watch ./server.ts", 8 | "build": "bun build ./server.ts ./main.ts --outdir dist --packages external" 9 | }, 10 | "devDependencies": { 11 | "@types/bun": "latest", 12 | "prettier": "^3.5.3", 13 | "typescript": "^5" 14 | }, 15 | "dependencies": { 16 | "@hono/node-server": "^1.14.0", 17 | "@hono/zod-validator": "^0.4.3", 18 | "hono": "^4.7.5", 19 | "zod": "^3.24.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # Build Stage 4 | FROM node:22-slim AS builder 5 | WORKDIR /app 6 | RUN npm i -g bun 7 | 8 | # Copy package files and install dependencies 9 | COPY package*.json ./ 10 | RUN bun i 11 | 12 | # Copy the rest of the application source code 13 | COPY . . 14 | 15 | # Build the TypeScript project 16 | # Assumes you have a "build" script in your package.json, e.g., "tsc -p tsconfig.json" 17 | RUN bun run build 18 | 19 | # Production Stage 20 | FROM node:22-slim AS production 21 | WORKDIR /app 22 | 23 | RUN apt-get update && apt-get install -y \ 24 | curl 25 | 26 | RUN npm i -g bun 27 | 28 | # Copy package.json 29 | COPY package.json ./ 30 | 31 | # Install production dependencies only 32 | RUN bun i --prod 33 | 34 | # Copy built code from the builder stage 35 | COPY --from=builder /app/dist ./dist 36 | 37 | ENV NODE_ENV=production 38 | ENV PORT=3000 39 | 40 | # Command to run the application 41 | # Assumes your entry point after build is dist/main.js 42 | CMD ["bun", "start"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 EGOIST 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AI Proxy 2 | 3 | This is a simple proxy for AI services. 4 | 5 | ## Sponsorship 6 | 7 | This project is sponsored by [ChatWise](https://chatwise.app), the fastest AI chatbot that works for any LLM. 8 | 9 | ## Usage 10 | 11 | Replace your API domain with the domain of the proxy deployed on your server. For example: 12 | 13 | - Gemini: 14 | - from `https://generativelanguage.googleapis.com/v1beta` 15 | - to`https://your-proxy/generativelanguage/v1beta` 16 | - OpenAI: 17 | - from `https://api.openai.com/v1` 18 | - to `https://your-proxy/openai/v1` 19 | - Anthropic: 20 | - from `https://api.anthropic.com/v1` 21 | - to `https://your-proxy/anthropic/v1` 22 | - Groq: 23 | - from `https://api.groq.com/openai/v1` 24 | - to `https://your-proxy/groq/openai/v1` 25 | - Perplexity: 26 | - from `https://api.perplexity.ai` 27 | - to `https://your-proxy/pplx` 28 | - Mistral: 29 | - from `https://api.mistral.ai` 30 | - to `https://your-proxy/mistral` 31 | - OpenRouter: 32 | - from `https://openrouter.ai/api` 33 | - to `https://your-proxy/openrouter` 34 | - xAI: 35 | - from `https://api.xai.ai` 36 | - to `https://your-proxy/xai` 37 | - Cerebras: 38 | - from `https://api.cerebras.ai` 39 | - to `https://your-proxy/cerebras` 40 | 41 | ## Hosted by ChatWise 42 | 43 | Use the hosted API, for example OpenAI `https://ai-proxy.chatwise.app/openai/v1` 44 | 45 | ## Deployment 46 | 47 | Deploy this as a Docker container, check out [Dockerfile](./Dockerfile). 48 | 49 | ## License 50 | 51 | MIT. 52 | -------------------------------------------------------------------------------- /bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "workspaces": { 4 | "": { 5 | "name": "ai-proxy", 6 | "dependencies": { 7 | "@hono/node-server": "^1.14.0", 8 | "@hono/zod-validator": "^0.4.3", 9 | "hono": "^4.7.5", 10 | "zod": "^3.24.2", 11 | }, 12 | "devDependencies": { 13 | "@types/bun": "latest", 14 | "prettier": "^3.5.3", 15 | "typescript": "^5", 16 | }, 17 | }, 18 | }, 19 | "packages": { 20 | "@hono/node-server": ["@hono/node-server@1.14.0", "", { "peerDependencies": { "hono": "^4" } }, "sha512-YUCxJwgHRKSqjrdTk9e4VMGKN27MK5r4+MGPyZTgKH+IYbK+KtYbHeOcPGJ91KGGD6RIQiz2dAHxvjauNhOS8g=="], 21 | 22 | "@hono/zod-validator": ["@hono/zod-validator@0.4.3", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-xIgMYXDyJ4Hj6ekm9T9Y27s080Nl9NXHcJkOvkXPhubOLj8hZkOL8pDnnXfvCf5xEE8Q4oMFenQUZZREUY2gqQ=="], 23 | 24 | "@types/bun": ["@types/bun@1.2.8", "", { "dependencies": { "bun-types": "1.2.7" } }, "sha512-t8L1RvJVUghW5V+M/fL3Thbxcs0HwNsXsnTEBEfEVqGteiJToOlZ/fyOEaR1kZsNqnu+3XA4RI/qmnX4w6+S+w=="], 25 | 26 | "@types/node": ["@types/node@22.14.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA=="], 27 | 28 | "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], 29 | 30 | "bun-types": ["bun-types@1.2.7", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-P4hHhk7kjF99acXqKvltyuMQ2kf/rzIw3ylEDpCxDS9Xa0X0Yp/gJu/vDCucmWpiur5qJ0lwB2bWzOXa2GlHqA=="], 31 | 32 | "hono": ["hono@4.7.5", "", {}, "sha512-fDOK5W2C1vZACsgLONigdZTRZxuBqFtcKh7bUQ5cVSbwI2RWjloJDcgFOVzbQrlI6pCmhlTsVYZ7zpLj4m4qMQ=="], 33 | 34 | "prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="], 35 | 36 | "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], 37 | 38 | "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], 39 | 40 | "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/claude.yml: -------------------------------------------------------------------------------- 1 | name: Claude Code 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | pull_request_review_comment: 7 | types: [created] 8 | issues: 9 | types: [opened, assigned] 10 | pull_request_review: 11 | types: [submitted] 12 | 13 | jobs: 14 | claude: 15 | if: | 16 | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || 17 | (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || 18 | (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || 19 | (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) 20 | runs-on: ubuntu-latest 21 | permissions: 22 | contents: read 23 | pull-requests: read 24 | issues: read 25 | id-token: write 26 | actions: read # Required for Claude to read CI results on PRs 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 1 32 | 33 | - name: Run Claude Code 34 | id: claude 35 | uses: anthropics/claude-code-action@beta 36 | with: 37 | claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} 38 | 39 | # This is an optional setting that allows Claude to read CI results on PRs 40 | additional_permissions: | 41 | actions: read 42 | 43 | # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) 44 | # model: "claude-opus-4-20250514" 45 | 46 | # Optional: Customize the trigger phrase (default: @claude) 47 | # trigger_phrase: "/claude" 48 | 49 | # Optional: Trigger when specific user is assigned to an issue 50 | # assignee_trigger: "claude-bot" 51 | 52 | # Optional: Allow Claude to run specific commands 53 | # allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)" 54 | 55 | # Optional: Add custom instructions for Claude to customize its behavior for your project 56 | # custom_instructions: | 57 | # Follow our coding standards 58 | # Ensure all new code has tests 59 | # Use TypeScript for new files 60 | 61 | # Optional: Custom environment variables for Claude 62 | # claude_env: | 63 | # NODE_ENV: test 64 | 65 | -------------------------------------------------------------------------------- /.github/workflows/claude-code-review.yml: -------------------------------------------------------------------------------- 1 | name: Claude Code Review 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize] 6 | # Optional: Only run on specific file changes 7 | # paths: 8 | # - "src/**/*.ts" 9 | # - "src/**/*.tsx" 10 | # - "src/**/*.js" 11 | # - "src/**/*.jsx" 12 | 13 | jobs: 14 | claude-review: 15 | # Optional: Filter by PR author 16 | # if: | 17 | # github.event.pull_request.user.login == 'external-contributor' || 18 | # github.event.pull_request.user.login == 'new-developer' || 19 | # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' 20 | 21 | runs-on: ubuntu-latest 22 | permissions: 23 | contents: read 24 | pull-requests: read 25 | issues: read 26 | id-token: write 27 | 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v4 31 | with: 32 | fetch-depth: 1 33 | 34 | - name: Run Claude Code Review 35 | id: claude-review 36 | uses: anthropics/claude-code-action@beta 37 | with: 38 | claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} 39 | 40 | # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) 41 | # model: "claude-opus-4-20250514" 42 | 43 | # Direct prompt for automated review (no @claude mention needed) 44 | direct_prompt: | 45 | Please review this pull request and provide feedback on: 46 | - Code quality and best practices 47 | - Potential bugs or issues 48 | - Performance considerations 49 | - Security concerns 50 | - Test coverage 51 | 52 | Be constructive and helpful in your feedback. 53 | 54 | # Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR 55 | # use_sticky_comment: true 56 | 57 | # Optional: Customize review based on file types 58 | # direct_prompt: | 59 | # Review this PR focusing on: 60 | # - For TypeScript files: Type safety and proper interface usage 61 | # - For API endpoints: Security, input validation, and error handling 62 | # - For React components: Performance, accessibility, and best practices 63 | # - For tests: Coverage, edge cases, and test quality 64 | 65 | # Optional: Different prompts for different authors 66 | # direct_prompt: | 67 | # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' && 68 | # 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' || 69 | # 'Please provide a thorough code review focusing on our coding standards and best practices.' }} 70 | 71 | # Optional: Add specific tools for running tests or linting 72 | # allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)" 73 | 74 | # Optional: Skip review for certain conditions 75 | # if: | 76 | # !contains(github.event.pull_request.title, '[skip-review]') && 77 | # !contains(github.event.pull_request.title, '[WIP]') 78 | 79 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono" 2 | import { cors } from "hono/cors" 3 | import { zValidator } from "@hono/zod-validator" 4 | import { z } from "zod" 5 | import { logger } from "hono/logger" 6 | import { proxy } from "hono/proxy" 7 | 8 | const app = new Hono() 9 | 10 | app.use(cors()) 11 | 12 | app.use(logger()) 13 | 14 | app.use(async (c, next) => { 15 | await next() 16 | c.res.headers.set("X-Accel-Buffering", "no") 17 | }) 18 | 19 | app.get("/", (c) => c.text("A proxy for AI!")) 20 | 21 | const fetchWithTimeout = async ( 22 | url: string, 23 | { timeout, ...options }: RequestInit & { timeout: number }, 24 | ) => { 25 | const controller = new AbortController() 26 | 27 | const timeoutId = setTimeout(() => { 28 | controller.abort() 29 | }, timeout) 30 | 31 | try { 32 | const res = await proxy(url, { 33 | ...options, 34 | signal: controller.signal, 35 | // @ts-expect-error 36 | duplex: "half", 37 | }) 38 | clearTimeout(timeoutId) 39 | return res 40 | } catch (error) { 41 | clearTimeout(timeoutId) 42 | if (controller.signal.aborted) { 43 | return new Response("Request timeout", { 44 | status: 504, 45 | }) 46 | } 47 | 48 | throw error 49 | } 50 | } 51 | 52 | const proxies: { pathSegment: string; target: string; orHostname?: string }[] = 53 | [ 54 | { 55 | pathSegment: "generativelanguage", 56 | orHostname: "gooai.chatkit.app", 57 | target: "https://generativelanguage.googleapis.com", 58 | }, 59 | { 60 | pathSegment: "groq", 61 | target: "https://api.groq.com", 62 | }, 63 | { 64 | pathSegment: "anthropic", 65 | target: "https://api.anthropic.com", 66 | }, 67 | { 68 | pathSegment: "pplx", 69 | target: "https://api.perplexity.ai", 70 | }, 71 | { 72 | pathSegment: "openai", 73 | target: "https://api.openai.com", 74 | }, 75 | { 76 | pathSegment: "mistral", 77 | target: "https://api.mistral.ai", 78 | }, 79 | { 80 | pathSegment: "openrouter/api", 81 | target: "https://openrouter.ai/api", 82 | }, 83 | { 84 | pathSegment: "openrouter", 85 | target: "https://openrouter.ai/api", 86 | }, 87 | { 88 | pathSegment: "xai", 89 | target: "https://api.x.ai", 90 | }, 91 | { 92 | pathSegment: "cerebras", 93 | target: "https://api.cerebras.ai", 94 | }, 95 | { 96 | pathSegment: "googleapis-cloudcode-pa", 97 | target: "https://cloudcode-pa.googleapis.com", 98 | }, 99 | ] 100 | 101 | app.post( 102 | "/custom-model-proxy", 103 | zValidator( 104 | "query", 105 | z.object({ 106 | url: z.string().url(), 107 | }), 108 | ), 109 | async (c) => { 110 | const { url } = c.req.valid("query") 111 | 112 | const res = await proxy(url, { 113 | method: c.req.method, 114 | body: c.req.raw.body, 115 | headers: c.req.raw.headers, 116 | }) 117 | 118 | return new Response(res.body, { 119 | headers: res.headers, 120 | status: res.status, 121 | }) 122 | }, 123 | ) 124 | 125 | app.use(async (c, next) => { 126 | const url = new URL(c.req.url) 127 | 128 | const proxy = proxies.find( 129 | (p) => 130 | url.pathname.startsWith(`/${p.pathSegment}/`) || 131 | (p.orHostname && url.hostname === p.orHostname), 132 | ) 133 | 134 | if (proxy) { 135 | const headers = new Headers() 136 | headers.set("host", new URL(proxy.target).hostname) 137 | 138 | c.req.raw.headers.forEach((value, key) => { 139 | const k = key.toLowerCase() 140 | if ( 141 | !k.startsWith("cf-") && 142 | !k.startsWith("x-forwarded-") && 143 | !k.startsWith("cdn-") && 144 | k !== "x-real-ip" && 145 | k !== "host" 146 | ) { 147 | headers.set(key, value) 148 | } 149 | }) 150 | 151 | const targetUrl = `${proxy.target}${url.pathname.replace( 152 | `/${proxy.pathSegment}/`, 153 | "/", 154 | )}${url.search}` 155 | 156 | const res = await fetchWithTimeout(targetUrl, { 157 | method: c.req.method, 158 | headers, 159 | body: c.req.raw.body, 160 | timeout: 60000, 161 | }) 162 | 163 | return new Response(res.body, { 164 | headers: res.headers, 165 | status: res.status, 166 | }) 167 | } 168 | 169 | next() 170 | }) 171 | 172 | export default app 173 | --------------------------------------------------------------------------------