caidaoli commited on
Commit
d7f796c
·
1 Parent(s): e5c64ef

feat: Initialize Claude to OpenAI API Proxy with core functionality

Browse files

- Add go.mod and go.sum for dependency management
- Implement main application logic in main.go
- Define data structures for Claude and OpenAI requests/responses
- Set up HTTP server with Gin framework and CORS support
- Create proxy handling for requests to OpenAI API
- Implement request conversion from Claude format to OpenAI format
- Add error handling and logging for debugging purposes
- Create test script (test.sh) for basic functionality and health checks

Files changed (10) hide show
  1. .env.example +36 -0
  2. .gitignore +52 -0
  3. CLAUDE.md +193 -0
  4. Dockerfile +22 -0
  5. README.md +259 -1
  6. claude_proxy.sh +273 -0
  7. go.mod +35 -0
  8. go.sum +88 -0
  9. main.go +610 -0
  10. test.sh +56 -0
.env.example ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environment variables for local development
2
+ # Copy this file to .env and set your own values
3
+
4
+ # Server configuration
5
+ PORT=8080
6
+
7
+ # Haiku model shortcut configuration (optional)
8
+ # If set, you can use paths containing "haiku" to access this model
9
+ HAIKU_BASE_URL=https://api.anthropic.com/v1
10
+ HAIKU_MODEL=claude-3-haiku-20240307
11
+
12
+ # Example configurations for different APIs:
13
+
14
+ # Google Gemini
15
+ # HAIKU_BASE_URL=https://generativelanguage.googleapis.com/v1beta
16
+ # HAIKU_MODEL=gemini-1.5-pro
17
+
18
+ # Groq
19
+ # HAIKU_BASE_URL=https://api.groq.com/openai/v1
20
+ # HAIKU_MODEL=llama3-70b-8192
21
+
22
+ # OpenAI
23
+ # HAIKU_BASE_URL=https://api.openai.com/v1
24
+ # HAIKU_MODEL=gpt-4
25
+
26
+ # Ollama (local)
27
+ # HAIKU_BASE_URL=http://localhost:11434/v1
28
+ # HAIKU_MODEL=llama3
29
+
30
+ # Debug mode (set to "true" to enable debug logging)
31
+ DEBUG=false
32
+
33
+ # CORS configuration (optional)
34
+ # CORS_ALLOW_ORIGIN=*
35
+ # CORS_ALLOW_METHODS=POST,GET,OPTIONS,PUT,DELETE
36
+ # CORS_ALLOW_HEADERS=Accept,Content-Type,Content-Length,Accept-Encoding,X-CSRF-Token,Authorization,x-api-key,anthropic-version
.gitignore ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Binaries for programs and plugins
2
+ *.exe
3
+ *.exe~
4
+ *.dll
5
+ *.so
6
+ *.dylib
7
+
8
+ # Test binary, built with `go test -c`
9
+ *.test
10
+
11
+ # Output of the go coverage tool, specifically when used with LiteIDE
12
+ *.out
13
+
14
+ # Go workspace file
15
+ go.work
16
+ go.work.sum
17
+
18
+ # Compiled binary
19
+ proxy
20
+ main
21
+
22
+ # Log files
23
+ *.log
24
+
25
+ # Environment variables
26
+ .env
27
+ .env.local
28
+
29
+ # IDE files
30
+ .vscode/
31
+ .idea/
32
+ *.swp
33
+ *.swo
34
+ *~
35
+
36
+ # OS generated files
37
+ .DS_Store
38
+ .DS_Store?
39
+ ._*
40
+ .Spotlight-V100
41
+ .Trashes
42
+ ehthumbs.db
43
+ Thumbs.db
44
+
45
+ # Configuration backups
46
+ *.bak
47
+ .claude_proxy_config
48
+
49
+ # Temporary files
50
+ tmp/
51
+ temp/
52
+ .claude/settings.local.json
CLAUDE.md ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CLAUDE.md
2
+
3
+ 这个文件为 Claude Code 提供工作指导,帮助理解这个代理服务器项目。
4
+
5
+ ## 📋 项目简介
6
+
7
+ 这是一个 **API 代理服务器**,主要作用是:
8
+ - 将 Claude API 格式的请求转换为 OpenAI API 格式
9
+ - 让 Claude CLI 能够与各种 OpenAI 兼容的 API 服务通信
10
+ - 支持多种 AI 服务(如 DeepSeek、Groq、Gemini 等)
11
+
12
+ ## 🎯 核心功能
13
+
14
+ ### 主要特性
15
+ - **URL 路径解析**:自动从 URL 中提取目标 API 服务器和模型信息
16
+ - **格式转换**:Claude API ↔ OpenAI API 双向转换
17
+ - **流式响应**:支持流式和非流式响应
18
+ - **错误处理**:完整的错误处理和调试日志
19
+ - **CORS 支持**:支持跨域请求
20
+
21
+ ### 技术栈
22
+ - **语言**:Go
23
+ - **框架**:Gin
24
+ - **部署**:Docker 容器化
25
+ - **平台**:Hugging Face Space
26
+
27
+ ## 📁 文件结构
28
+
29
+ ```
30
+ 项目根目录/
31
+ ├── main.go # 核心代理服务器代码
32
+ ├── go.mod # Go 模块依赖
33
+ ├── go.sum # 依赖校验文件
34
+ ├── Dockerfile # Docker 构建配置
35
+ ├── claude_proxy.sh # Claude CLI 配置脚本
36
+ ├── test.sh # 功能测试脚本
37
+ ├── .env.example # 环境变量模板
38
+ ├── .env # 本地环境变量(git 忽略)
39
+ ├── .gitignore # Git 忽略配置
40
+ ├── README.md # 项目说明文档
41
+ └── CLAUDE.md # 本文件
42
+ ```
43
+
44
+ ## ⚙️ 环境配置
45
+
46
+ ### 重要环境变量
47
+
48
+ ```bash
49
+ # 服务器配置
50
+ PORT=8080 # 服务器端口
51
+ DEBUG=true # 开启调试模式
52
+
53
+ # Haiku 快捷方式(可选)
54
+ HAIKU_BASE_URL=https://api.anthropic.com/v1
55
+ HAIKU_MODEL=claude-3-haiku-20240307
56
+
57
+ # CORS 配置
58
+ CORS_ALLOW_ORIGIN=*
59
+ CORS_ALLOW_METHODS=POST,GET,OPTIONS,PUT,DELETE
60
+ CORS_ALLOW_HEADERS=Accept,Content-Type,Authorization,x-api-key
61
+ ```
62
+
63
+ ### 配置加载顺序
64
+ 1. `.env` 文件
65
+ 2. 系统环境变量
66
+ 3. 默认值
67
+
68
+ ## 🌐 API 使用说明
69
+
70
+ ### URL 格式
71
+ ```
72
+ /<协议>/<域名>/<路径>/<模型名>/v1/messages
73
+ ```
74
+
75
+ ### 使用示例
76
+
77
+ ```bash
78
+ # DeepSeek API
79
+ POST /https/voapi.16931.com/openai/v1/deepseek-v3-fast/v1/messages
80
+
81
+ # Groq API
82
+ POST /https/api.groq.com/openai/v1/llama3-70b-8192/v1/messages
83
+
84
+ # Gemini API
85
+ POST /https/generativelanguage.googleapis.com/v1beta/gemini-1.5-pro/v1/messages
86
+
87
+ # Haiku 快捷方式(需要设置环境变量)
88
+ POST /haiku/v1/messages
89
+ ```
90
+
91
+ ### 请求头部
92
+ 代理服务器支持以下 API 密钥头部格式:
93
+ - `x-api-key: your-api-key`
94
+ - `Authorization: Bearer your-api-key`
95
+ - `anthropic-api-key: your-api-key`
96
+
97
+ ## 🔄 工作流程
98
+
99
+ 1. **接收请求**:Claude CLI 发送 Claude API 格式的请求
100
+ 2. **解析 URL**:从路径中提取协议、域名、路径和模型信息
101
+ 3. **格式转换**:将 Claude 请求格式转换为 OpenAI 格式
102
+ 4. **转发请求**:向目标 API 发送转换后的请求
103
+ 5. **处理响应**:将 OpenAI 响应转换回 Claude 格式
104
+ 6. **返回结果**:将转换后的响应返回给客户端
105
+
106
+ ## 🛠️ 开发和测试
107
+
108
+ ### 本地开发
109
+ ```bash
110
+ # 1. 安装依赖
111
+ go mod tidy
112
+
113
+ # 2. 配置环境变量
114
+ cp .env.example .env
115
+ # 编辑 .env 文件
116
+
117
+ # 3. 运行服务器
118
+ go run main.go
119
+ ```
120
+
121
+ ### 健康检查
122
+ ```bash
123
+ curl http://localhost:8080/
124
+
125
+ # 预期响应
126
+ {
127
+ "message": "Claude to OpenAI API Proxy",
128
+ "version": "1.0.0",
129
+ "environment": "debug"
130
+ }
131
+ ```
132
+
133
+ ### 调试技巧
134
+ - 设置 `DEBUG=true` 查看详细日志
135
+ - 查看请求和响应的完整调试信息
136
+ - 使用 `./test.sh` 进行基本功能测试
137
+
138
+ ## 🚀 部署说明
139
+
140
+ ### Hugging Face Space 部署
141
+ - 专为 Hugging Face Space 优化
142
+ - 使用 Docker 容器化部署
143
+ - 支持通过 Space 设置页面配置环境变量
144
+ - 自动处理容器生命周期
145
+
146
+ ### Docker 特性
147
+ - 多阶段构建优化镜像大小
148
+ - 非 root 用户运行增强安全性
149
+ - 健康检查和日志输出
150
+ - 环境变量注入支持
151
+
152
+ ## 🐛 故障排除
153
+
154
+ ### 常见问题
155
+
156
+ | 问题 | 解决方案 |
157
+ |------|---------|
158
+ | 400 错误(请求格式错误) | 检查 URL 格式是否正确 |
159
+ | 401 错误(未授权) | 检查 API 密钥是否正确设置 |
160
+ | 环境变量未生效 | 检查 `.env` 文件格式,确保没有多余空格 |
161
+ | 端口冲突 | 修改 `PORT` 环境变量 |
162
+ | CORS 错误 | 检查 `CORS_*` 环境变量配置 |
163
+
164
+ ### 调试步骤
165
+ 1. 开启调试模式:`DEBUG=true`
166
+ 2. 查看服务器日志
167
+ 3. 检查请求和响应的调试信息
168
+ 4. 验证目标 API 的 URL 格式和密钥
169
+
170
+ ## ✅ 项目定位
171
+
172
+ ### 这个项目是:
173
+ - API 代理服务器
174
+ - Claude API 到 OpenAI API 的格式转换器
175
+ - 容器化的 Go 应用程序
176
+ - 部署在 Hugging Face Space 的服务
177
+
178
+ ### 这个项目不是:
179
+ - Cloudflare Workers 项目
180
+ - 传统的 Web 应用程序
181
+ - 需要前端界面的应用
182
+ - 直接的 AI 服务提供商
183
+
184
+ ## 🎯 主要用途
185
+
186
+ - 让 Claude CLI 与各种 OpenAI 兼容的 API 服务通信
187
+ - 作为不同 AI 服务的统���接入点
188
+ - 提供 API 格式转换的中间层服务
189
+ - 支持多种 AI 模型的统一调用接口
190
+
191
+ ---
192
+
193
+ **注意**:这个代理服务器主要用于开发和测试环境,生产环境使用时请确保妥善处理 API 密钥和安全配置。
Dockerfile ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM golang:1.21-alpine AS builder
2
+
3
+ WORKDIR /app
4
+
5
+ COPY go.mod go.sum ./
6
+ RUN go mod download
7
+
8
+ COPY . .
9
+
10
+ RUN CGO_ENABLED=0 GOOS=linux go build -o main .
11
+
12
+ FROM alpine:latest
13
+
14
+ RUN apk --no-cache add ca-certificates
15
+
16
+ WORKDIR /root/
17
+
18
+ COPY --from=builder /app/main .
19
+
20
+ EXPOSE 8080
21
+
22
+ CMD ["./main"]
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: Cc
3
  emoji: 🔥
4
  colorFrom: red
5
  colorTo: red
@@ -8,3 +8,261 @@ pinned: false
8
  ---
9
 
10
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Claude Code to OpenAI API Proxy
3
  emoji: 🔥
4
  colorFrom: red
5
  colorTo: red
 
8
  ---
9
 
10
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
11
+
12
+ # Claude to OpenAI API Proxy
13
+
14
+ 🔥 一个强大的Go代理服务器,将Claude API格式的请求无缝转换为OpenAI API格式,使Claude CLI能够与任何OpenAI兼容的服务通信。
15
+
16
+ **部署在 Hugging Face Space 上的容器化应用**
17
+
18
+ ## ✨ 功能特性
19
+
20
+ - **动态路由**:无需修改代码,通过URL路径动态指定目标API和模型
21
+ - **全功能API兼容**:完全支持Claude的`/v1/messages`端点,包括流式和非流式响应
22
+ - **Tool Calling转换**:智能转换Claude的tools格式为OpenAI格式
23
+ - **Haiku模型快捷方式**:支持环境变量配置固定路由
24
+ - **一键配置脚本**:交互式配置Claude CLI工具
25
+ - **环境变量配置**:支持.env文件进行本地开发配置
26
+
27
+ ## 🔬 工作原理
28
+
29
+ ### 动态路由格式
30
+ ```
31
+ https://<代理地址>/<协议>/<目标API域名>/<路径>/<模型名称>/v1/messages
32
+ ```
33
+
34
+ ### 处理流程
35
+ 1. 解析URL提取目标API和模型信息
36
+ 2. 转发`x-api-key`头部作为`Authorization: Bearer <key>`
37
+ 3. 将Claude格式请求转换为OpenAI格式
38
+ 4. 发送到目标API的`/chat/completions`端点
39
+ 5. 将OpenAI响应转换回Claude格式
40
+
41
+ ## 🚀 快速开始
42
+
43
+ ### 本地开发
44
+
45
+ 1. 克隆项目:
46
+ ```bash
47
+ git clone <repository-url>
48
+ cd openai2claudecode
49
+ ```
50
+
51
+ 2. 配置环境变量:
52
+ ```bash
53
+ cp .env.example .env
54
+ # 编辑 .env 文件设置您的配置
55
+ ```
56
+
57
+ 3. 安装依赖:
58
+ ```bash
59
+ go mod tidy
60
+ ```
61
+
62
+ 4. 运行服务器:
63
+ ```bash
64
+ go run main.go
65
+ ```
66
+
67
+ 服务器将在`http://localhost:8080`启动。
68
+
69
+ ### 使用配置脚本
70
+
71
+ 运行一键配置脚本来设置Claude CLI:
72
+ ```bash
73
+ ./claude_proxy.sh
74
+ ```
75
+
76
+ 脚本会引导您:
77
+ - 安装Claude CLI(如果需要)
78
+ - 选择API服务(Google Gemini、Groq、OpenAI等)
79
+ - 配置API密钥
80
+ - 设置代理URL
81
+ - 测试配置
82
+
83
+ ### Docker部署
84
+
85
+ 1. 构建Docker镜像:
86
+ ```bash
87
+ docker build -t claude-proxy .
88
+ ```
89
+
90
+ 2. 运行容器:
91
+ ```bash
92
+ docker run -p 8080:8080 \
93
+ -e HAIKU_BASE_URL=https://api.anthropic.com/v1 \
94
+ -e HAIKU_MODEL=claude-3-haiku-20240307 \
95
+ -e DEBUG=false \
96
+ claude-proxy
97
+ ```
98
+
99
+ ## 📖 使用示例
100
+
101
+ ### Google Gemini
102
+ ```bash
103
+ curl -X POST http://localhost:8080/https/generativelanguage.googleapis.com/v1beta/gemini-1.5-pro/v1/messages \
104
+ -H 'Content-Type: application/json' \
105
+ -H 'x-api-key: YOUR_GEMINI_API_KEY' \
106
+ -d '{
107
+ "model": "gemini-1.5-pro",
108
+ "max_tokens": 1000,
109
+ "messages": [{"role": "user", "content": "Hello!"}]
110
+ }'
111
+ ```
112
+
113
+ ### Groq
114
+ ```bash
115
+ curl -X POST http://localhost:8080/https/api.groq.com/openai/v1/llama3-70b-8192/v1/messages \
116
+ -H 'Content-Type: application/json' \
117
+ -H 'x-api-key: YOUR_GROQ_API_KEY' \
118
+ -d '{
119
+ "model": "llama3-70b-8192",
120
+ "max_tokens": 1000,
121
+ "messages": [{"role": "user", "content": "Hello!"}]
122
+ }'
123
+ ```
124
+
125
+ ### OpenAI
126
+ ```bash
127
+ curl -X POST http://localhost:8080/https/api.openai.com/v1/gpt-4/v1/messages \
128
+ -H 'Content-Type: application/json' \
129
+ -H 'x-api-key: YOUR_OPENAI_API_KEY' \
130
+ -d '{
131
+ "model": "gpt-4",
132
+ "max_tokens": 1000,
133
+ "messages": [{"role": "user", "content": "Hello!"}]
134
+ }'
135
+ ```
136
+
137
+ ### 流式响应
138
+ 添加`"stream": true`到请求体中启用流式响应:
139
+ ```json
140
+ {
141
+ "model": "gpt-4",
142
+ "max_tokens": 1000,
143
+ "messages": [{"role": "user", "content": "Hello!"}],
144
+ "stream": true
145
+ }
146
+ ```
147
+
148
+ ### Tool Calling
149
+ ```json
150
+ {
151
+ "model": "gpt-4",
152
+ "max_tokens": 1000,
153
+ "messages": [{"role": "user", "content": "What's the weather like?"}],
154
+ "tools": [
155
+ {
156
+ "name": "get_weather",
157
+ "description": "Get the current weather",
158
+ "input_schema": {
159
+ "type": "object",
160
+ "properties": {
161
+ "location": {"type": "string"}
162
+ },
163
+ "required": ["location"]
164
+ }
165
+ }
166
+ ]
167
+ }
168
+ ```
169
+
170
+ ## ⚙️ 配置
171
+
172
+ ### 环境变量
173
+
174
+ 创建`.env`文件来配置本地开发环境:
175
+
176
+ ```bash
177
+ # 服务器配置
178
+ PORT=8080
179
+ DEBUG=true
180
+
181
+ # Haiku模型快捷方式(可选)
182
+ HAIKU_BASE_URL=https://api.anthropic.com/v1
183
+ HAIKU_MODEL=claude-3-haiku-20240307
184
+
185
+ # CORS配置(可选)
186
+ CORS_ALLOW_ORIGIN=*
187
+ CORS_ALLOW_METHODS=POST,GET,OPTIONS,PUT,DELETE
188
+ CORS_ALLOW_HEADERS=Accept,Content-Type,Content-Length,Accept-Encoding,X-CSRF-Token,Authorization,x-api-key,anthropic-version
189
+ ```
190
+
191
+ ### Haiku快捷方式
192
+
193
+ 设置环境变量后,可以使用简化的URL:
194
+ ```bash
195
+ # 然后可以使用包含"haiku"的任何路径
196
+ curl -X POST http://localhost:8080/haiku/v1/messages \
197
+ -H 'Content-Type: application/json' \
198
+ -H 'x-api-key: YOUR_ANTHROPIC_API_KEY' \
199
+ -d '{...}'
200
+ ```
201
+
202
+ ## 🔧 开发
203
+
204
+ ### 项目结构
205
+ ```
206
+ .
207
+ ├── main.go # 主服务器代码
208
+ ├── go.mod # Go模块依赖
209
+ ├── go.sum # Go模块校验和
210
+ ├── claude_proxy.sh # 配置脚本
211
+ ├── test.sh # 测试���本
212
+ ├── Dockerfile # Docker配置
213
+ ├── .env.example # 环境变量示例
214
+ ├── .env # 本地环境变量(需要创建)
215
+ └── README.md # 项目文档
216
+ ```
217
+
218
+ ### 测试
219
+
220
+ 启动服务器后,访问根路径检查状态:
221
+ ```bash
222
+ curl http://localhost:8080/
223
+ ```
224
+
225
+ 应该返回:
226
+ ```json
227
+ {
228
+ "message": "Claude to OpenAI API Proxy",
229
+ "version": "1.0.0",
230
+ "environment": "debug"
231
+ }
232
+ ```
233
+
234
+ 或者运行测试脚本:
235
+ ```bash
236
+ ./test.sh
237
+ ```
238
+
239
+ ## 🐳 Hugging Face Space 部署
240
+
241
+ 此项目设计为在Hugging Face Space上运行,通过Docker容器部署。
242
+
243
+ ### 部署配置
244
+
245
+ 1. 项目使用Docker SDK进行容器化部署
246
+ 2. 支持环境变量配置
247
+ 3. 自动处理CORS和API代理功能
248
+
249
+ ### 环境变量设置
250
+
251
+ 在Hugging Face Space设置中配置以下环境变量:
252
+
253
+ - `PORT`: 服务器端口(默认8080)
254
+ - `DEBUG`: 调试模式(true/false)
255
+ - `HAIKU_BASE_URL`: Haiku快捷方式的基础URL
256
+ - `HAIKU_MODEL`: Haiku快捷方式的模型名称
257
+
258
+ ## 🤝 贡献
259
+
260
+ 欢迎提交Issue和Pull Request!
261
+
262
+ ## 📄 许可证
263
+
264
+ MIT License
265
+
266
+ ---
267
+
268
+ 🌟 如果这个项目对你有帮助,请给个Star!
claude_proxy.sh ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Claude Proxy Configuration Script
4
+ # This script helps you set up the claude CLI to work with various OpenAI-compatible APIs
5
+
6
+ set -e
7
+
8
+ echo "🚀 Claude Proxy Configuration Script"
9
+ echo "======================================"
10
+ echo ""
11
+
12
+ # Colors for output
13
+ RED='\033[0;31m'
14
+ GREEN='\033[0;32m'
15
+ YELLOW='\033[1;33m'
16
+ BLUE='\033[0;34m'
17
+ NC='\033[0m' # No Color
18
+
19
+ # Function to print colored output
20
+ print_color() {
21
+ printf "${1}${2}${NC}\n"
22
+ }
23
+
24
+ # Function to check if command exists
25
+ command_exists() {
26
+ command -v "$1" >/dev/null 2>&1
27
+ }
28
+
29
+ # Function to install claude CLI if not exists
30
+ install_claude_cli() {
31
+ if ! command_exists claude; then
32
+ print_color $YELLOW "Claude CLI not found. Installing..."
33
+
34
+ if [[ "$OSTYPE" == "darwin"* ]]; then
35
+ if command_exists brew; then
36
+ brew install anthropics/claude/claude
37
+ else
38
+ print_color $RED "Homebrew not found. Please install Homebrew first:"
39
+ print_color $BLUE "https://brew.sh"
40
+ exit 1
41
+ fi
42
+ elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
43
+ if command_exists curl; then
44
+ curl -fsSL https://claude.ai/claude.sh | sh
45
+ else
46
+ print_color $RED "curl not found. Please install curl first."
47
+ exit 1
48
+ fi
49
+ else
50
+ print_color $RED "Unsupported OS. Please install Claude CLI manually:"
51
+ print_color $BLUE "https://docs.anthropic.com/claude/reference/getting-started"
52
+ exit 1
53
+ fi
54
+
55
+ print_color $GREEN "✅ Claude CLI installed successfully!"
56
+ else
57
+ print_color $GREEN "✅ Claude CLI already installed."
58
+ fi
59
+ }
60
+
61
+ # Function to get user input
62
+ get_input() {
63
+ local prompt="$1"
64
+ local default="$2"
65
+ local var_name="$3"
66
+
67
+ if [ -n "$default" ]; then
68
+ echo -n "$prompt [$default]: "
69
+ else
70
+ echo -n "$prompt: "
71
+ fi
72
+
73
+ read -r input
74
+ if [ -z "$input" ] && [ -n "$default" ]; then
75
+ input="$default"
76
+ fi
77
+
78
+ eval "$var_name='$input'"
79
+ }
80
+
81
+ # Function to select API service
82
+ select_api_service() {
83
+ echo ""
84
+ print_color $BLUE "Select your API service:"
85
+ echo "1) Google Gemini (gemini.googleapis.com)"
86
+ echo "2) Groq (api.groq.com)"
87
+ echo "3) OpenAI (api.openai.com)"
88
+ echo "4) Ollama (localhost:11434)"
89
+ echo "5) Custom endpoint"
90
+ echo ""
91
+
92
+ while true; do
93
+ get_input "Enter your choice (1-5)" "1" "choice"
94
+
95
+ case $choice in
96
+ 1)
97
+ BASE_URL="https://generativelanguage.googleapis.com/v1beta"
98
+ DEFAULT_MODEL="gemini-1.5-pro"
99
+ SERVICE_NAME="Google Gemini"
100
+ break
101
+ ;;
102
+ 2)
103
+ BASE_URL="https://api.groq.com/openai/v1"
104
+ DEFAULT_MODEL="llama3-70b-8192"
105
+ SERVICE_NAME="Groq"
106
+ break
107
+ ;;
108
+ 3)
109
+ BASE_URL="https://api.openai.com/v1"
110
+ DEFAULT_MODEL="gpt-4"
111
+ SERVICE_NAME="OpenAI"
112
+ break
113
+ ;;
114
+ 4)
115
+ BASE_URL="http://localhost:11434/v1"
116
+ DEFAULT_MODEL="llama3"
117
+ SERVICE_NAME="Ollama"
118
+ break
119
+ ;;
120
+ 5)
121
+ get_input "Enter your API base URL" "" "BASE_URL"
122
+ get_input "Enter default model name" "" "DEFAULT_MODEL"
123
+ SERVICE_NAME="Custom"
124
+ break
125
+ ;;
126
+ *)
127
+ print_color $RED "Invalid choice. Please enter 1-5."
128
+ ;;
129
+ esac
130
+ done
131
+ }
132
+
133
+ # Function to get API key
134
+ get_api_key() {
135
+ echo ""
136
+ get_input "Enter your API key for $SERVICE_NAME" "" "API_KEY"
137
+
138
+ if [ -z "$API_KEY" ]; then
139
+ print_color $RED "API key is required!"
140
+ exit 1
141
+ fi
142
+ }
143
+
144
+ # Function to get proxy URL
145
+ get_proxy_url() {
146
+ echo ""
147
+ get_input "Enter your proxy server URL" "http://localhost:8080" "PROXY_URL"
148
+ }
149
+
150
+ # Function to get model name
151
+ get_model_name() {
152
+ echo ""
153
+ get_input "Enter the model name" "$DEFAULT_MODEL" "MODEL_NAME"
154
+ }
155
+
156
+ # Function to construct the proxy endpoint URL
157
+ construct_proxy_url() {
158
+ # Parse the base URL
159
+ local protocol=$(echo "$BASE_URL" | sed 's|://.*||')
160
+ local domain_and_path=$(echo "$BASE_URL" | sed 's|^[^:]*://||')
161
+ local domain=$(echo "$domain_and_path" | cut -d'/' -f1)
162
+ local path=$(echo "$domain_and_path" | cut -d'/' -f2-)
163
+
164
+ if [ "$path" = "$domain" ]; then
165
+ path=""
166
+ else
167
+ path="/$path"
168
+ fi
169
+
170
+ # Construct the proxy URL
171
+ CLAUDE_API_URL="$PROXY_URL/$protocol/$domain$path/$MODEL_NAME"
172
+ }
173
+
174
+ # Function to configure claude CLI
175
+ configure_claude() {
176
+ print_color $BLUE "Configuring Claude CLI..."
177
+
178
+ # Set the API key
179
+ claude config set api_key "$API_KEY"
180
+
181
+ # Set the API URL
182
+ claude config set api_url "$CLAUDE_API_URL"
183
+
184
+ print_color $GREEN "✅ Claude CLI configured successfully!"
185
+ }
186
+
187
+ # Function to test the configuration
188
+ test_configuration() {
189
+ echo ""
190
+ print_color $BLUE "Testing configuration..."
191
+
192
+ echo "Testing with a simple message..."
193
+ if claude message "Hello, can you respond with just 'Configuration test successful!'?"; then
194
+ print_color $GREEN "✅ Configuration test passed!"
195
+ else
196
+ print_color $RED "❌ Configuration test failed. Please check your settings."
197
+ exit 1
198
+ fi
199
+ }
200
+
201
+ # Function to show configuration summary
202
+ show_summary() {
203
+ echo ""
204
+ print_color $GREEN "Configuration Summary:"
205
+ print_color $GREEN "====================="
206
+ echo "Service: $SERVICE_NAME"
207
+ echo "Model: $MODEL_NAME"
208
+ echo "Proxy URL: $PROXY_URL"
209
+ echo "Claude API URL: $CLAUDE_API_URL"
210
+ echo ""
211
+ print_color $BLUE "You can now use the 'claude' command to interact with $SERVICE_NAME!"
212
+ print_color $BLUE "Example: claude message 'Hello, world!'"
213
+ echo ""
214
+ }
215
+
216
+ # Function to save configuration to file
217
+ save_config() {
218
+ local config_file="$HOME/.claude_proxy_config"
219
+
220
+ cat > "$config_file" << EOF
221
+ # Claude Proxy Configuration
222
+ # Generated on $(date)
223
+
224
+ SERVICE_NAME="$SERVICE_NAME"
225
+ BASE_URL="$BASE_URL"
226
+ MODEL_NAME="$MODEL_NAME"
227
+ PROXY_URL="$PROXY_URL"
228
+ CLAUDE_API_URL="$CLAUDE_API_URL"
229
+ API_KEY="$API_KEY"
230
+ EOF
231
+
232
+ print_color $GREEN "✅ Configuration saved to $config_file"
233
+ }
234
+
235
+ # Main execution
236
+ main() {
237
+ print_color $BLUE "This script will help you configure the Claude CLI to work with various OpenAI-compatible APIs through a proxy server."
238
+ echo ""
239
+
240
+ # Install claude CLI if needed
241
+ install_claude_cli
242
+
243
+ # Get user configuration
244
+ select_api_service
245
+ get_api_key
246
+ get_proxy_url
247
+ get_model_name
248
+
249
+ # Construct the proxy URL
250
+ construct_proxy_url
251
+
252
+ # Configure claude CLI
253
+ configure_claude
254
+
255
+ # Save configuration
256
+ save_config
257
+
258
+ # Show summary
259
+ show_summary
260
+
261
+ # Ask if user wants to test
262
+ echo ""
263
+ get_input "Do you want to test the configuration now? (y/n)" "y" "test_choice"
264
+
265
+ if [[ "$test_choice" =~ ^[Yy]$ ]]; then
266
+ test_configuration
267
+ fi
268
+
269
+ print_color $GREEN "🎉 Setup complete! Happy coding with Claude!"
270
+ }
271
+
272
+ # Run the main function
273
+ main "$@"
go.mod ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module openai2claudecode
2
+
3
+ go 1.21
4
+
5
+ require (
6
+ github.com/gin-gonic/gin v1.9.1
7
+ github.com/joho/godotenv v1.5.1
8
+ )
9
+
10
+ require (
11
+ github.com/bytedance/sonic v1.9.1 // indirect
12
+ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
13
+ github.com/gabriel-vasile/mimetype v1.4.2 // indirect
14
+ github.com/gin-contrib/sse v0.1.0 // indirect
15
+ github.com/go-playground/locales v0.14.1 // indirect
16
+ github.com/go-playground/universal-translator v0.18.1 // indirect
17
+ github.com/go-playground/validator/v10 v10.14.0 // indirect
18
+ github.com/goccy/go-json v0.10.2 // indirect
19
+ github.com/json-iterator/go v1.1.12 // indirect
20
+ github.com/klauspost/cpuid/v2 v2.2.4 // indirect
21
+ github.com/leodido/go-urn v1.2.4 // indirect
22
+ github.com/mattn/go-isatty v0.0.19 // indirect
23
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
24
+ github.com/modern-go/reflect2 v1.0.2 // indirect
25
+ github.com/pelletier/go-toml/v2 v2.0.8 // indirect
26
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
27
+ github.com/ugorji/go/codec v1.2.11 // indirect
28
+ golang.org/x/arch v0.3.0 // indirect
29
+ golang.org/x/crypto v0.9.0 // indirect
30
+ golang.org/x/net v0.10.0 // indirect
31
+ golang.org/x/sys v0.8.0 // indirect
32
+ golang.org/x/text v0.9.0 // indirect
33
+ google.golang.org/protobuf v1.30.0 // indirect
34
+ gopkg.in/yaml.v3 v3.0.1 // indirect
35
+ )
go.sum ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
2
+ github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
3
+ github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
4
+ github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
5
+ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
6
+ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
7
+ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8
+ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
9
+ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10
+ github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
11
+ github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
12
+ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
13
+ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
14
+ github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
15
+ github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
16
+ github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
17
+ github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
18
+ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
19
+ github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
20
+ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
21
+ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
22
+ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
23
+ github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
24
+ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
25
+ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
26
+ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
27
+ github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
28
+ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
29
+ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
30
+ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
31
+ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
32
+ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
33
+ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
34
+ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
35
+ github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
36
+ github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
37
+ github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
38
+ github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
39
+ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
40
+ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
41
+ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
42
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
43
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
44
+ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
45
+ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
46
+ github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
47
+ github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
48
+ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
49
+ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
50
+ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
51
+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
52
+ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
53
+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
54
+ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
55
+ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
56
+ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
57
+ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
58
+ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
59
+ github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
60
+ github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
61
+ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
62
+ github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
63
+ github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
64
+ github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
65
+ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
66
+ golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
67
+ golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
68
+ golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
69
+ golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
70
+ golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
71
+ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
72
+ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
73
+ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
74
+ golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
75
+ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
76
+ golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
77
+ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
78
+ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
79
+ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
80
+ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
81
+ google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
82
+ google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
83
+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
84
+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
85
+ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
86
+ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
87
+ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
88
+ rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
main.go ADDED
@@ -0,0 +1,610 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package main
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "fmt"
7
+ "io"
8
+ "log"
9
+ "net/http"
10
+ "os"
11
+ "regexp"
12
+ "strings"
13
+ "time"
14
+
15
+ "github.com/gin-gonic/gin"
16
+ "github.com/joho/godotenv"
17
+ )
18
+
19
+ type ClaudeMessage struct {
20
+ Role string `json:"role"`
21
+ Content interface{} `json:"content"`
22
+ }
23
+
24
+ type ClaudeContentBlock struct {
25
+ Type string `json:"type"`
26
+ Text string `json:"text"`
27
+ }
28
+
29
+ type ClaudeRequest struct {
30
+ Model string `json:"model"`
31
+ MaxTokens int `json:"max_tokens"`
32
+ Messages []ClaudeMessage `json:"messages"`
33
+ Temperature *float64 `json:"temperature,omitempty"`
34
+ TopP *float64 `json:"top_p,omitempty"`
35
+ Stream bool `json:"stream,omitempty"`
36
+ Tools []ClaudeTool `json:"tools,omitempty"`
37
+ }
38
+
39
+ type ClaudeTool struct {
40
+ Name string `json:"name"`
41
+ Description string `json:"description"`
42
+ InputSchema map[string]interface{} `json:"input_schema"`
43
+ }
44
+
45
+ type OpenAIMessage struct {
46
+ Role string `json:"role"`
47
+ Content string `json:"content"`
48
+ }
49
+
50
+ type OpenAIRequest struct {
51
+ Model string `json:"model"`
52
+ Messages []OpenAIMessage `json:"messages"`
53
+ MaxTokens int `json:"max_tokens"`
54
+ Temperature *float64 `json:"temperature,omitempty"`
55
+ TopP *float64 `json:"top_p,omitempty"`
56
+ Stream bool `json:"stream,omitempty"`
57
+ Tools []OpenAITool `json:"tools,omitempty"`
58
+ }
59
+
60
+ type OpenAITool struct {
61
+ Type string `json:"type"`
62
+ Function OpenAIToolFunction `json:"function"`
63
+ }
64
+
65
+ type OpenAIToolFunction struct {
66
+ Name string `json:"name"`
67
+ Description string `json:"description"`
68
+ Parameters map[string]interface{} `json:"parameters"`
69
+ }
70
+
71
+ type ClaudeResponse struct {
72
+ ID string `json:"id"`
73
+ Type string `json:"type"`
74
+ Role string `json:"role"`
75
+ Content []struct {
76
+ Type string `json:"type"`
77
+ Text string `json:"text"`
78
+ } `json:"content"`
79
+ Model string `json:"model"`
80
+ StopReason string `json:"stop_reason"`
81
+ StopSequence string `json:"stop_sequence"`
82
+ Usage struct {
83
+ InputTokens int `json:"input_tokens"`
84
+ OutputTokens int `json:"output_tokens"`
85
+ } `json:"usage"`
86
+ }
87
+
88
+ type OpenAIResponse struct {
89
+ ID string `json:"id"`
90
+ Object string `json:"object"`
91
+ Created int64 `json:"created"`
92
+ Model string `json:"model"`
93
+ Choices []struct {
94
+ Index int `json:"index"`
95
+ Message struct {
96
+ Role string `json:"role"`
97
+ Content string `json:"content"`
98
+ } `json:"message"`
99
+ FinishReason string `json:"finish_reason"`
100
+ } `json:"choices"`
101
+ Usage struct {
102
+ PromptTokens int `json:"prompt_tokens"`
103
+ CompletionTokens int `json:"completion_tokens"`
104
+ TotalTokens int `json:"total_tokens"`
105
+ } `json:"usage"`
106
+ }
107
+
108
+ type ProxyConfig struct {
109
+ BaseURL string
110
+ Model string
111
+ }
112
+
113
+ func main() {
114
+ // Load .env file if it exists
115
+ if err := godotenv.Load(); err != nil {
116
+ log.Println("No .env file found or error loading .env file")
117
+ }
118
+
119
+ port := os.Getenv("PORT")
120
+ if port == "" {
121
+ port = "8080"
122
+ }
123
+
124
+ // Set debug mode based on environment variable
125
+ if os.Getenv("DEBUG") == "true" {
126
+ gin.SetMode(gin.DebugMode)
127
+ } else {
128
+ gin.SetMode(gin.ReleaseMode)
129
+ }
130
+
131
+ router := gin.Default()
132
+
133
+ // CORS configuration from environment or defaults
134
+ corsOrigin := os.Getenv("CORS_ALLOW_ORIGIN")
135
+ if corsOrigin == "" {
136
+ corsOrigin = "*"
137
+ }
138
+
139
+ corsMethods := os.Getenv("CORS_ALLOW_METHODS")
140
+ if corsMethods == "" {
141
+ corsMethods = "POST, GET, OPTIONS, PUT, DELETE"
142
+ }
143
+
144
+ corsHeaders := os.Getenv("CORS_ALLOW_HEADERS")
145
+ if corsHeaders == "" {
146
+ corsHeaders = "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, x-api-key, anthropic-version"
147
+ }
148
+
149
+ router.Use(func(c *gin.Context) {
150
+ c.Header("Access-Control-Allow-Origin", corsOrigin)
151
+ c.Header("Access-Control-Allow-Methods", corsMethods)
152
+ c.Header("Access-Control-Allow-Headers", corsHeaders)
153
+
154
+ if c.Request.Method == "OPTIONS" {
155
+ c.AbortWithStatus(204)
156
+ return
157
+ }
158
+
159
+ c.Next()
160
+ })
161
+
162
+ router.POST("/*path", handleProxy)
163
+ router.GET("/", func(c *gin.Context) {
164
+ c.JSON(200, gin.H{
165
+ "message": "Claude to OpenAI API Proxy",
166
+ "version": "1.0.0",
167
+ "environment": gin.Mode(),
168
+ })
169
+ })
170
+
171
+ log.Printf("Server starting on port %s (mode: %s)", port, gin.Mode())
172
+ router.Run(":" + port)
173
+ }
174
+
175
+ func handleProxy(c *gin.Context) {
176
+ path := c.Param("path")
177
+
178
+ if !strings.HasSuffix(path, "/v1/messages") {
179
+ c.JSON(404, gin.H{"error": "Not found"})
180
+ return
181
+ }
182
+
183
+ config, err := parseProxyConfig(path)
184
+ if err != nil {
185
+ c.JSON(400, gin.H{"error": "Invalid proxy configuration: " + err.Error()})
186
+ return
187
+ }
188
+
189
+ var claudeReq ClaudeRequest
190
+ if err := c.ShouldBindJSON(&claudeReq); err != nil {
191
+ log.Printf("=== Claude Request Parse Error ===")
192
+ log.Printf("Error: %v", err)
193
+ log.Printf("=== End Parse Error ===")
194
+ c.JSON(400, gin.H{"error": "Invalid request body: " + err.Error()})
195
+ return
196
+ }
197
+
198
+ // Debug: Print the Claude request structure
199
+ log.Printf("=== Claude Request Debug ===")
200
+ claudeReqJSON, _ := json.MarshalIndent(claudeReq, "", " ")
201
+ log.Printf("Parsed Claude Request:")
202
+ log.Printf("%s", string(claudeReqJSON))
203
+ log.Printf("=== End Claude Request Debug ===")
204
+
205
+ openaiReq := convertClaudeToOpenAI(claudeReq, config.Model)
206
+
207
+ // Debug: Print all incoming headers
208
+ log.Printf("=== Incoming Headers Debug ===")
209
+ for key, values := range c.Request.Header {
210
+ for _, value := range values {
211
+ log.Printf(" %s: %s", key, value)
212
+ }
213
+ }
214
+ log.Printf("=== End Headers Debug ===")
215
+
216
+ apiKey := c.GetHeader("x-api-key")
217
+ if apiKey == "" {
218
+ // Try alternative header names that Claude CLI might use
219
+ apiKey = c.GetHeader("Authorization")
220
+ if apiKey != "" && strings.HasPrefix(apiKey, "Bearer ") {
221
+ apiKey = strings.TrimPrefix(apiKey, "Bearer ")
222
+ }
223
+ }
224
+ if apiKey == "" {
225
+ apiKey = c.GetHeader("anthropic-api-key")
226
+ }
227
+ if apiKey == "" {
228
+ apiKey = c.GetHeader("anthropic-version")
229
+ }
230
+
231
+ log.Printf("=== API Key Debug ===")
232
+ log.Printf("Final API Key: %s", apiKey)
233
+ log.Printf("=== End API Key Debug ===")
234
+
235
+ if apiKey == "" {
236
+ c.JSON(401, gin.H{"error": "API key required"})
237
+ return
238
+ }
239
+
240
+ if claudeReq.Stream {
241
+ handleStreamingRequest(c, openaiReq, config, apiKey)
242
+ } else {
243
+ handleRegularRequest(c, openaiReq, config, apiKey)
244
+ }
245
+ }
246
+
247
+ func parseProxyConfig(path string) (*ProxyConfig, error) {
248
+ log.Printf("Debug: Parsing path: %s", path)
249
+
250
+ haikuBaseURL := os.Getenv("HAIKU_BASE_URL")
251
+ haikuModel := os.Getenv("HAIKU_MODEL")
252
+
253
+ if haikuBaseURL != "" && haikuModel != "" && strings.Contains(path, "haiku") {
254
+ log.Printf("Debug: Using haiku shortcut")
255
+ return &ProxyConfig{
256
+ BaseURL: haikuBaseURL,
257
+ Model: haikuModel,
258
+ }, nil
259
+ }
260
+
261
+ // Remove query parameters from path before matching
262
+ pathWithoutQuery := strings.Split(path, "?")[0]
263
+ log.Printf("Debug: Path without query: %s", pathWithoutQuery)
264
+
265
+ re := regexp.MustCompile(`^/([^/]+)/([^/]+)(/.*?)/([^/]+)/v1/messages$`)
266
+ matches := re.FindStringSubmatch(pathWithoutQuery)
267
+
268
+ log.Printf("Debug: Regex matches: %v", matches)
269
+
270
+ if len(matches) != 5 {
271
+ return nil, fmt.Errorf("invalid path format. Expected: /protocol/domain/path/model/v1/messages, got: %s", pathWithoutQuery)
272
+ }
273
+
274
+ protocol := matches[1]
275
+ domain := matches[2]
276
+ urlPath := matches[3]
277
+ model := matches[4]
278
+
279
+ baseURL := fmt.Sprintf("%s://%s%s", protocol, domain, urlPath)
280
+
281
+ log.Printf("Debug: Parsed - protocol: %s, domain: %s, urlPath: %s, model: %s", protocol, domain, urlPath, model)
282
+ log.Printf("Debug: Final baseURL: %s", baseURL)
283
+
284
+ return &ProxyConfig{
285
+ BaseURL: baseURL,
286
+ Model: model,
287
+ }, nil
288
+ }
289
+
290
+ func convertClaudeToOpenAI(claudeReq ClaudeRequest, model string) OpenAIRequest {
291
+ messages := make([]OpenAIMessage, len(claudeReq.Messages))
292
+ for i, msg := range claudeReq.Messages {
293
+ messages[i] = OpenAIMessage{
294
+ Role: msg.Role,
295
+ Content: extractTextContent(msg.Content),
296
+ }
297
+ }
298
+
299
+ openaiReq := OpenAIRequest{
300
+ Model: model,
301
+ Messages: messages,
302
+ MaxTokens: claudeReq.MaxTokens,
303
+ Temperature: claudeReq.Temperature,
304
+ TopP: claudeReq.TopP,
305
+ Stream: claudeReq.Stream,
306
+ }
307
+
308
+ if len(claudeReq.Tools) > 0 {
309
+ openaiReq.Tools = make([]OpenAITool, len(claudeReq.Tools))
310
+ for i, tool := range claudeReq.Tools {
311
+ openaiReq.Tools[i] = OpenAITool{
312
+ Type: "function",
313
+ Function: OpenAIToolFunction{
314
+ Name: tool.Name,
315
+ Description: tool.Description,
316
+ Parameters: cleanInputSchema(tool.InputSchema),
317
+ },
318
+ }
319
+ }
320
+ }
321
+
322
+ return openaiReq
323
+ }
324
+
325
+ func extractTextContent(content interface{}) string {
326
+ switch v := content.(type) {
327
+ case string:
328
+ // Simple string content
329
+ return v
330
+ case []interface{}:
331
+ // Array of content blocks
332
+ var text strings.Builder
333
+ for _, block := range v {
334
+ if blockMap, ok := block.(map[string]interface{}); ok {
335
+ if blockType, exists := blockMap["type"]; exists && blockType == "text" {
336
+ if textContent, exists := blockMap["text"]; exists {
337
+ if textStr, ok := textContent.(string); ok {
338
+ text.WriteString(textStr)
339
+ }
340
+ }
341
+ }
342
+ }
343
+ }
344
+ return text.String()
345
+ default:
346
+ // Try to convert to string
347
+ if str, ok := content.(string); ok {
348
+ return str
349
+ }
350
+ return ""
351
+ }
352
+ }
353
+
354
+ func cleanInputSchema(schema map[string]interface{}) map[string]interface{} {
355
+ cleaned := make(map[string]interface{})
356
+ for k, v := range schema {
357
+ if k == "additionalProperties" {
358
+ continue
359
+ }
360
+ cleaned[k] = v
361
+ }
362
+ return cleaned
363
+ }
364
+
365
+ func handleRegularRequest(c *gin.Context, openaiReq OpenAIRequest, config *ProxyConfig, apiKey string) {
366
+ jsonData, err := json.Marshal(openaiReq)
367
+ if err != nil {
368
+ c.JSON(500, gin.H{"error": "Failed to marshal request"})
369
+ return
370
+ }
371
+
372
+ // Debug: Print complete request information
373
+ targetURL := config.BaseURL + "/chat/completions"
374
+ log.Printf("=== OpenAI Request Debug ===")
375
+ log.Printf("Target URL: %s", targetURL)
376
+ log.Printf("Method: POST")
377
+ log.Printf("Headers:")
378
+ log.Printf(" Content-Type: application/json")
379
+ log.Printf(" Authorization: Bearer %s", apiKey)
380
+ log.Printf("Request Body:")
381
+ log.Printf("%s", string(jsonData))
382
+ log.Printf("=== End Request Debug ===")
383
+
384
+ client := &http.Client{Timeout: 60 * time.Second}
385
+ req, err := http.NewRequest("POST", targetURL, bytes.NewBuffer(jsonData))
386
+ if err != nil {
387
+ c.JSON(500, gin.H{"error": "Failed to create request"})
388
+ return
389
+ }
390
+
391
+ req.Header.Set("Content-Type", "application/json")
392
+ req.Header.Set("Authorization", "Bearer "+apiKey)
393
+
394
+ resp, err := client.Do(req)
395
+ if err != nil {
396
+ log.Printf("=== Request Error Debug ===")
397
+ log.Printf("Error: %v", err)
398
+ log.Printf("=== End Error Debug ===")
399
+ c.JSON(500, gin.H{"error": "Failed to make request: " + err.Error()})
400
+ return
401
+ }
402
+ defer resp.Body.Close()
403
+
404
+ body, err := io.ReadAll(resp.Body)
405
+ if err != nil {
406
+ c.JSON(500, gin.H{"error": "Failed to read response"})
407
+ return
408
+ }
409
+
410
+ // Debug: Print response information
411
+ log.Printf("=== OpenAI Response Debug ===")
412
+ log.Printf("Status Code: %d", resp.StatusCode)
413
+ log.Printf("Status: %s", resp.Status)
414
+ log.Printf("Headers:")
415
+ for key, values := range resp.Header {
416
+ for _, value := range values {
417
+ log.Printf(" %s: %s", key, value)
418
+ }
419
+ }
420
+ log.Printf("Response Body:")
421
+ log.Printf("%s", string(body))
422
+ log.Printf("=== End Response Debug ===")
423
+
424
+ if resp.StatusCode != 200 {
425
+ c.Data(resp.StatusCode, "application/json", body)
426
+ return
427
+ }
428
+
429
+ var openaiResp OpenAIResponse
430
+ if err := json.Unmarshal(body, &openaiResp); err != nil {
431
+ c.JSON(500, gin.H{"error": "Failed to parse response"})
432
+ return
433
+ }
434
+
435
+ claudeResp := convertOpenAIToClaude(openaiResp)
436
+ c.JSON(200, claudeResp)
437
+ }
438
+
439
+ func handleStreamingRequest(c *gin.Context, openaiReq OpenAIRequest, config *ProxyConfig, apiKey string) {
440
+ jsonData, err := json.Marshal(openaiReq)
441
+ if err != nil {
442
+ c.JSON(500, gin.H{"error": "Failed to marshal request"})
443
+ return
444
+ }
445
+
446
+ // Debug: Print complete streaming request information
447
+ targetURL := config.BaseURL + "/chat/completions"
448
+ log.Printf("=== OpenAI Streaming Request Debug ===")
449
+ log.Printf("Target URL: %s", targetURL)
450
+ log.Printf("Method: POST")
451
+ log.Printf("Headers:")
452
+ log.Printf(" Content-Type: application/json")
453
+ log.Printf(" Authorization: Bearer %s", apiKey)
454
+ log.Printf("Request Body:")
455
+ log.Printf("%s", string(jsonData))
456
+ log.Printf("=== End Streaming Request Debug ===")
457
+
458
+ client := &http.Client{Timeout: 60 * time.Second}
459
+ req, err := http.NewRequest("POST", targetURL, bytes.NewBuffer(jsonData))
460
+ if err != nil {
461
+ c.JSON(500, gin.H{"error": "Failed to create request"})
462
+ return
463
+ }
464
+
465
+ req.Header.Set("Content-Type", "application/json")
466
+ req.Header.Set("Authorization", "Bearer "+apiKey)
467
+
468
+ resp, err := client.Do(req)
469
+ if err != nil {
470
+ log.Printf("=== Streaming Request Error Debug ===")
471
+ log.Printf("Error: %v", err)
472
+ log.Printf("=== End Streaming Error Debug ===")
473
+ c.JSON(500, gin.H{"error": "Failed to make request: " + err.Error()})
474
+ return
475
+ }
476
+ defer resp.Body.Close()
477
+
478
+ // Debug: Print streaming response information
479
+ log.Printf("=== OpenAI Streaming Response Debug ===")
480
+ log.Printf("Status Code: %d", resp.StatusCode)
481
+ log.Printf("Status: %s", resp.Status)
482
+ log.Printf("Headers:")
483
+ for key, values := range resp.Header {
484
+ for _, value := range values {
485
+ log.Printf(" %s: %s", key, value)
486
+ }
487
+ }
488
+ log.Printf("=== End Streaming Response Debug ===")
489
+
490
+ if resp.StatusCode != 200 {
491
+ body, _ := io.ReadAll(resp.Body)
492
+ log.Printf("=== Streaming Error Response Body ===")
493
+ log.Printf("%s", string(body))
494
+ log.Printf("=== End Streaming Error Response Body ===")
495
+ c.Data(resp.StatusCode, "application/json", body)
496
+ return
497
+ }
498
+
499
+ c.Header("Content-Type", "text/event-stream")
500
+ c.Header("Cache-Control", "no-cache")
501
+ c.Header("Connection", "keep-alive")
502
+
503
+ flusher, ok := c.Writer.(http.Flusher)
504
+ if !ok {
505
+ c.JSON(500, gin.H{"error": "Streaming not supported"})
506
+ return
507
+ }
508
+
509
+ reader := resp.Body
510
+ buffer := make([]byte, 1024)
511
+
512
+ for {
513
+ n, err := reader.Read(buffer)
514
+ if err != nil {
515
+ if err == io.EOF {
516
+ break
517
+ }
518
+ log.Printf("Error reading stream: %v", err)
519
+ break
520
+ }
521
+
522
+ data := string(buffer[:n])
523
+ lines := strings.Split(data, "\n")
524
+
525
+ for _, line := range lines {
526
+ if strings.HasPrefix(line, "data: ") {
527
+ eventData := strings.TrimPrefix(line, "data: ")
528
+ if eventData == "[DONE]" {
529
+ c.Writer.WriteString("data: [DONE]\n\n")
530
+ flusher.Flush()
531
+ return
532
+ }
533
+
534
+ convertedData := convertStreamingData(eventData)
535
+ if convertedData != "" {
536
+ c.Writer.WriteString("data: " + convertedData + "\n\n")
537
+ flusher.Flush()
538
+ }
539
+ }
540
+ }
541
+ }
542
+ }
543
+
544
+ func convertStreamingData(data string) string {
545
+ if data == "" || data == "[DONE]" {
546
+ return data
547
+ }
548
+
549
+ var openaiChunk map[string]interface{}
550
+ if err := json.Unmarshal([]byte(data), &openaiChunk); err != nil {
551
+ return ""
552
+ }
553
+
554
+ claudeChunk := map[string]interface{}{
555
+ "type": "content_block_delta",
556
+ "index": 0,
557
+ "delta": map[string]interface{}{
558
+ "type": "text_delta",
559
+ },
560
+ }
561
+
562
+ if choices, ok := openaiChunk["choices"].([]interface{}); ok && len(choices) > 0 {
563
+ if choice, ok := choices[0].(map[string]interface{}); ok {
564
+ if delta, ok := choice["delta"].(map[string]interface{}); ok {
565
+ if content, ok := delta["content"].(string); ok {
566
+ claudeChunk["delta"].(map[string]interface{})["text"] = content
567
+ }
568
+ }
569
+ }
570
+ }
571
+
572
+ result, _ := json.Marshal(claudeChunk)
573
+ return string(result)
574
+ }
575
+
576
+ func convertOpenAIToClaude(openaiResp OpenAIResponse) ClaudeResponse {
577
+ claudeResp := ClaudeResponse{
578
+ ID: openaiResp.ID,
579
+ Type: "message",
580
+ Role: "assistant",
581
+ Model: openaiResp.Model,
582
+ }
583
+
584
+ if len(openaiResp.Choices) > 0 {
585
+ choice := openaiResp.Choices[0]
586
+ claudeResp.Content = []struct {
587
+ Type string `json:"type"`
588
+ Text string `json:"text"`
589
+ }{
590
+ {
591
+ Type: "text",
592
+ Text: choice.Message.Content,
593
+ },
594
+ }
595
+
596
+ switch choice.FinishReason {
597
+ case "stop":
598
+ claudeResp.StopReason = "end_turn"
599
+ case "length":
600
+ claudeResp.StopReason = "max_tokens"
601
+ default:
602
+ claudeResp.StopReason = "end_turn"
603
+ }
604
+ }
605
+
606
+ claudeResp.Usage.InputTokens = openaiResp.Usage.PromptTokens
607
+ claudeResp.Usage.OutputTokens = openaiResp.Usage.CompletionTokens
608
+
609
+ return claudeResp
610
+ }
test.sh ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Test script for Claude to OpenAI API Proxy
4
+
5
+ echo "🧪 Testing Claude to OpenAI API Proxy"
6
+ echo "====================================="
7
+
8
+ # Test 1: Health check
9
+ echo ""
10
+ echo "📊 Test 1: Health check"
11
+ response=$(curl -s http://localhost:8080/)
12
+ echo "Response: $response"
13
+
14
+ if echo "$response" | grep -q "Claude to OpenAI API Proxy"; then
15
+ echo "✅ Health check passed"
16
+ else
17
+ echo "❌ Health check failed"
18
+ exit 1
19
+ fi
20
+
21
+ # Test 2: Invalid path
22
+ echo ""
23
+ echo "📊 Test 2: Invalid path"
24
+ response=$(curl -s -w "%{http_code}" -o /dev/null http://localhost:8080/invalid/path)
25
+ echo "HTTP Status: $response"
26
+
27
+ if [ "$response" = "404" ]; then
28
+ echo "✅ Invalid path test passed"
29
+ else
30
+ echo "❌ Invalid path test failed"
31
+ fi
32
+
33
+ # Test 3: Valid path format validation
34
+ echo ""
35
+ echo "📊 Test 3: Valid path format validation"
36
+ response=$(curl -s -w "%{http_code}" -o /dev/null -X POST http://localhost:8080/https/api.openai.com/v1/gpt-4/v1/messages \
37
+ -H 'Content-Type: application/json' \
38
+ -H 'x-api-key: test-key' \
39
+ -d '{"model": "gpt-4", "max_tokens": 10, "messages": [{"role": "user", "content": "test"}]}')
40
+
41
+ echo "HTTP Status: $response"
42
+
43
+ if [ "$response" = "500" ] || [ "$response" = "401" ] || [ "$response" = "400" ]; then
44
+ echo "✅ Path format validation passed (expected error due to invalid API key)"
45
+ else
46
+ echo "❌ Path format validation failed"
47
+ fi
48
+
49
+ echo ""
50
+ echo "🎉 Basic tests completed!"
51
+ echo ""
52
+ echo "💡 To test with real APIs, use:"
53
+ echo " ./claude_proxy.sh"
54
+ echo ""
55
+ echo "🚀 To deploy to Cloudflare Workers:"
56
+ echo " ./deploy.sh"