open-sse 1.0.0
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 +180 -0
- package/config/constants.js +206 -0
- package/config/defaultThinkingSignature.js +7 -0
- package/config/ollamaModels.js +19 -0
- package/config/providerModels.js +161 -0
- package/handlers/chatCore.js +277 -0
- package/handlers/responsesHandler.js +69 -0
- package/index.js +69 -0
- package/package.json +44 -0
- package/services/accountFallback.js +148 -0
- package/services/combo.js +69 -0
- package/services/compact.js +64 -0
- package/services/model.js +109 -0
- package/services/provider.js +237 -0
- package/services/tokenRefresh.js +542 -0
- package/services/usage.js +398 -0
- package/translator/formats.js +12 -0
- package/translator/from-openai/claude.js +341 -0
- package/translator/from-openai/gemini.js +469 -0
- package/translator/from-openai/openai-responses.js +361 -0
- package/translator/helpers/claudeHelper.js +179 -0
- package/translator/helpers/geminiHelper.js +131 -0
- package/translator/helpers/openaiHelper.js +80 -0
- package/translator/helpers/responsesApiHelper.js +103 -0
- package/translator/helpers/toolCallHelper.js +111 -0
- package/translator/index.js +167 -0
- package/translator/to-openai/claude.js +238 -0
- package/translator/to-openai/gemini.js +151 -0
- package/translator/to-openai/openai-responses.js +140 -0
- package/translator/to-openai/openai.js +371 -0
- package/utils/bypassHandler.js +258 -0
- package/utils/error.js +133 -0
- package/utils/ollamaTransform.js +82 -0
- package/utils/requestLogger.js +217 -0
- package/utils/stream.js +274 -0
- package/utils/streamHandler.js +131 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared combo (model combo) handling with fallback support
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get combo models from combos data
|
|
7
|
+
* @param {string} modelStr - Model string to check
|
|
8
|
+
* @param {Array|Object} combosData - Array of combos or object with combos
|
|
9
|
+
* @returns {string[]|null} Array of models or null if not a combo
|
|
10
|
+
*/
|
|
11
|
+
export function getComboModelsFromData(modelStr, combosData) {
|
|
12
|
+
// Don't check if it's in provider/model format
|
|
13
|
+
if (modelStr.includes("/")) return null;
|
|
14
|
+
|
|
15
|
+
// Handle both array and object formats
|
|
16
|
+
const combos = Array.isArray(combosData) ? combosData : (combosData?.combos || []);
|
|
17
|
+
|
|
18
|
+
const combo = combos.find(c => c.name === modelStr);
|
|
19
|
+
if (combo && combo.models && combo.models.length > 0) {
|
|
20
|
+
return combo.models;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Handle combo chat with fallback
|
|
27
|
+
* @param {Object} options
|
|
28
|
+
* @param {Object} options.body - Request body
|
|
29
|
+
* @param {string[]} options.models - Array of model strings to try
|
|
30
|
+
* @param {Function} options.handleSingleModel - Function to handle single model: (body, modelStr) => Promise<Response>
|
|
31
|
+
* @param {Object} options.log - Logger object
|
|
32
|
+
* @returns {Promise<Response>}
|
|
33
|
+
*/
|
|
34
|
+
export async function handleComboChat({ body, models, handleSingleModel, log }) {
|
|
35
|
+
let lastError = null;
|
|
36
|
+
|
|
37
|
+
for (let i = 0; i < models.length; i++) {
|
|
38
|
+
const modelStr = models[i];
|
|
39
|
+
log.info("COMBO", `Trying model ${i + 1}/${models.length}: ${modelStr}`);
|
|
40
|
+
|
|
41
|
+
const result = await handleSingleModel(body, modelStr);
|
|
42
|
+
|
|
43
|
+
// Success or client error - return response
|
|
44
|
+
if (result.ok || result.status < 500) {
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 5xx error - try next model
|
|
49
|
+
lastError = `${modelStr}: ${result.statusText || result.status}`;
|
|
50
|
+
log.warn("COMBO", `Model failed, trying next`, { model: modelStr, status: result.status });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
log.warn("COMBO", "All models failed");
|
|
54
|
+
|
|
55
|
+
// Return 503 with last error
|
|
56
|
+
return new Response(
|
|
57
|
+
JSON.stringify({ error: lastError || "All combo models unavailable" }),
|
|
58
|
+
{
|
|
59
|
+
status: 503,
|
|
60
|
+
headers: { "Content-Type": "application/json" }
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Provider alias to ID mapping
|
|
2
|
+
const ALIAS_TO_PROVIDER_ID = {
|
|
3
|
+
cc: "claude",
|
|
4
|
+
cx: "codex",
|
|
5
|
+
gc: "gemini-cli",
|
|
6
|
+
qw: "qwen",
|
|
7
|
+
if: "iflow",
|
|
8
|
+
ag: "antigravity",
|
|
9
|
+
gh: "github",
|
|
10
|
+
// API Key providers (alias = id)
|
|
11
|
+
openai: "openai",
|
|
12
|
+
anthropic: "anthropic",
|
|
13
|
+
gemini: "gemini",
|
|
14
|
+
openrouter: "openrouter",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve provider alias to provider ID
|
|
19
|
+
*/
|
|
20
|
+
export function resolveProviderAlias(aliasOrId) {
|
|
21
|
+
return ALIAS_TO_PROVIDER_ID[aliasOrId] || aliasOrId;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parse model string: "alias/model" or "provider/model" or just alias
|
|
26
|
+
*/
|
|
27
|
+
export function parseModel(modelStr) {
|
|
28
|
+
if (!modelStr) {
|
|
29
|
+
return { provider: null, model: null, isAlias: false, providerAlias: null };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check if standard format: provider/model or alias/model
|
|
33
|
+
if (modelStr.includes("/")) {
|
|
34
|
+
const firstSlash = modelStr.indexOf("/");
|
|
35
|
+
const providerOrAlias = modelStr.slice(0, firstSlash);
|
|
36
|
+
const model = modelStr.slice(firstSlash + 1);
|
|
37
|
+
const provider = resolveProviderAlias(providerOrAlias);
|
|
38
|
+
return { provider, model, isAlias: false, providerAlias: providerOrAlias };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Alias format (model alias, not provider alias)
|
|
42
|
+
return { provider: null, model: modelStr, isAlias: true, providerAlias: null };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolve model alias from aliases object
|
|
47
|
+
* Format: { "alias": "provider/model" }
|
|
48
|
+
*/
|
|
49
|
+
export function resolveModelAliasFromMap(alias, aliases) {
|
|
50
|
+
if (!aliases) return null;
|
|
51
|
+
|
|
52
|
+
// Check if alias exists
|
|
53
|
+
const resolved = aliases[alias];
|
|
54
|
+
if (!resolved) return null;
|
|
55
|
+
|
|
56
|
+
// Resolved value is "provider/model" format
|
|
57
|
+
if (typeof resolved === "string" && resolved.includes("/")) {
|
|
58
|
+
const firstSlash = resolved.indexOf("/");
|
|
59
|
+
const providerOrAlias = resolved.slice(0, firstSlash);
|
|
60
|
+
return {
|
|
61
|
+
provider: resolveProviderAlias(providerOrAlias),
|
|
62
|
+
model: resolved.slice(firstSlash + 1)
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Or object { provider, model }
|
|
67
|
+
if (typeof resolved === "object" && resolved.provider && resolved.model) {
|
|
68
|
+
return {
|
|
69
|
+
provider: resolveProviderAlias(resolved.provider),
|
|
70
|
+
model: resolved.model
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get full model info (parse or resolve)
|
|
79
|
+
* @param {string} modelStr - Model string
|
|
80
|
+
* @param {object|function} aliasesOrGetter - Aliases object or async function to get aliases
|
|
81
|
+
*/
|
|
82
|
+
export async function getModelInfoCore(modelStr, aliasesOrGetter) {
|
|
83
|
+
const parsed = parseModel(modelStr);
|
|
84
|
+
|
|
85
|
+
if (!parsed.isAlias) {
|
|
86
|
+
return {
|
|
87
|
+
provider: parsed.provider,
|
|
88
|
+
model: parsed.model
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Get aliases (from object or function)
|
|
93
|
+
const aliases = typeof aliasesOrGetter === "function"
|
|
94
|
+
? await aliasesOrGetter()
|
|
95
|
+
: aliasesOrGetter;
|
|
96
|
+
|
|
97
|
+
// Resolve alias
|
|
98
|
+
const resolved = resolveModelAliasFromMap(parsed.model, aliases);
|
|
99
|
+
if (resolved) {
|
|
100
|
+
return resolved;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Fallback: treat as openai model
|
|
104
|
+
return {
|
|
105
|
+
provider: "openai",
|
|
106
|
+
model: parsed.model
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { PROVIDERS } from "../config/constants.js";
|
|
2
|
+
|
|
3
|
+
// Detect request format from body structure
|
|
4
|
+
export function detectFormat(body) {
|
|
5
|
+
// OpenAI Responses API: has input[] array instead of messages[]
|
|
6
|
+
if (body.input && Array.isArray(body.input)) {
|
|
7
|
+
return "openai-responses";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Gemini format: has contents array
|
|
11
|
+
if (body.contents && Array.isArray(body.contents)) {
|
|
12
|
+
return "gemini";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// OpenAI-specific indicators (check BEFORE Claude)
|
|
16
|
+
// These fields are OpenAI-specific and never appear in Claude format
|
|
17
|
+
if (
|
|
18
|
+
body.stream_options || // OpenAI streaming options
|
|
19
|
+
body.response_format || // JSON mode, etc.
|
|
20
|
+
body.logprobs !== undefined || // Log probabilities
|
|
21
|
+
body.top_logprobs !== undefined ||
|
|
22
|
+
body.n !== undefined || // Number of completions
|
|
23
|
+
body.presence_penalty !== undefined || // Penalties
|
|
24
|
+
body.frequency_penalty !== undefined ||
|
|
25
|
+
body.logit_bias || // Token biasing
|
|
26
|
+
body.user // User identifier
|
|
27
|
+
) {
|
|
28
|
+
return "openai";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Claude format: messages with content as array of objects with type
|
|
32
|
+
// Claude requires content to be array with specific structure
|
|
33
|
+
if (body.messages && Array.isArray(body.messages)) {
|
|
34
|
+
const firstMsg = body.messages[0];
|
|
35
|
+
|
|
36
|
+
// If content is array, check if it follows Claude structure
|
|
37
|
+
if (firstMsg?.content && Array.isArray(firstMsg.content)) {
|
|
38
|
+
const firstContent = firstMsg.content[0];
|
|
39
|
+
|
|
40
|
+
// Claude format has specific types: text, image, tool_use, tool_result
|
|
41
|
+
// OpenAI multimodal has: text, image_url (note the difference)
|
|
42
|
+
if (firstContent?.type === "text" && !body.model?.includes("/")) {
|
|
43
|
+
// Could be Claude or OpenAI multimodal
|
|
44
|
+
// Check for Claude-specific fields
|
|
45
|
+
if (body.system || body.anthropic_version) {
|
|
46
|
+
return "claude";
|
|
47
|
+
}
|
|
48
|
+
// Check if image format is Claude (source.type) vs OpenAI (image_url.url)
|
|
49
|
+
const hasClaudeImage = firstMsg.content.some(c =>
|
|
50
|
+
c.type === "image" && c.source?.type === "base64"
|
|
51
|
+
);
|
|
52
|
+
const hasOpenAIImage = firstMsg.content.some(c =>
|
|
53
|
+
c.type === "image_url" && c.image_url?.url
|
|
54
|
+
);
|
|
55
|
+
if (hasClaudeImage) return "claude";
|
|
56
|
+
if (hasOpenAIImage) return "openai";
|
|
57
|
+
|
|
58
|
+
// If still unclear, check for tool format
|
|
59
|
+
const hasClaudeTool = firstMsg.content.some(c =>
|
|
60
|
+
c.type === "tool_use" || c.type === "tool_result"
|
|
61
|
+
);
|
|
62
|
+
if (hasClaudeTool) return "claude";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// If content is string, it's likely OpenAI (Claude also supports this)
|
|
67
|
+
// Check for other Claude-specific indicators
|
|
68
|
+
if (body.system !== undefined || body.anthropic_version) {
|
|
69
|
+
return "claude";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Default to OpenAI format
|
|
74
|
+
return "openai";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Get provider config
|
|
78
|
+
export function getProviderConfig(provider) {
|
|
79
|
+
return PROVIDERS[provider] || PROVIDERS.openai;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Build provider URL
|
|
83
|
+
export function buildProviderUrl(provider, model, stream = true) {
|
|
84
|
+
const config = getProviderConfig(provider);
|
|
85
|
+
|
|
86
|
+
switch (provider) {
|
|
87
|
+
case "claude":
|
|
88
|
+
return `${config.baseUrl}?beta=true`;
|
|
89
|
+
|
|
90
|
+
case "gemini": {
|
|
91
|
+
const action = stream ? "streamGenerateContent?alt=sse" : "generateContent";
|
|
92
|
+
return `${config.baseUrl}/${model}:${action}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
case "gemini-cli": {
|
|
96
|
+
const action = stream ? "streamGenerateContent?alt=sse" : "generateContent";
|
|
97
|
+
return `${config.baseUrl}:${action}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
case "antigravity": {
|
|
101
|
+
const baseUrl = config.baseUrls[0];
|
|
102
|
+
const path = stream ? "/v1internal:streamGenerateContent?alt=sse" : "/v1internal:generateContent";
|
|
103
|
+
return `${baseUrl}${path}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
case "codex":
|
|
107
|
+
return config.baseUrl;
|
|
108
|
+
|
|
109
|
+
case "github":
|
|
110
|
+
return config.baseUrl;
|
|
111
|
+
|
|
112
|
+
case "glm":
|
|
113
|
+
case "kimi":
|
|
114
|
+
case "minimax":
|
|
115
|
+
// Claude-compatible providers
|
|
116
|
+
return `${config.baseUrl}?beta=true`;
|
|
117
|
+
|
|
118
|
+
default:
|
|
119
|
+
return config.baseUrl;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Build provider headers
|
|
124
|
+
export function buildProviderHeaders(provider, credentials, stream = true, body = null) {
|
|
125
|
+
const config = getProviderConfig(provider);
|
|
126
|
+
const headers = {
|
|
127
|
+
"Content-Type": "application/json",
|
|
128
|
+
...config.headers
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Add auth header
|
|
132
|
+
switch (provider) {
|
|
133
|
+
case "gemini":
|
|
134
|
+
if (credentials.apiKey) {
|
|
135
|
+
headers["x-goog-api-key"] = credentials.apiKey;
|
|
136
|
+
} else if (credentials.accessToken) {
|
|
137
|
+
headers["Authorization"] = `Bearer ${credentials.accessToken}`;
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
|
|
141
|
+
case "antigravity":
|
|
142
|
+
case "gemini-cli":
|
|
143
|
+
// Antigravity and Gemini CLI use OAuth access token
|
|
144
|
+
headers["Authorization"] = `Bearer ${credentials.accessToken}`;
|
|
145
|
+
break;
|
|
146
|
+
|
|
147
|
+
case "claude":
|
|
148
|
+
// Claude uses x-api-key header for API key, or Authorization for OAuth
|
|
149
|
+
if (credentials.apiKey) {
|
|
150
|
+
headers["x-api-key"] = credentials.apiKey;
|
|
151
|
+
} else if (credentials.accessToken) {
|
|
152
|
+
headers["Authorization"] = `Bearer ${credentials.accessToken}`;
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
|
|
156
|
+
case "github":
|
|
157
|
+
// GitHub Copilot requires special headers to mimic VSCode
|
|
158
|
+
// Prioritize copilotToken from providerSpecificData, fallback to accessToken
|
|
159
|
+
const githubToken = credentials.copilotToken || credentials.accessToken;
|
|
160
|
+
// Add headers in exact same order as test endpoint
|
|
161
|
+
headers["Authorization"] = `Bearer ${githubToken}`;
|
|
162
|
+
headers["Content-Type"] = "application/json";
|
|
163
|
+
headers["copilot-integration-id"] = "vscode-chat";
|
|
164
|
+
headers["editor-version"] = "vscode/1.107.1";
|
|
165
|
+
headers["editor-plugin-version"] = "copilot-chat/0.26.7";
|
|
166
|
+
headers["user-agent"] = "GitHubCopilotChat/0.26.7";
|
|
167
|
+
headers["openai-intent"] = "conversation-panel";
|
|
168
|
+
headers["x-github-api-version"] = "2025-04-01";
|
|
169
|
+
// Generate a UUID for x-request-id (Cloudflare Workers compatible)
|
|
170
|
+
headers["x-request-id"] = crypto.randomUUID ? crypto.randomUUID() :
|
|
171
|
+
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
172
|
+
const r = Math.random() * 16 | 0;
|
|
173
|
+
const v = c == 'x' ? r : (r & 0x3 | 0x8);
|
|
174
|
+
return v.toString(16);
|
|
175
|
+
});
|
|
176
|
+
headers["x-vscode-user-agent-library-version"] = "electron-fetch";
|
|
177
|
+
headers["X-Initiator"] = "user";
|
|
178
|
+
headers["Accept"] = "application/json";
|
|
179
|
+
break;
|
|
180
|
+
|
|
181
|
+
case "codex":
|
|
182
|
+
case "qwen":
|
|
183
|
+
case "openai":
|
|
184
|
+
case "openrouter":
|
|
185
|
+
headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`;
|
|
186
|
+
break;
|
|
187
|
+
|
|
188
|
+
case "glm":
|
|
189
|
+
case "kimi":
|
|
190
|
+
case "minimax":
|
|
191
|
+
// Claude-compatible API providers use x-api-key
|
|
192
|
+
headers["x-api-key"] = credentials.apiKey;
|
|
193
|
+
break;
|
|
194
|
+
|
|
195
|
+
default:
|
|
196
|
+
headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Stream accept header
|
|
201
|
+
if (stream) {
|
|
202
|
+
headers["Accept"] = "text/event-stream";
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return headers;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Get target format for provider
|
|
209
|
+
export function getTargetFormat(provider) {
|
|
210
|
+
const config = getProviderConfig(provider);
|
|
211
|
+
return config.format || "openai";
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Check if last message is from user
|
|
215
|
+
export function isLastMessageFromUser(body) {
|
|
216
|
+
const messages = body.messages || body.contents;
|
|
217
|
+
if (!messages?.length) return true;
|
|
218
|
+
const lastMsg = messages[messages.length - 1];
|
|
219
|
+
return lastMsg?.role === "user";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Check if request has thinking config
|
|
223
|
+
export function hasThinkingConfig(body) {
|
|
224
|
+
return !!(body.reasoning_effort || body.thinking?.type === "enabled");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Normalize thinking config based on last message role
|
|
228
|
+
// - If lastMessage is not user → remove thinking config
|
|
229
|
+
// - If lastMessage is user AND has thinking config → keep it (force enable)
|
|
230
|
+
export function normalizeThinkingConfig(body) {
|
|
231
|
+
if (!isLastMessageFromUser(body)) {
|
|
232
|
+
delete body.reasoning_effort;
|
|
233
|
+
delete body.thinking;
|
|
234
|
+
}
|
|
235
|
+
return body;
|
|
236
|
+
}
|
|
237
|
+
|