deepseek-proxy 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,381 @@
1
+ import os
2
+ from flask import Flask, request, Response, jsonify
3
+ import requests
4
+ import json
5
+ import uuid
6
+ import time
7
+ import traceback
8
+
9
+ import logging
10
+ logger = logging.getLogger(__name__)
11
+
12
+ app = Flask(__name__)
13
+
14
+ # 配置日志格式和级别
15
+ logging.basicConfig(
16
+ level=logging.DEBUG,
17
+ format="%(asctime)s [%(levelname)s] %(message)s",
18
+ datefmt="%Y-%m-%d %H:%M:%S"
19
+ )
20
+ logger.setLevel(logging.DEBUG)
21
+ DEEPSEEK_CHAT_URL = "https://api.deepseek.com/v1/chat/completions"
22
+
23
+ # ASGI 包装(供 uvicorn 使用)
24
+ try:
25
+ from asgiref.wsgi import WsgiToAsgi
26
+ asgi_app = WsgiToAsgi(app)
27
+ except ImportError:
28
+ asgi_app = None
29
+
30
+ CRLF = "\r\n"
31
+
32
+ def translate_usage(usage):
33
+ """将 DeepSeek usage 格式转为 Responses API 格式。"""
34
+ if not usage:
35
+ return {'input_tokens': 0, 'output_tokens': 0, 'total_tokens': 0}
36
+ result = {
37
+ 'input_tokens': usage.get('prompt_tokens', 0),
38
+ 'output_tokens': usage.get('completion_tokens', 0),
39
+ 'total_tokens': usage.get('total_tokens', 0),
40
+ }
41
+ # 保留 DeepSeek 独有的细节字段(可选)
42
+ prompt_details = usage.get('prompt_tokens_details')
43
+ if prompt_details:
44
+ result['input_tokens_details'] = {
45
+ 'cached_tokens': prompt_details.get('cached_tokens', 0)
46
+ }
47
+ completion_details = usage.get('completion_tokens_details')
48
+ if completion_details:
49
+ result['output_tokens_details'] = {
50
+ 'reasoning_tokens': completion_details.get('reasoning_tokens', 0)
51
+ }
52
+ logger.debug(f"usage 转换: {usage} → {result}")
53
+ return result
54
+
55
+ def sse_event(event_type, data):
56
+ """生成 Responses API 标准 SSE 事件(含 event: 行,JSON 中使用 type 字段)。"""
57
+ data['type'] = event_type
58
+ payload = json.dumps(data, ensure_ascii=False)
59
+ logger.debug(f"SSE << {event_type}")
60
+ return f"event: {event_type}{CRLF}data: {payload}{CRLF}{CRLF}"
61
+
62
+
63
+ def generate_codex_stream(auth, messages):
64
+ resp_id = f"resp_{uuid.uuid4()}"
65
+ output_id = f"output_{uuid.uuid4()}"
66
+ logger.info(f"开始生成响应,ID: {resp_id}")
67
+
68
+ # 当前时间戳
69
+ created_at = int(time.time())
70
+
71
+ # 1. response.created
72
+ yield sse_event("response.created", {
73
+ 'id': resp_id,
74
+ 'object': 'response.created',
75
+ 'response': {
76
+ 'id': resp_id,
77
+ 'object': 'response',
78
+ 'created_at': created_at,
79
+ 'status': 'in_progress',
80
+ 'model': 'deepseek-v4-flash',
81
+ 'output': []
82
+ }
83
+ })
84
+
85
+ # 2. response.in_progress
86
+ yield sse_event("response.in_progress", {
87
+ 'id': resp_id,
88
+ 'object': 'response.in_progress',
89
+ 'response': {
90
+ 'id': resp_id,
91
+ 'object': 'response',
92
+ 'created_at': created_at,
93
+ 'status': 'in_progress',
94
+ 'model': 'deepseek-v4-flash',
95
+ 'output': []
96
+ }
97
+ })
98
+
99
+ # 3. response.output_item.added
100
+ yield sse_event("response.output_item.added", {
101
+ 'id': resp_id,
102
+ 'object': 'response.output_item.added',
103
+ 'item_id': output_id,
104
+ 'output_index': 0,
105
+ 'item': {
106
+ 'id': output_id,
107
+ 'object': 'response.output_item',
108
+ 'type': 'message',
109
+ 'role': 'assistant',
110
+ 'status': 'in_progress',
111
+ 'content': []
112
+ }
113
+ })
114
+
115
+ # 3. response.content_part.added
116
+ yield sse_event("response.content_part.added", {
117
+ 'id': resp_id,
118
+ 'object': 'response.content_part.added',
119
+ 'output_index': 0,
120
+ 'content_index': 0,
121
+ 'part': {
122
+ 'type': 'text',
123
+ 'text': ''
124
+ }
125
+ })
126
+
127
+ full_text = "" # 累积全部文本
128
+ usage_info = None # 保存 DeepSeek 返回的 usage (原始格式)
129
+
130
+ try:
131
+ payload = {
132
+ "model": "deepseek-v4-flash",
133
+ "messages": messages,
134
+ "stream": True,
135
+ "temperature": 0.7,
136
+ "max_tokens": 4096
137
+ }
138
+
139
+ logger.debug(f"发送到 DeepSeek 的请求: {json.dumps(payload, indent=2)}")
140
+
141
+ with requests.post(
142
+ DEEPSEEK_CHAT_URL,
143
+ headers={"Authorization": auth, "Content-Type": "application/json"},
144
+ json=payload,
145
+ stream=True,
146
+ timeout=30
147
+ ) as resp:
148
+ logger.info(f"DeepSeek 响应状态码: {resp.status_code}")
149
+
150
+ if resp.status_code != 200:
151
+ error_text = resp.text
152
+ logger.error(f"DeepSeek API 错误: {error_text}")
153
+ raise Exception(f"DeepSeek API 返回错误: {resp.status_code} - {error_text}")
154
+
155
+ line_count = 0
156
+ for line in resp.iter_lines():
157
+ line_count += 1
158
+ if not line:
159
+ continue
160
+
161
+ line = line.decode('utf-8')
162
+ logger.debug(f"DeepSeek 行 {line_count}: {line}")
163
+
164
+ if line.startswith('data: '):
165
+ data_str = line[6:]
166
+ if data_str == '[DONE]':
167
+ logger.debug("收到 DeepSeek [DONE]")
168
+ continue
169
+
170
+ try:
171
+ data = json.loads(data_str)
172
+ # 捕获 usage(一般最后一条 chunk 会带)
173
+ if 'usage' in data:
174
+ usage_info = data['usage']
175
+ logger.info(f"捕获 DeepSeek usage: {usage_info}")
176
+
177
+ if 'choices' in data and len(data['choices']) > 0:
178
+ choice = data['choices'][0]
179
+ delta = choice.get('delta', {})
180
+ content = delta.get('content', '')
181
+
182
+ # 记录 finish_reason
183
+ finish_reason = choice.get('finish_reason')
184
+ if finish_reason:
185
+ logger.info(f"DeepSeek finish_reason={finish_reason}")
186
+
187
+ if content:
188
+ full_text += content
189
+ logger.debug(f" 增量文本: {repr(content)}")
190
+ yield sse_event("response.output_text.delta", {
191
+ "id": resp_id,
192
+ "object": "response.output_text.delta",
193
+ "output_index": 0,
194
+ "content_index": 0,
195
+ "delta": content
196
+ })
197
+ except Exception as e:
198
+ logger.error(f"解析 DeepSeek 数据失败: {e}")
199
+ logger.error(f"原始数据: {data_str}")
200
+ raise Exception(f"数据解析失败: {e}")
201
+
202
+ logger.info(f"DeepSeek 流结束,共处理 {line_count} 行")
203
+
204
+ # 5. response.output_text.done
205
+ yield sse_event("response.output_text.done", {
206
+ 'id': resp_id,
207
+ 'object': 'response.output_text.done',
208
+ 'output_index': 0,
209
+ 'content_index': 0,
210
+ 'text': full_text
211
+ })
212
+
213
+ # 6. response.output_item.done
214
+ yield sse_event("response.output_item.done", {
215
+ 'id': resp_id,
216
+ 'object': 'response.output_item.done',
217
+ 'output_index': 0,
218
+ 'item': {
219
+ 'id': output_id,
220
+ 'object': 'response.output_item',
221
+ 'type': 'message',
222
+ 'role': 'assistant',
223
+ 'status': 'completed',
224
+ 'content': [{'type': 'text', 'text': full_text}]
225
+ }
226
+ })
227
+
228
+ # 7. response.completed
229
+ translated_usage = translate_usage(usage_info)
230
+ logger.info(f"最终 usage: {translated_usage}")
231
+ yield sse_event("response.completed", {
232
+ 'id': resp_id,
233
+ 'object': 'response.completed',
234
+ 'response': {
235
+ 'id': resp_id,
236
+ 'object': 'response',
237
+ 'created_at': created_at,
238
+ 'status': 'completed',
239
+ 'model': 'deepseek-v4-flash',
240
+ 'output': [
241
+ {
242
+ 'id': output_id,
243
+ 'object': 'response.output_item',
244
+ 'type': 'message',
245
+ 'role': 'assistant',
246
+ 'status': 'completed',
247
+ 'content': [{'type': 'text', 'text': full_text}]
248
+ }
249
+ ],
250
+ 'usage': translated_usage
251
+ }
252
+ })
253
+
254
+ # 注意:Responses API 流不需要 [DONE]
255
+ logger.info(f"响应成功完成 (full_text_len={len(full_text)})")
256
+
257
+ except Exception as e:
258
+ logger.error(f"流生成失败: {str(e)}")
259
+ logger.error(traceback.format_exc())
260
+ yield sse_event("response.error", {
261
+ 'id': resp_id,
262
+ 'object': 'response.error',
263
+ 'message': str(e)
264
+ })
265
+ return
266
+
267
+
268
+ @app.route("/v1/responses", methods=["POST"])
269
+ def responses():
270
+ auth_header = request.headers.get("Authorization", "")
271
+ auth_prefix = auth_header[:20] if auth_header else "(空)"
272
+ logger.info("=== 收到 /v1/responses 请求 ===")
273
+ logger.info(f"Authorization: {auth_prefix}...")
274
+ logger.info(f"Content-Type: {request.headers.get('Content-Type', 'N/A')}")
275
+
276
+ data = request.get_json()
277
+ if not data:
278
+ logger.error("请求体为空或非 JSON")
279
+ return jsonify({"error": "invalid request body"}), 400
280
+
281
+ input_msgs = data.get("input", [])
282
+ logger.info(f"input 消息数量: {len(input_msgs)}")
283
+ logger.info(f"请求 model: {data.get('model', '(未指定)')}")
284
+ logger.info(f"请求 settings: {json.dumps(data.get('settings', {}))}")
285
+
286
+ messages = []
287
+ for i, msg in enumerate(input_msgs):
288
+ role = msg.get("role", "")
289
+ logger.debug(f" 消息[{i}]: role={role}, content_type={'list' if isinstance(msg.get('content'), list) else 'string'}")
290
+
291
+ if role == "developer":
292
+ logger.debug(f" 消息[{i}]: 将 role 'developer' → 'system'")
293
+ role = "system"
294
+
295
+ content = ""
296
+ if isinstance(msg.get("content"), list):
297
+ for j, part in enumerate(msg["content"]):
298
+ if part.get("type") == "input_text":
299
+ text = part.get("text", "")
300
+ content += text
301
+ logger.debug(f" 消息[{i}] part[{j}]: type=input_text, len={len(text)}")
302
+ else:
303
+ logger.debug(f" 消息[{i}] part[{j}]: type={part.get('type')}, 跳过")
304
+ else:
305
+ content = msg.get("content", "")
306
+
307
+ messages.append({"role": role, "content": content})
308
+ logger.info(f" 转换后 消息[{i}]: role={role}, content_len={len(content)}, content_preview={content[:80]!r}")
309
+
310
+ logger.info("消息转换完成,开始流式请求 DeepSeek")
311
+
312
+ return Response(
313
+ generate_codex_stream(auth_header, messages),
314
+ content_type="text/event-stream",
315
+ headers={
316
+ "Cache-Control": "no-cache",
317
+ "Connection": "keep-alive",
318
+ "X-Accel-Buffering": "no",
319
+ "Transfer-Encoding": "chunked"
320
+ }
321
+ )
322
+
323
+ # 模型公共信息
324
+ _MODEL_INFO = {
325
+ "id": "deepseek-v4-flash",
326
+ "object": "model",
327
+ "created": 1778889145,
328
+ "owned_by": "deepseek",
329
+ "context_window": 65536,
330
+ "max_output_tokens": 8192,
331
+ "type": "model",
332
+ "capabilities": {
333
+ "chat": True,
334
+ "streaming": True,
335
+ "tools": True,
336
+ "tool_choice": True
337
+ },
338
+ "pricing": {
339
+ "input": 0.0,
340
+ "output": 0.0,
341
+ "cached_input": 0.0
342
+ }
343
+ }
344
+ _EMPTY_MODELS_LIST = {"data": [_MODEL_INFO]}
345
+
346
+
347
+ @app.route("/v1/models", methods=["GET"])
348
+ def list_models():
349
+ logger.info(f"=== GET /v1/models {dict(request.args)} ===")
350
+ return jsonify(_EMPTY_MODELS_LIST)
351
+
352
+ @app.route("/v1/models/<model_id>", methods=["GET"])
353
+ def get_model(model_id):
354
+ logger.info(f"=== GET /v1/models/{model_id} {dict(request.args)} ===")
355
+ return jsonify({**_MODEL_INFO, "id": model_id})
356
+
357
+ def main():
358
+ """Entry point for CLI (`deepseek-proxy` command or `python -m deepseek_proxy`)."""
359
+ logger.info("=" * 50)
360
+ logger.info("DeepSeek Proxy 启动")
361
+ logger.info(f"监听地址: http://127.0.0.1:8787")
362
+ logger.info(f"DeepSeek API: {DEEPSEEK_CHAT_URL}")
363
+ logger.info(f"DEEPSEEK_API_KEY 已设置: {bool(os.environ.get('DEEPSEEK_API_KEY'))}")
364
+ logger.info("=" * 50)
365
+
366
+ try:
367
+ import uvicorn
368
+ if asgi_app is None:
369
+ raise ImportError("asgiref 未安装")
370
+ uvicorn.run(
371
+ asgi_app,
372
+ host="127.0.0.1",
373
+ port=8787,
374
+ log_level="info",
375
+ )
376
+ except ImportError as e:
377
+ logger.warning(f"ASGI 依赖未安装 ({e}),回退到 Flask 开发服务器")
378
+ app.run(host="127.0.0.1", port=8787, threaded=True)
379
+
380
+ if __name__ == "__main__":
381
+ main()
@@ -0,0 +1,2 @@
1
+ from deepseek_proxy import main
2
+ main()
@@ -0,0 +1,409 @@
1
+ Metadata-Version: 2.4
2
+ Name: deepseek-proxy
3
+ Version: 0.1.0
4
+ Summary: Translate OpenAI Responses API ↔ DeepSeek Chat Completions API for codex
5
+ Author: peinibiancheng
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/peinibiancheng/deepseek-proxy
8
+ Project-URL: Source, https://github.com/peinibiancheng/deepseek-proxy
9
+ Keywords: deepseek,claude-code,codex,proxy,responses-api
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: flask>=3.0
13
+ Requires-Dist: requests>=2.31
14
+ Requires-Dist: uvicorn>=0.27
15
+ Requires-Dist: asgiref>=3.7
16
+
17
+ <p align="center">
18
+ <a href="#english">English</a> | <a href="#chinese">中文</a>
19
+ </p>
20
+
21
+ ---
22
+
23
+ <h1 align="center">DeepSeek Proxy</h1>
24
+
25
+ <p align="center">
26
+ A lightweight proxy that translates <strong>OpenAI Responses API</strong> requests into <strong>DeepSeek Chat Completions API</strong> calls.
27
+ <br/>
28
+ Designed for use with <a href="https://claude.ai/code">Claude Code</a> as a backend model provider.
29
+ </p>
30
+
31
+ ---
32
+
33
+ <a name="english"></a>
34
+
35
+ # English
36
+
37
+ ## Motivation
38
+
39
+ **codex** (Claude Code CLI) uses the OpenAI **Responses API** (`/v1/responses`) natively, with SSE streaming and a specific event lifecycle. DeepSeek only provides a standard **Chat Completions API** (`/v1/chat/completions`). These two protocols are incompatible — you cannot simply point codex at a DeepSeek endpoint and expect it to work.
40
+
41
+ This proxy solves that problem by translating between the two protocols in real time, allowing codex to use DeepSeek as its backend model.
42
+
43
+ ## Overview
44
+
45
+ DeepSeek Proxy is a single-file Flask application that sits between codex and the DeepSeek API. It accepts requests in the Responses API streaming format, translates them into DeepSeek's chat completions format, and converts the streaming SSE output back into the Responses API event protocol.
46
+
47
+ ## Architecture
48
+
49
+ ```
50
+ ┌─────────────┐ Responses API SSE ┌────────────────┐ DeepSeek Chat API ┌────────────┐
51
+ │ codex │ ───────────────────────→ │ ds_proxy.py │ ──────────────────────→ │ DeepSeek │
52
+ │ (CLI Client)│ ←─────────────────────── │ (Flask Proxy) │ ←────────────────────── │ API │
53
+ └─────────────┘ SSE Events (7 types) └────────────────┘ SSE stream chunks └────────────┘
54
+ ```
55
+
56
+ ### Components
57
+
58
+ | Layer | Technology | Role |
59
+ |---|---|---|
60
+ | **Server** | Flask + uvicorn (ASGI) | HTTP server, SSE streaming |
61
+ | **Adapter** | `asgiref.wsgi.WsgiToAsgi` | WSGI → ASGI wrapper for uvicorn |
62
+ | **Translator** | Custom logic in `ds_proxy.py` | Responses API ↔ Chat API conversion |
63
+ | **Client** | `requests` (streaming) | HTTP client to DeepSeek API |
64
+
65
+ ### Endpoints
66
+
67
+ | Route | Method | Purpose |
68
+ |---|---|---|
69
+ | `/v1/responses` | POST | Accept Responses API request, return SSE stream |
70
+ | `/v1/models` | GET | List available model (`deepseek-v4-flash`) |
71
+ | `/v1/models/<id>` | GET | Get model capabilities |
72
+
73
+ ## Design Approach
74
+
75
+ The proxy follows a **stream-through** architecture:
76
+
77
+ 1. **No buffering** — as DeepSeek streams tokens, the proxy immediately forwards them as SSE events
78
+ 2. **Minimal transformation** — only the necessary field mappings are applied, keeping latency low
79
+ 3. **Fail-fast** — errors from DeepSeek are propagated back as SSE error events
80
+ 4. **Single-file** — the entire proxy is one Python file for easy deployment and modification
81
+
82
+ ## Processing Flow
83
+
84
+ ### Request Translation
85
+
86
+ When a client sends a request to `/v1/responses`, the proxy:
87
+
88
+ 1. Extracts the `input` array from the Responses API payload
89
+ 2. For each message:
90
+ - Maps `role: "developer"` → `role: "system"` (DeepSeek doesn't support "developer" role)
91
+ - Flattens content arrays into a single string (concatenates `input_text` parts)
92
+ - Passes through `role: "user"` and `role: "assistant"` unchanged
93
+ 3. Constructs a DeepSeek Chat Completions payload with `stream: true`
94
+ 4. Streams the request to DeepSeek's API
95
+
96
+ ### SSE Event Lifecycle
97
+
98
+ The proxy emits exactly these 8 events in order during a successful stream:
99
+
100
+ ```
101
+ 1. response.created → Stream begins, response metadata
102
+ 2. response.in_progress → Response status confirmed
103
+ 3. response.output_item.added → Output slot opened (type: "message")
104
+ 4. response.content_part.added → Text part initialized
105
+ 5. response.output_text.delta → Incremental content (one per token)
106
+ 6. response.output_text.done → Full text with aggregated content
107
+ 7. response.output_item.done → Output item completed
108
+ 8. response.completed → Full response with usage info
109
+ ```
110
+
111
+ On error, the proxy emits a single `response.error` event and stops.
112
+
113
+ ### Field Mapping
114
+
115
+ | OpenAI Responses API | DeepSeek Chat API | Direction |
116
+ |---|---|---|
117
+ | `input[].role: "developer"` | `messages[].role: "system"` | → |
118
+ | `input[].content[].input_text` | `messages[].content` (string) | → |
119
+ | `choices[0].delta.content` | `response.output_text.delta.delta` | ← |
120
+ | `usage.prompt_tokens` | `usage.input_tokens` | ← |
121
+ | `usage.completion_tokens` | `usage.output_tokens` | ← |
122
+ | `prompt_tokens_details.cached_tokens` | `input_tokens_details.cached_tokens` | ← |
123
+ | `completion_tokens_details.reasoning_tokens` | `output_tokens_details.reasoning_tokens` | ← |
124
+
125
+ ## Quick Start
126
+
127
+ ### Prerequisites
128
+
129
+ - Python 3.8+
130
+ - A DeepSeek API key
131
+
132
+ ### Installation
133
+
134
+ ```bash
135
+ git clone <repo-url>
136
+ cd deepseek-proxy
137
+ pip install flask requests uvicorn asgiref
138
+ ```
139
+
140
+ ### Run
141
+
142
+ ```bash
143
+ export DEEPSEEK_API_KEY=sk-your-key-here
144
+ python ds_proxy.py
145
+ ```
146
+
147
+ The proxy starts on `http://127.0.0.1:8787`.
148
+
149
+ ### Verify
150
+
151
+ ```bash
152
+ curl -X POST http://127.0.0.1:8787/v1/responses \
153
+ -H "Authorization: Bearer $DEEPSEEK_API_KEY" \
154
+ -H "Content-Type: application/json" \
155
+ -d '{"input":[{"role":"user","content":[{"type":"input_text","text":"hello"}]}],"model":"deepseek-v4-flash"}'
156
+ ```
157
+
158
+ ## Configure codex (Claude Code CLI)
159
+
160
+ codex can be configured to use the proxy in two ways.
161
+
162
+ ### Option A: Proxy config (simpler)
163
+
164
+ Add to `~/.claude/settings.local.json`:
165
+
166
+ ```json
167
+ {
168
+ "proxy": {
169
+ "url": "http://127.0.0.1:8787/v1/responses",
170
+ "model": "deepseek-v4-flash"
171
+ }
172
+ }
173
+ ```
174
+
175
+ This tells Claude Code to route all requests through the proxy URL and use the specified model.
176
+
177
+ ### Option B: Model provider config
178
+
179
+ Add to `~/.codex/config.toml`:
180
+
181
+ ```toml
182
+ model = "deepseek-v4-flash"
183
+ model_provider = "deepseek"
184
+
185
+ [model_providers.deepseek]
186
+ name = "DeepSeek"
187
+ base_url = "http://127.0.0.1:8787/v1"
188
+ env_key = "DEEPSEEK_API_KEY"
189
+ wire_api = "responses"
190
+ ```
191
+
192
+ This registers DeepSeek as a custom model provider, making it selectable alongside other providers.
193
+
194
+ ### Environment Variable
195
+
196
+ Regardless of which option you choose, set the API key:
197
+
198
+ ```bash
199
+ export DEEPSEEK_API_KEY=sk-your-actual-key-here
200
+ ```
201
+
202
+ The proxy reads this from the environment and passes it as the `Authorization` header to DeepSeek's API.
203
+
204
+ ### Verification
205
+
206
+ Run Claude Code and send a message:
207
+
208
+ ```bash
209
+ codex "hello"
210
+ ```
211
+
212
+ You should see a response from the DeepSeek model. Check the proxy logs for details:
213
+
214
+ ```bash
215
+ tail -f /tmp/ds_proxy.log
216
+ ```
217
+
218
+ ---
219
+
220
+ <a name="chinese"></a>
221
+
222
+ # 中文
223
+
224
+ ## 初衷
225
+
226
+ **codex**(Claude Code CLI)原生使用 OpenAI **Responses API**(`/v1/responses`),采用 SSE 流式传输和特定的事件生命周期。而 DeepSeek 只提供标准的 **Chat Completions API**(`/v1/chat/completions`)。这两种协议互不兼容——不能简单地把 codex 指向 DeepSeek 端点就指望它能工作。
227
+
228
+ 这个代理通过实时转换两种协议解决了这个问题,让 codex 可以使用 DeepSeek 作为后端模型。
229
+
230
+ ## 概述
231
+
232
+ DeepSeek Proxy 是一个单文件 Flask 应用,充当 codex 和 DeepSeek API 之间的桥梁。它将 Responses API 流式格式的请求转换为 DeepSeek 的对话补全格式,再将 DeepSeek 的流式输出转换回 Responses API 事件协议。
233
+
234
+ ## 架构
235
+
236
+ ```
237
+ ┌─────────────┐ Responses API SSE ┌────────────────┐ DeepSeek Chat API ┌────────────┐
238
+ │ codex │ ───────────────────────→ │ ds_proxy.py │ ──────────────────────→ │ DeepSeek │
239
+ │ (CLI 客户端) │ ←─────────────────────── │ (Flask 代理) │ ←────────────────────── │ API │
240
+ └─────────────┘ SSE 事件 (7 种类型) └────────────────┘ SSE 流式数据块 └────────────┘
241
+ ```
242
+
243
+ ### 组件
244
+
245
+ | 层级 | 技术 | 职责 |
246
+ |---|---|---|
247
+ | **服务器** | Flask + uvicorn (ASGI) | HTTP 服务、SSE 流式传输 |
248
+ | **适配器** | `asgiref.wsgi.WsgiToAsgi` | WSGI → ASGI 包装,用于 uvicorn |
249
+ | **转换器** | `ds_proxy.py` 中的自定义逻辑 | 请求/响应格式转换 |
250
+ | **客户端** | `requests` (流式) | 向 DeepSeek API 发送 HTTP 请求 |
251
+
252
+ ### 接口
253
+
254
+ | 路由 | 方法 | 用途 |
255
+ |---|---|---|
256
+ | `/v1/responses` | POST | 接收 Responses API 请求,返回 SSE 流 |
257
+ | `/v1/models` | GET | 列出可用模型 (`deepseek-v4-flash`) |
258
+ | `/v1/models/<id>` | GET | 获取模型能力信息 |
259
+
260
+ ## 设计思路
261
+
262
+ 代理采用**流式直通**架构:
263
+
264
+ 1. **不缓冲** — DeepSeek 逐 token 返回时,代理立即转发为 SSE 事件
265
+ 2. **最小转换** — 只应用必要的字段映射,保持低延迟
266
+ 3. **快速失败** — DeepSeek 的错误通过 SSE 错误事件传播回客户端
267
+ 4. **单文件** — 整个代理只有一个 Python 文件,便于部署和修改
268
+
269
+ ## 处理流程
270
+
271
+ ### 请求转换
272
+
273
+ 客户端向 `/v1/responses` 发送请求时,代理执行以下操作:
274
+
275
+ 1. 从 Responses API 请求体中提取 `input` 数组
276
+ 2. 对每条消息:
277
+ - 将 `role: "developer"` 映射为 `role: "system"`(DeepSeek 不支持 "developer" 角色)
278
+ - 将 content 数组合并为一个字符串(拼接所有 `input_text` 片段)
279
+ - `role: "user"` 和 `role: "assistant"` 保持不变
280
+ 3. 构造 DeepSeek Chat Completions 请求体,设置 `stream: true`
281
+ 4. 以流式方式向 DeepSeek API 发送请求
282
+
283
+ ### SSE 事件生命周期
284
+
285
+ 一次成功的流式响应会按顺序发送以下 8 个事件:
286
+
287
+ ```
288
+ 1. response.created → 流开始,响应元数据
289
+ 2. response.in_progress → 确认响应进行中
290
+ 3. response.output_item.added → 开启输出槽 (类型: "message")
291
+ 4. response.content_part.added → 初始化文本部分
292
+ 5. response.output_text.delta → 增量内容(每个 token 一次)
293
+ 6. response.output_text.done → 完整文本内容
294
+ 7. response.output_item.done → 输出项完成
295
+ 8. response.completed → 完整响应,含 usage 信息
296
+ ```
297
+
298
+ 发生错误时,代理发送一个 `response.error` 事件并停止。
299
+
300
+ ### 字段映射
301
+
302
+ | OpenAI Responses API | DeepSeek Chat API | 方向 |
303
+ |---|---|---|
304
+ | `input[].role: "developer"` | `messages[].role: "system"` | → |
305
+ | `input[].content[].input_text` | `messages[].content` (字符串) | → |
306
+ | `choices[0].delta.content` | `response.output_text.delta.delta` | ← |
307
+ | `usage.prompt_tokens` | `usage.input_tokens` | ← |
308
+ | `usage.completion_tokens` | `usage.output_tokens` | ← |
309
+ | `prompt_tokens_details.cached_tokens` | `input_tokens_details.cached_tokens` | ← |
310
+ | `completion_tokens_details.reasoning_tokens` | `output_tokens_details.reasoning_tokens` | ← |
311
+
312
+ ## 快速开始
313
+
314
+ ### 前置条件
315
+
316
+ - Python 3.8+
317
+ - DeepSeek API 密钥
318
+
319
+ ### 安装
320
+
321
+ ```bash
322
+ git clone <repo-url>
323
+ cd deepseek-proxy
324
+ pip install flask requests uvicorn asgiref
325
+ ```
326
+
327
+ ### 运行
328
+
329
+ ```bash
330
+ export DEEPSEEK_API_KEY=sk-your-key-here
331
+ python ds_proxy.py
332
+ ```
333
+
334
+ 代理启动在 `http://127.0.0.1:8787`。
335
+
336
+ ### 验证
337
+
338
+ ```bash
339
+ curl -X POST http://127.0.0.1:8787/v1/responses \
340
+ -H "Authorization: Bearer $DEEPSEEK_API_KEY" \
341
+ -H "Content-Type: application/json" \
342
+ -d '{"input":[{"role":"user","content":[{"type":"input_text","text":"你好"}]}],"model":"deepseek-v4-flash"}'
343
+ ```
344
+
345
+ ## 配置 codex (Claude Code CLI)
346
+
347
+ 有两种方式让 codex 使用代理。
348
+
349
+ ### 方式 A:代理配置(更简单)
350
+
351
+ 添加到 `~/.claude/settings.local.json`:
352
+
353
+ ```json
354
+ {
355
+ "proxy": {
356
+ "url": "http://127.0.0.1:8787/v1/responses",
357
+ "model": "deepseek-v4-flash"
358
+ }
359
+ }
360
+ ```
361
+
362
+ 这告诉 Claude Code 将所有请求通过代理 URL 路由,并使用指定模型。
363
+
364
+ ### 方式 B:模型提供商配置
365
+
366
+ 添加到 `~/.codex/config.toml`:
367
+
368
+ ```toml
369
+ model = "deepseek-v4-flash"
370
+ model_provider = "deepseek"
371
+
372
+ [model_providers.deepseek]
373
+ name = "DeepSeek"
374
+ base_url = "http://127.0.0.1:8787/v1"
375
+ env_key = "DEEPSEEK_API_KEY"
376
+ wire_api = "responses"
377
+ ```
378
+
379
+ 这将 DeepSeek 注册为自定义模型提供商,可以在多个提供商之间切换选择。
380
+
381
+ ### 环境变量
382
+
383
+ 无论选择哪种方式,都需要设置 API 密钥:
384
+
385
+ ```bash
386
+ export DEEPSEEK_API_KEY=sk-your-actual-key-here
387
+ ```
388
+
389
+ 代理从环境变量读取密钥,并将其作为 `Authorization` 头传递给 DeepSeek API。
390
+
391
+ ### 验证
392
+
393
+ 运行 Claude Code 发送消息:
394
+
395
+ ```bash
396
+ codex "hello"
397
+ ```
398
+
399
+ 你应该能看到来自 DeepSeek 模型的响应。查看代理日志获取详情:
400
+
401
+ ```bash
402
+ tail -f /tmp/ds_proxy.log
403
+ ```
404
+
405
+ ---
406
+
407
+ <p align="center">
408
+ <a href="#english">English</a> | <a href="#chinese">中文</a>
409
+ </p>
@@ -0,0 +1,7 @@
1
+ deepseek_proxy/__init__.py,sha256=fKRYGSya08HblyesTJNPOUKujbZFO19Vv5mkLtpbtWA,13008
2
+ deepseek_proxy/__main__.py,sha256=w8WYEWtr6bMaGCeJaVDLMcWJfCfSJY56XysCNXVdwio,39
3
+ deepseek_proxy-0.1.0.dist-info/METADATA,sha256=wDNMnInzVryEYu_39g0K3RBXV1xvY47l9v4v6YcoRWs,13758
4
+ deepseek_proxy-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
5
+ deepseek_proxy-0.1.0.dist-info/entry_points.txt,sha256=I21H-5j2InEEm56iEPXeXeWQxU-NPBp0sMJxLiA5zjg,55
6
+ deepseek_proxy-0.1.0.dist-info/top_level.txt,sha256=L7yQO91SECzjonuSnqXxA7HZ0Yx2mJXIFQkoNRFGaUc,15
7
+ deepseek_proxy-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ deepseek-proxy = deepseek_proxy:main
@@ -0,0 +1 @@
1
+ deepseek_proxy