From 36ca2b399c3ef8cddfe4d36b1f20ce32af97ece2 Mon Sep 17 00:00:00 2001 From: kazaff Date: Wed, 27 May 2026 11:09:08 +0800 Subject: [PATCH 1/3] ``` feat(proxy): add TypeScript implementation of DeepSeek proxy server Add a complete TypeScript proxy server that converts OpenAI Responses API requests to DeepSeek Chat Completions API. This includes support for streaming/non-streaming responses, tool/function call conversions, and XML function call parsing. - Convert OpenAI Responses API requests to DeepSeek Chat Completions format - Support both streaming and non-streaming responses - Handle tool/function call conversions - Parse XML function calls from DeepSeek responses - Map model names between OpenAI and DeepSeek - Provide health check endpoint - Implement graceful shutdown with signal handling Add AGENTS.md documentation covering project overview, features, API endpoints, environment variables, and development instructions. chore(converter): add usage conversion function for DeepSeek responses Add convert_deepseek_usage_to_responses function to properly map DeepSeek usage metrics (prompt_tokens, completion_tokens, total_tokens) to the expected response format. Apply this conversion across all response types including tool calls and regular responses. ``` --- AGENTS.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++ src/converter.ts | 20 +++++++++++++---- 2 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2b24dba --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,56 @@ +# Codex Bridge - DeepSeek Proxy + +A TypeScript proxy server that converts OpenAI Responses API requests to DeepSeek Chat Completions API. + +## Project Overview + +This is a translation of the Python proxy server (`deepseek_proxy.py`) to TypeScript. The server acts as a bridge between OpenAI's Responses API format and DeepSeek's Chat Completions API. + +## Key Features + +- Converts OpenAI Responses API requests to DeepSeek Chat Completions format +- Supports both streaming and non-streaming responses +- Handles tool/function call conversions +- Parses XML function calls from DeepSeek responses +- Maps model names between OpenAI and DeepSeek +- Provides health check endpoint +- Graceful shutdown with signal handling (SIGTERM, SIGINT) and optional HTTP shutdown endpoint + +## API Endpoints + +- `POST /v1/responses` - Main proxy endpoint +- `POST /responses` - Alternative endpoint +- `GET /health` - Health check +- `POST /shutdown` - Graceful shutdown endpoint (requires SHUTDOWN_SECRET environment variable and X-Shutdown-Secret header) + +## Environment Variables + +- `DEEPSEEK_API_KEY` - DeepSeek API key (primary) +- `OPENAI_API_KEY` - OpenAI API key (fallback) +- `SHUTDOWN_SECRET` - Optional secret for HTTP shutdown endpoint (if set, enables POST /shutdown) + +## Development + +The project uses TypeScript with Express.js. Key files: + +- `src/server.ts` - Main server file +- `src/converter.ts` - Request/response conversion logic +- `src/streaming.ts` - Streaming response handling + +## Code Style + +- TypeScript with strict typing +- Snake case naming for variables and functions +- No Chinese comments +- Error handling with proper HTTP status codes + +## Running the Server + +```bash +npm start +``` + +For development: +```bash +npm run dev +``` \ No newline at end of file diff --git a/src/converter.ts b/src/converter.ts index cc76a5d..f4b31e5 100644 --- a/src/converter.ts +++ b/src/converter.ts @@ -723,6 +723,18 @@ export function convert_responses_to_chat_completions(responses_request: OpenAiR return chat_request; } + +export function convert_deepseek_usage_to_responses(usage: any): any { + if (!usage || typeof usage !== 'object') { + return { input_tokens: 0, output_tokens: 0, total_tokens: 0 }; + } + return { + input_tokens: usage.prompt_tokens ?? 0, + output_tokens: usage.completion_tokens ?? 0, + total_tokens: usage.total_tokens ?? 0 + }; +} + export function convert_chat_completions_to_responses(deepseek_response: DeepSeekChatResponse, original_request: OpenAiResponsesRequest, logger: any): OpenAiResponsesResponse | null { // Check for tool calls in the response const choices = deepseek_response.choices || []; @@ -821,7 +833,7 @@ export function convert_tool_calls_response(deepseek_response: DeepSeekChatRespo message: { role: "assistant", content: "" }, finish_reason: "stop" }], - usage: deepseek_response.usage || {}, + usage: convert_deepseek_usage_to_responses(deepseek_response.usage), created: deepseek_response.created || 0 }; } @@ -860,7 +872,7 @@ export function convert_tool_calls_response(deepseek_response: DeepSeekChatRespo finish_reason: choices[0].finish_reason || "tool_calls" } ], - usage: deepseek_response.usage || {}, + usage: convert_deepseek_usage_to_responses(deepseek_response.usage), created: deepseek_response.created || 0 }; @@ -903,7 +915,7 @@ export function convert_regular_response(deepseek_response: DeepSeekChatResponse finish_reason: choices[0].finish_reason || "stop" } ], - usage: deepseek_response.usage || {}, + usage: convert_deepseek_usage_to_responses(deepseek_response.usage), created: deepseek_response.created || 0 }; @@ -957,7 +969,7 @@ export function convert_tool_calls_from_xml(deepseek_response: DeepSeekChatRespo finish_reason: "tool_calls" } ], - usage: deepseek_response.usage || {}, + usage: convert_deepseek_usage_to_responses(deepseek_response.usage), created: deepseek_response.created || 0 }; From 00d60837d8b9a176b2511a807ea2ff3aace2f7de Mon Sep 17 00:00:00 2001 From: kazaff Date: Wed, 27 May 2026 11:26:50 +0800 Subject: [PATCH 2/3] fix: pass usage from DeepSeek stream chunks to StreamChunk output --- src/converter.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/converter.ts b/src/converter.ts index f4b31e5..d7a04d5 100644 --- a/src/converter.ts +++ b/src/converter.ts @@ -1076,7 +1076,8 @@ export function convert_stream_chunk(chunk: any, original_request: OpenAiRespons delta: new_delta, finish_reason: finish_reason || null } - ] + ], + usage: chunk.usage || undefined }; // Add system_fingerprint if present, otherwise add default if (chunk.system_fingerprint !== undefined) { @@ -1108,7 +1109,8 @@ export function convert_stream_chunk(chunk: any, original_request: OpenAiRespons delta: new_delta, finish_reason: finish_reason || null } - ] + ], + usage: chunk.usage || undefined }; // Add system_fingerprint if present, otherwise add default if (chunk.system_fingerprint !== undefined) { From 9ef0642db11998625f686567d9e3fbe7b1a03850 Mon Sep 17 00:00:00 2001 From: kazaff Date: Wed, 27 May 2026 11:55:02 +0800 Subject: [PATCH 3/3] ``` feat(security): sanitize sensitive data from logs - Disable full request logging in converter.ts that could contain sensitive data - Implement header sanitization in server.ts to redact authorization, x-api-key, and shutdown secrets with truncated values - Remove debug file writing functionality that could leak sensitive data - Disable message logging in streaming.ts that could expose sensitive information ``` --- src/converter.ts | 4 ++-- src/server.ts | 20 ++++++++++++++++---- src/streaming.ts | 2 +- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/converter.ts b/src/converter.ts index d7a04d5..eac8302 100644 --- a/src/converter.ts +++ b/src/converter.ts @@ -373,7 +373,7 @@ export interface StreamChunk { export function convert_responses_to_chat_completions(responses_request: OpenAiResponsesRequest, logger: any): DeepSeekChatRequest { logger.debug(`DEBUG convert_responses_to_chat_completions called, keys: ${Object.keys(responses_request)}`); - logger.debug(`Full request: ${JSON.stringify(responses_request, null, 2)}`); + // logger.debug(`Full request: ${JSON.stringify(responses_request, null, 2)}`); // Disabled: could contain sensitive data // Use client's stream setting const stream = responses_request.stream || false; logger.info(`Using streaming mode: ${stream}`); @@ -718,7 +718,7 @@ export function convert_responses_to_chat_completions(responses_request: OpenAiR if (chat_request.tools !== undefined) { logger.info(`Tools count: ${chat_request.tools.length}`); } - logger.debug(`Messages: ${JSON.stringify(messages, null, 2)}`); + // logger.debug(`Messages: ${JSON.stringify(messages, null, 2)}`); // Disabled: could contain sensitive data return chat_request; } diff --git a/src/server.ts b/src/server.ts index be3ca9d..d0c3363 100644 --- a/src/server.ts +++ b/src/server.ts @@ -149,7 +149,21 @@ app.use(rateLimiter.middleware()); app.use((req: Request, _res: Response, next: NextFunction) => { const request_id = (req as any).requestId; logger.info(`Incoming ${req.method} request to ${req.path}`, { reqId: request_id }); - logger.info(`Headers: ${JSON.stringify(req.headers)}`, { reqId: request_id }); + // Sanitize headers to avoid leaking sensitive data like API keys + const sanitized_headers: Record = {}; + for (const [key, value] of Object.entries(req.headers)) { + sanitized_headers[key] = Array.isArray(value) ? value.join(', ') : (value || ''); + }; + if (sanitized_headers['authorization']) { + sanitized_headers['authorization'] = sanitized_headers['authorization'].substring(0, 15) + '... [REDACTED]'; + } + if (sanitized_headers['x-api-key']) { + sanitized_headers['x-api-key'] = sanitized_headers['x-api-key'].substring(0, 8) + '... [REDACTED]'; + } + if (sanitized_headers['x-shutdown-secret']) { + sanitized_headers['x-shutdown-secret'] = '[REDACTED]'; + } + logger.info(`Headers: ${JSON.stringify(sanitized_headers)}`, { reqId: request_id }); logger.info(`Content-Type: ${req.get('Content-Type')}`, { reqId: request_id }); if (req.get('Content-Length')) { logger.info(`Content-Length: ${req.get('Content-Length')}`, { reqId: request_id }); @@ -256,9 +270,7 @@ async function responses_proxy(req: Request, res: Response) { loggerWithReqId.info(`DEBUG stream value: ${data.stream}, type: ${typeof data.stream}`); console.log(`PROXY: Received request, model: ${data.model || 'unknown'}`); - // Write request to file for debugging - const fs = await import('fs'); - fs.writeFileSync(path.join(log_dir, 'request_debug_new.json'), JSON.stringify(data, null, 2), 'utf-8'); + // Debug file writing disabled to prevent leaking sensitive data // Resolve converter plugin for the requested model const modelName = data.model || 'deepseek-v4-pro'; diff --git a/src/streaming.ts b/src/streaming.ts index dbae059..71a952b 100644 --- a/src/streaming.ts +++ b/src/streaming.ts @@ -515,7 +515,7 @@ export async function handle_non_streaming_response( plugin: ConverterPlugin ): Promise { try { - logger.info(`Sending to DeepSeek (truncated): ${JSON.stringify(converted_data).substring(0, 1000)}`); + // logger.info(`Sending to DeepSeek (truncated): ${JSON.stringify(converted_data).substring(0, 1000)}`); // Disabled: could contain sensitive data const deepseek_start_time = Date.now(); const baseURL = new URL(target_url).origin;