Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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
```
30 changes: 22 additions & 8 deletions src/converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down Expand Up @@ -718,11 +718,23 @@ 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;
}


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 || [];
Expand Down Expand Up @@ -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
};
}
Expand Down Expand Up @@ -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
};

Expand Down Expand Up @@ -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
};

Expand Down Expand Up @@ -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
};

Expand Down Expand Up @@ -1064,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) {
Expand Down Expand Up @@ -1096,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) {
Expand Down
20 changes: 16 additions & 4 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {};
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 });
Expand Down Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion src/streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,7 @@ export async function handle_non_streaming_response(
plugin: ConverterPlugin
): Promise<any> {
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;
Expand Down