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 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
@@ -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
@@ -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
@@ -64,6 +64,3 @@ export {
64
64
  createSSETransformStreamWithLogger,
65
65
  createPassthroughStreamWithLogger
66
66
  } from "./utils/stream.js";
67
-
68
- export { createRequestLogger } from "./utils/requestLogger.js";
69
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-sse",
3
- "version": "1.0.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/router4.git",
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
- // 401 - Authentication error (token expired/invalid)
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: chunk.index || 0,
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.max_tokens;
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
- result.max_tokens = config.maxOutputTokens;
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, DEFAULT_MAX_TOKENS } from "../../config/constants.js";
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.max_tokens || DEFAULT_MAX_TOKENS,
10
+ max_tokens: adjustMaxTokens(body),
10
11
  stream: stream
11
12
  };
12
13
 
@@ -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(" ");
@@ -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
- // Only import fs/path in Node.js environment
9
- if (isNode) {
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} Logger object with methods to log each stage
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
- const sessionPath = createLogSession(sourceFormat, targetFormat, model);
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
  }