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.
Files changed (36) hide show
  1. package/README.md +180 -0
  2. package/config/constants.js +206 -0
  3. package/config/defaultThinkingSignature.js +7 -0
  4. package/config/ollamaModels.js +19 -0
  5. package/config/providerModels.js +161 -0
  6. package/handlers/chatCore.js +277 -0
  7. package/handlers/responsesHandler.js +69 -0
  8. package/index.js +69 -0
  9. package/package.json +44 -0
  10. package/services/accountFallback.js +148 -0
  11. package/services/combo.js +69 -0
  12. package/services/compact.js +64 -0
  13. package/services/model.js +109 -0
  14. package/services/provider.js +237 -0
  15. package/services/tokenRefresh.js +542 -0
  16. package/services/usage.js +398 -0
  17. package/translator/formats.js +12 -0
  18. package/translator/from-openai/claude.js +341 -0
  19. package/translator/from-openai/gemini.js +469 -0
  20. package/translator/from-openai/openai-responses.js +361 -0
  21. package/translator/helpers/claudeHelper.js +179 -0
  22. package/translator/helpers/geminiHelper.js +131 -0
  23. package/translator/helpers/openaiHelper.js +80 -0
  24. package/translator/helpers/responsesApiHelper.js +103 -0
  25. package/translator/helpers/toolCallHelper.js +111 -0
  26. package/translator/index.js +167 -0
  27. package/translator/to-openai/claude.js +238 -0
  28. package/translator/to-openai/gemini.js +151 -0
  29. package/translator/to-openai/openai-responses.js +140 -0
  30. package/translator/to-openai/openai.js +371 -0
  31. package/utils/bypassHandler.js +258 -0
  32. package/utils/error.js +133 -0
  33. package/utils/ollamaTransform.js +82 -0
  34. package/utils/requestLogger.js +217 -0
  35. package/utils/stream.js +274 -0
  36. 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
+