add server

This commit is contained in:
jinhui.li
2025-02-25 16:44:06 +08:00
parent fd5ce5a9f8
commit 5a81d53045
6 changed files with 1331 additions and 1 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules

View File

@@ -1,8 +1,33 @@
# Claude Code Reverse
> You can switch the API endpoint by modifying the ANTHROPIC_BASE_URL environment variable.
![demo.png](https://github.com/musistudio/claude-code-reverse/blob/main/screenshoots/demo.png)
## Usage
1. Clone this repo
```shell
git clone git@github.com:musistudio/claude-code-reverse.git
```
2. Install dependencies
```shell
npm i
```
3. Change OpenAI apiKey and baseUrl in index.mjs file
4. Start server
```shell
node index.mjs
```
5. Set environment variable
```shell
export DISABLE_PROMPT_CACHING=1
export ANTHROPIC_BASE_URL="http://127.0.0.1:3456"
```
- Init Request
POST /v1/messages?beta=true
```json
{
@@ -30,6 +55,7 @@ POST /v1/messages?beta=true
- Ask Request
POST /v1/messages?beta=true
```json
{
@@ -59,6 +85,7 @@ POST /v1/messages?beta=true
- Ask Request 2
POST /v1/messages?beta=true
```json
{

284
index.mjs Normal file
View File

@@ -0,0 +1,284 @@
import express from "express";
import { OpenAI } from "openai";
const app = express();
const port = 3456;
app.use(express.json());
const chatClient = new OpenAI({
apiKey: "",
baseURL: "",
});
// Define POST /v1/messages interface
app.post("/v1/messages", async (req, res) => {
try {
let {
model,
max_tokens,
messages,
system = [],
temperature,
metadata,
tools,
} = req.body;
messages = messages.map((item) => {
if (item.content instanceof Array) {
return {
role: item.role,
content: item.content.map((it) => {
return {
type: ["tool_result", "tool_use"].includes(it?.type)
? "text"
: it?.type,
text: it?.content || it?.text || "",
};
}),
};
}
return {
role: item.role,
content: item.content,
};
});
const data = {
model: "qwen-max-2025-01-25",
messages: [
...system.map((item) => ({
role: "system",
content: item.text,
})),
...messages,
],
temperature,
stream: true,
};
if (tools) {
data.tools = tools.map((item) => ({
type: "function",
function: {
name: item.name,
description: item.description,
parameters: item.input_schema,
},
}));
}
const completion = await chatClient.chat.completions.create(data);
// Set SSE response headers
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
const messageId = "msg_" + Date.now();
let contentBlockIndex = 0;
let currentContentBlocks = [];
// Send message_start event
const messageStart = {
type: "message_start",
message: {
id: messageId,
type: "message",
role: "assistant",
content: [],
model,
stop_reason: null,
stop_sequence: null,
usage: { input_tokens: 1, output_tokens: 1 },
},
};
res.write(
`event: message_start\ndata: ${JSON.stringify(messageStart)}\n\n`
);
let isToolUse = false;
let toolUseJson = "";
let currentToolCall = null;
let hasStartedTextBlock = false;
for await (const chunk of completion) {
const delta = chunk.choices[0].delta;
// Handle tool call response
if (delta.tool_calls && delta.tool_calls.length > 0) {
const toolCall = delta.tool_calls[0];
if (!isToolUse) {
// Start new tool call block
isToolUse = true;
currentToolCall = toolCall;
const toolBlockStart = {
type: "content_block_start",
index: contentBlockIndex,
content_block: {
type: "tool_use",
id: `toolu_${Date.now()}`,
name: toolCall.function.name,
input: {},
},
};
// Add to content blocks list
currentContentBlocks.push({
type: "tool_use",
id: toolBlockStart.content_block.id,
name: toolCall.function.name,
input: {},
});
res.write(
`event: content_block_start\ndata: ${JSON.stringify(
toolBlockStart
)}\n\n`
);
toolUseJson = "";
}
// Stream tool call JSON
if (toolCall.function.arguments) {
const jsonDelta = {
type: "content_block_delta",
index: contentBlockIndex,
delta: {
type: "input_json_delta",
partial_json: toolCall.function.arguments,
},
};
toolUseJson += toolCall.function.arguments;
// Try to parse complete JSON and update content block
try {
const parsedJson = JSON.parse(toolUseJson);
currentContentBlocks[contentBlockIndex].input = parsedJson;
} catch (e) {
// JSON not yet complete, continue accumulating
}
res.write(
`event: content_block_delta\ndata: ${JSON.stringify(jsonDelta)}\n\n`
);
}
} else if (delta.content) {
// Handle regular text content
if (isToolUse) {
// End previous tool call block
const contentBlockStop = {
type: "content_block_stop",
index: contentBlockIndex,
};
res.write(
`event: content_block_stop\ndata: ${JSON.stringify(
contentBlockStop
)}\n\n`
);
contentBlockIndex++;
isToolUse = false;
}
if (!delta.content) continue;
// If text block not yet started, send content_block_start
if (!hasStartedTextBlock) {
const textBlockStart = {
type: "content_block_start",
index: contentBlockIndex,
content_block: {
type: "text",
text: "",
},
};
// Add to content blocks list
currentContentBlocks.push({
type: "text",
text: "",
});
res.write(
`event: content_block_start\ndata: ${JSON.stringify(
textBlockStart
)}\n\n`
);
hasStartedTextBlock = true;
}
// Send regular text content
const contentDelta = {
type: "content_block_delta",
index: contentBlockIndex,
delta: {
type: "text_delta",
text: delta.content,
},
};
// Update content block text
if (currentContentBlocks[contentBlockIndex]) {
currentContentBlocks[contentBlockIndex].text += delta.content;
}
res.write(
`event: content_block_delta\ndata: ${JSON.stringify(
contentDelta
)}\n\n`
);
}
}
// Close last content block
const contentBlockStop = {
type: "content_block_stop",
index: contentBlockIndex,
};
res.write(
`event: content_block_stop\ndata: ${JSON.stringify(
contentBlockStop
)}\n\n`
);
// Send message_delta event with appropriate stop_reason
const messageDelta = {
type: "message_delta",
delta: {
stop_reason: isToolUse ? "tool_use" : "end_turn",
stop_sequence: null,
content: currentContentBlocks,
},
usage: { input_tokens: 100, output_tokens: 150 },
};
res.write(
`event: message_delta\ndata: ${JSON.stringify(
messageDelta
)}\n\n`
);
// Send message_stop event
const messageStop = {
type: "message_stop",
};
res.write(
`event: message_stop\ndata: ${JSON.stringify(
messageStop
)}\n\n`
);
res.end();
} catch (error) {
console.error("Error in streaming response:", error);
res.status(400).json({
status: "error",
message: error.message,
});
}
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});

1001
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

17
package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "claude-code-reverse",
"version": "1.0.0",
"description": "> You can switch the API endpoint by modifying the ANTHROPIC_BASE_URL environment variable.",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.21.2",
"openai": "^4.85.4"
}
}

BIN
screenshoots/demo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB