open-sse 1.0.0 → 1.0.2
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.
- package/README.md +18 -0
- package/config/constants.js +3 -0
- package/handlers/chatCore.js +1 -1
- package/index.js +0 -3
- package/package.json +2 -2
- package/services/accountFallback.js +16 -16
- package/translator/from-openai/claude.js +8 -1
- package/translator/helpers/maxTokensHelper.js +22 -0
- package/translator/to-openai/claude.js +2 -1
- package/translator/to-openai/gemini.js +4 -1
- package/translator/to-openai/openai.js +3 -2
- package/utils/bypassHandler.js +8 -0
- package/utils/requestLogger.js +37 -10
- package/utils/stream.js +5 -1
package/README.md
CHANGED
|
@@ -110,6 +110,24 @@ import { createSSETransformStreamWithLogger } from "open-sse";
|
|
|
110
110
|
|
|
111
111
|
## Configuration
|
|
112
112
|
|
|
113
|
+
### Environment Variables
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# Enable detailed request/response logging (default: false)
|
|
117
|
+
ENABLE_REQUEST_LOGS=true
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
When enabled, logs are saved to `logs/` directory with structure:
|
|
121
|
+
```
|
|
122
|
+
logs/
|
|
123
|
+
└── {sourceFormat}_{targetFormat}_{model}_{timestamp}/
|
|
124
|
+
├── 0_client_raw_request.json
|
|
125
|
+
├── 1_raw_request.json
|
|
126
|
+
├── 2_converted_request.json
|
|
127
|
+
├── 3_raw_response.json
|
|
128
|
+
└── 4_converted_response.json
|
|
129
|
+
```
|
|
130
|
+
|
|
113
131
|
### Provider Models
|
|
114
132
|
|
|
115
133
|
```javascript
|
package/config/constants.js
CHANGED
|
@@ -179,6 +179,9 @@ export const CACHE_TTL = {
|
|
|
179
179
|
// Default max tokens
|
|
180
180
|
export const DEFAULT_MAX_TOKENS = 64000;
|
|
181
181
|
|
|
182
|
+
// Minimum max tokens for tool calling (to prevent truncated arguments)
|
|
183
|
+
export const DEFAULT_MIN_TOKENS = 32000;
|
|
184
|
+
|
|
182
185
|
// Exponential backoff config for rate limits (like CLIProxyAPI)
|
|
183
186
|
export const BACKOFF_CONFIG = {
|
|
184
187
|
base: 1000, // 1 second base
|
package/handlers/chatCore.js
CHANGED
|
@@ -41,7 +41,7 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
|
|
|
41
41
|
const stream = body.stream !== false;
|
|
42
42
|
|
|
43
43
|
// Create request logger for this session: sourceFormat_targetFormat_model
|
|
44
|
-
const reqLogger = createRequestLogger(sourceFormat, targetFormat, model);
|
|
44
|
+
const reqLogger = await createRequestLogger(sourceFormat, targetFormat, model);
|
|
45
45
|
|
|
46
46
|
// 0. Log client raw request (before any conversion)
|
|
47
47
|
if (clientRawRequest) {
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-sse",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Universal AI proxy library with SSE streaming support for OpenAI, Claude, Gemini and more",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"repository": {
|
|
23
23
|
"type": "git",
|
|
24
|
-
"url": "https://github.com/yourusername/
|
|
24
|
+
"url": "https://github.com/yourusername/9router.git",
|
|
25
25
|
"directory": "open-sse"
|
|
26
26
|
},
|
|
27
27
|
"keywords": [
|
|
@@ -19,22 +19,7 @@ export function getQuotaCooldown(backoffLevel = 0) {
|
|
|
19
19
|
* @returns {{ shouldFallback: boolean, cooldownMs: number, newBackoffLevel?: number }}
|
|
20
20
|
*/
|
|
21
21
|
export function checkFallbackError(status, errorText, backoffLevel = 0) {
|
|
22
|
-
//
|
|
23
|
-
if (status === 401) {
|
|
24
|
-
return { shouldFallback: true, cooldownMs: COOLDOWN_MS.unauthorized };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// 402/403 - Payment required / Forbidden (quota/permission)
|
|
28
|
-
if (status === 402 || status === 403) {
|
|
29
|
-
return { shouldFallback: true, cooldownMs: COOLDOWN_MS.paymentRequired };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// 404 - Model not found (long cooldown)
|
|
33
|
-
if (status === 404) {
|
|
34
|
-
return { shouldFallback: true, cooldownMs: COOLDOWN_MS.notFound };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Check error message FIRST (before status codes) for specific patterns
|
|
22
|
+
// Check error message FIRST - specific patterns take priority over status codes
|
|
38
23
|
if (errorText) {
|
|
39
24
|
const lowerError = errorText.toLowerCase();
|
|
40
25
|
|
|
@@ -60,6 +45,21 @@ export function checkFallbackError(status, errorText, backoffLevel = 0) {
|
|
|
60
45
|
}
|
|
61
46
|
}
|
|
62
47
|
|
|
48
|
+
// 401 - Authentication error (token expired/invalid)
|
|
49
|
+
if (status === 401) {
|
|
50
|
+
return { shouldFallback: true, cooldownMs: COOLDOWN_MS.unauthorized };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 402/403 - Payment required / Forbidden (quota/permission)
|
|
54
|
+
if (status === 402 || status === 403) {
|
|
55
|
+
return { shouldFallback: true, cooldownMs: COOLDOWN_MS.paymentRequired };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 404 - Model not found (long cooldown)
|
|
59
|
+
if (status === 404) {
|
|
60
|
+
return { shouldFallback: true, cooldownMs: COOLDOWN_MS.notFound };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
63
|
// 429 - Rate limit with exponential backoff
|
|
64
64
|
if (status === 429) {
|
|
65
65
|
const newLevel = Math.min(backoffLevel + 1, BACKOFF_CONFIG.maxLevel);
|
|
@@ -27,6 +27,8 @@ function claudeToOpenAIResponse(chunk, state) {
|
|
|
27
27
|
case "message_start": {
|
|
28
28
|
state.messageId = chunk.message?.id || `msg_${Date.now()}`;
|
|
29
29
|
state.model = chunk.message?.model;
|
|
30
|
+
state.toolCallIndex = 0; // Reset tool call counter for OpenAI format
|
|
31
|
+
console.log("🔍 ----------- toolCallIndex", state.toolCallIndex);
|
|
30
32
|
results.push(createChunk(state, { role: "assistant" }));
|
|
31
33
|
break;
|
|
32
34
|
}
|
|
@@ -41,8 +43,10 @@ function claudeToOpenAIResponse(chunk, state) {
|
|
|
41
43
|
state.currentBlockIndex = chunk.index;
|
|
42
44
|
results.push(createChunk(state, { content: "<think>" }));
|
|
43
45
|
} else if (block?.type === "tool_use") {
|
|
46
|
+
// OpenAI format: tool_calls index must be independent and start from 0
|
|
47
|
+
const toolCallIndex = state.toolCallIndex++;
|
|
44
48
|
const toolCall = {
|
|
45
|
-
index:
|
|
49
|
+
index: toolCallIndex,
|
|
46
50
|
id: block.id,
|
|
47
51
|
type: "function",
|
|
48
52
|
function: {
|
|
@@ -50,6 +54,7 @@ function claudeToOpenAIResponse(chunk, state) {
|
|
|
50
54
|
arguments: ""
|
|
51
55
|
}
|
|
52
56
|
};
|
|
57
|
+
// Map Claude content_block index to OpenAI tool_call index
|
|
53
58
|
state.toolCalls.set(chunk.index, toolCall);
|
|
54
59
|
results.push(createChunk(state, { tool_calls: [toolCall] }));
|
|
55
60
|
}
|
|
@@ -67,9 +72,11 @@ function claudeToOpenAIResponse(chunk, state) {
|
|
|
67
72
|
const toolCall = state.toolCalls.get(chunk.index);
|
|
68
73
|
if (toolCall) {
|
|
69
74
|
toolCall.function.arguments += delta.partial_json;
|
|
75
|
+
// Include both index and id for better client compatibility
|
|
70
76
|
results.push(createChunk(state, {
|
|
71
77
|
tool_calls: [{
|
|
72
78
|
index: toolCall.index,
|
|
79
|
+
id: toolCall.id,
|
|
73
80
|
function: { arguments: delta.partial_json }
|
|
74
81
|
}]
|
|
75
82
|
}));
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { DEFAULT_MAX_TOKENS, DEFAULT_MIN_TOKENS } from "../../config/constants.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Adjust max_tokens based on request context
|
|
5
|
+
* @param {object} body - Request body
|
|
6
|
+
* @returns {number} Adjusted max_tokens
|
|
7
|
+
*/
|
|
8
|
+
export function adjustMaxTokens(body) {
|
|
9
|
+
let maxTokens = body.max_tokens || DEFAULT_MAX_TOKENS;
|
|
10
|
+
|
|
11
|
+
// Auto-increase for tool calling to prevent truncated arguments
|
|
12
|
+
// Tool calls with large content (like writing files) need more tokens
|
|
13
|
+
if (body.tools && Array.isArray(body.tools) && body.tools.length > 0) {
|
|
14
|
+
if (maxTokens < DEFAULT_MIN_TOKENS) {
|
|
15
|
+
console.log(`[AUTO-ADJUST] max_tokens: ${maxTokens} → ${DEFAULT_MIN_TOKENS} (tool calling detected)`);
|
|
16
|
+
maxTokens = DEFAULT_MIN_TOKENS;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return maxTokens;
|
|
21
|
+
}
|
|
22
|
+
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { register } from "../index.js";
|
|
2
2
|
import { FORMATS } from "../formats.js";
|
|
3
|
+
import { adjustMaxTokens } from "../helpers/maxTokensHelper.js";
|
|
3
4
|
|
|
4
5
|
// Convert Claude request to OpenAI format
|
|
5
6
|
function claudeToOpenAI(model, body, stream) {
|
|
@@ -11,7 +12,7 @@ function claudeToOpenAI(model, body, stream) {
|
|
|
11
12
|
|
|
12
13
|
// Max tokens
|
|
13
14
|
if (body.max_tokens) {
|
|
14
|
-
result.max_tokens = body
|
|
15
|
+
result.max_tokens = adjustMaxTokens(body);
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
// Temperature
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { register } from "../index.js";
|
|
2
2
|
import { FORMATS } from "../formats.js";
|
|
3
|
+
import { adjustMaxTokens } from "../helpers/maxTokensHelper.js";
|
|
3
4
|
|
|
4
5
|
// Convert Gemini request to OpenAI format
|
|
5
6
|
function geminiToOpenAI(model, body, stream) {
|
|
@@ -13,7 +14,9 @@ function geminiToOpenAI(model, body, stream) {
|
|
|
13
14
|
if (body.generationConfig) {
|
|
14
15
|
const config = body.generationConfig;
|
|
15
16
|
if (config.maxOutputTokens) {
|
|
16
|
-
|
|
17
|
+
// Create temporary body object for adjustMaxTokens
|
|
18
|
+
const tempBody = { max_tokens: config.maxOutputTokens, tools: body.tools };
|
|
19
|
+
result.max_tokens = adjustMaxTokens(tempBody);
|
|
17
20
|
}
|
|
18
21
|
if (config.temperature !== undefined) {
|
|
19
22
|
result.temperature = config.temperature;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { register } from "../index.js";
|
|
2
2
|
import { FORMATS } from "../formats.js";
|
|
3
|
-
import { CLAUDE_SYSTEM_PROMPT
|
|
3
|
+
import { CLAUDE_SYSTEM_PROMPT } from "../../config/constants.js";
|
|
4
|
+
import { adjustMaxTokens } from "../helpers/maxTokensHelper.js";
|
|
4
5
|
|
|
5
6
|
// Convert OpenAI request to Claude format
|
|
6
7
|
function openaiToClaude(model, body, stream) {
|
|
7
8
|
const result = {
|
|
8
9
|
model: model,
|
|
9
|
-
max_tokens: body
|
|
10
|
+
max_tokens: adjustMaxTokens(body),
|
|
10
11
|
stream: stream
|
|
11
12
|
};
|
|
12
13
|
|
package/utils/bypassHandler.js
CHANGED
|
@@ -32,6 +32,14 @@ export function handleBypassRequest(body, model) {
|
|
|
32
32
|
const firstText = getText(messages[0]?.content);
|
|
33
33
|
if (firstText === "Warmup") shouldBypass = true;
|
|
34
34
|
|
|
35
|
+
// Check count pattern: [{"role":"user","content":"count"}]
|
|
36
|
+
if (!shouldBypass &&
|
|
37
|
+
messages.length === 1 &&
|
|
38
|
+
messages[0]?.role === "user" &&
|
|
39
|
+
firstText === "count") {
|
|
40
|
+
shouldBypass = true;
|
|
41
|
+
}
|
|
42
|
+
|
|
35
43
|
// Check skip patterns
|
|
36
44
|
if (!shouldBypass && SKIP_PATTERNS?.length) {
|
|
37
45
|
const allText = messages.map(m => getText(m.content)).join(" ");
|
package/utils/requestLogger.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
// Check if running in Node.js environment (has fs module)
|
|
2
|
-
const isNode = typeof process !== "undefined" && process.versions?.node;
|
|
2
|
+
const isNode = typeof process !== "undefined" && process.versions?.node && typeof window === "undefined";
|
|
3
|
+
|
|
4
|
+
// Check if logging is enabled via environment variable (default: false)
|
|
5
|
+
const LOGGING_ENABLED = typeof process !== "undefined" && process.env?.ENABLE_REQUEST_LOGS === 'true';
|
|
3
6
|
|
|
4
7
|
let fs = null;
|
|
5
8
|
let path = null;
|
|
6
9
|
let LOGS_DIR = null;
|
|
7
10
|
|
|
8
|
-
//
|
|
9
|
-
|
|
11
|
+
// Lazy load Node.js modules (avoid top-level await)
|
|
12
|
+
async function ensureNodeModules() {
|
|
13
|
+
if (!isNode || !LOGGING_ENABLED || fs) return;
|
|
10
14
|
try {
|
|
11
15
|
fs = await import("fs");
|
|
12
16
|
path = await import("path");
|
|
13
|
-
LOGS_DIR = path.join(process.cwd(), "logs");
|
|
17
|
+
LOGS_DIR = path.join(typeof process !== "undefined" && process.cwd ? process.cwd() : ".", "logs");
|
|
14
18
|
} catch {
|
|
15
|
-
// Running in non-Node environment (Worker, etc.)
|
|
19
|
+
// Running in non-Node environment (Worker, Browser, etc.)
|
|
16
20
|
}
|
|
17
21
|
}
|
|
18
22
|
|
|
@@ -29,7 +33,8 @@ function formatTimestamp(date = new Date()) {
|
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
// Create log session folder: {sourceFormat}_{targetFormat}_{model}_{timestamp}
|
|
32
|
-
function createLogSession(sourceFormat, targetFormat, model) {
|
|
36
|
+
async function createLogSession(sourceFormat, targetFormat, model) {
|
|
37
|
+
await ensureNodeModules();
|
|
33
38
|
if (!fs || !LOGS_DIR) return null;
|
|
34
39
|
|
|
35
40
|
try {
|
|
@@ -81,18 +86,40 @@ function maskSensitiveHeaders(headers) {
|
|
|
81
86
|
return masked;
|
|
82
87
|
}
|
|
83
88
|
|
|
89
|
+
// No-op logger when logging is disabled
|
|
90
|
+
function createNoOpLogger() {
|
|
91
|
+
return {
|
|
92
|
+
sessionPath: null,
|
|
93
|
+
logClientRawRequest() {},
|
|
94
|
+
logRawRequest() {},
|
|
95
|
+
logFormatInfo() {},
|
|
96
|
+
logConvertedRequest() {},
|
|
97
|
+
logRawResponse() {},
|
|
98
|
+
logConvertedResponse() {},
|
|
99
|
+
logStreamChunk() {},
|
|
100
|
+
logStreamComplete() {},
|
|
101
|
+
logError() {}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
84
105
|
/**
|
|
85
106
|
* Create a new log session and return logger functions
|
|
86
107
|
* @param {string} sourceFormat - Source format from client (claude, openai, etc.)
|
|
87
108
|
* @param {string} targetFormat - Target format to provider (antigravity, gemini-cli, etc.)
|
|
88
109
|
* @param {string} model - Model name
|
|
89
|
-
* @returns {object}
|
|
110
|
+
* @returns {Promise<object>} Promise that resolves to logger object with methods to log each stage
|
|
90
111
|
*/
|
|
91
|
-
export function createRequestLogger(sourceFormat, targetFormat, model) {
|
|
92
|
-
|
|
112
|
+
export async function createRequestLogger(sourceFormat, targetFormat, model) {
|
|
113
|
+
// Return no-op logger if logging is disabled
|
|
114
|
+
if (!LOGGING_ENABLED) {
|
|
115
|
+
return createNoOpLogger();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Wait for session to be created before returning logger
|
|
119
|
+
const sessionPath = await createLogSession(sourceFormat, targetFormat, model);
|
|
93
120
|
|
|
94
121
|
return {
|
|
95
|
-
sessionPath,
|
|
122
|
+
get sessionPath() { return sessionPath; },
|
|
96
123
|
|
|
97
124
|
// 0. Log client raw request (before any conversion)
|
|
98
125
|
logClientRawRequest(endpoint, body, headers = {}) {
|
package/utils/stream.js
CHANGED
|
@@ -74,7 +74,11 @@ function parseSSELine(line) {
|
|
|
74
74
|
|
|
75
75
|
try {
|
|
76
76
|
return JSON.parse(data);
|
|
77
|
-
} catch {
|
|
77
|
+
} catch (error) {
|
|
78
|
+
// Log parse errors for debugging incomplete chunks
|
|
79
|
+
if (data.length > 0 && data.length < 1000) {
|
|
80
|
+
console.log(`[WARN] Failed to parse SSE line (${data.length} chars): ${data.substring(0, 100)}...`);
|
|
81
|
+
}
|
|
78
82
|
return null;
|
|
79
83
|
}
|
|
80
84
|
}
|