freegate 0.6.2 → 0.6.4
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/package.json +1 -1
- package/server.js +23 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "freegate",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "Free multi-provider LLM gateway with automatic failover. One OpenAI-compatible endpoint routes to 25 free models (Groq, Mistral, Gemini, NIM, OpenRouter, ZAI, Cerebras, DeepSeek). Never pay for LLMs.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
package/server.js
CHANGED
|
@@ -134,13 +134,19 @@ async function handleChatCompletion(req, res, body) {
|
|
|
134
134
|
}
|
|
135
135
|
return false;
|
|
136
136
|
});
|
|
137
|
-
//
|
|
138
|
-
|
|
137
|
+
// Log any message that LOOKS like it carries an image (any field), so we can
|
|
138
|
+
// adapt detection to opencode's actual format.
|
|
139
|
+
if (Array.isArray(body.messages)) {
|
|
139
140
|
for (const m of body.messages) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
const suspicious = m && (
|
|
142
|
+
m.attachments || m.image || m.file ||
|
|
143
|
+
(Array.isArray(m.content) && m.content.some(c => c && (c.image || c.image_url || c.file || c.data || c.type === 'file')))
|
|
144
|
+
);
|
|
145
|
+
if (suspicious) {
|
|
146
|
+
logger.info('Vision format detected', {
|
|
147
|
+
keys: Object.keys(m || {}),
|
|
148
|
+
contentTypes: Array.isArray(m.content) ? m.content.map(c => c && (c.type || 'plain')) : null,
|
|
149
|
+
contentKeys: Array.isArray(m.content) ? m.content.map(c => c && Object.keys(c).slice(0,5)) : null,
|
|
144
150
|
});
|
|
145
151
|
break;
|
|
146
152
|
}
|
|
@@ -162,7 +168,11 @@ async function handleChatCompletion(req, res, body) {
|
|
|
162
168
|
role: 'user',
|
|
163
169
|
content: [
|
|
164
170
|
{ type: 'text', text: 'Распознай и извлеки ВЕСЬ текст с изображения (ошибка, код, сообщение). Верни только содержимое, без комментариев. Если это код — верни код как есть.' },
|
|
165
|
-
...(Array.isArray(body.messages) ? body.messages.flatMap((m) => (Array.isArray(m.content) ? m.content.filter((c) => c.type === 'image_url' || c.type === 'image'
|
|
171
|
+
...(Array.isArray(body.messages) ? body.messages.flatMap((m) => (Array.isArray(m.content) ? m.content.filter((c) => c && (c.type === 'image_url' || c.type === 'image' || c.type === 'input_image')).map((c) => {
|
|
172
|
+
// Normalize any image part to the universal image_url format
|
|
173
|
+
const url = c.image_url?.url || c.image?.url || (c.image && typeof c.image === 'string' ? c.image : null) || c.url;
|
|
174
|
+
return url ? { type: 'image_url', image_url: { url } } : null;
|
|
175
|
+
}).filter(Boolean) : [])) : []),
|
|
166
176
|
],
|
|
167
177
|
}],
|
|
168
178
|
max_tokens: 2000,
|
|
@@ -183,8 +193,9 @@ async function handleChatCompletion(req, res, body) {
|
|
|
183
193
|
body = {
|
|
184
194
|
...body,
|
|
185
195
|
messages: userMsgs.map((m) => {
|
|
186
|
-
if (Array.isArray(m.content) && m.content.some((c) => c.type === 'image_url' || c.type === 'image')) {
|
|
187
|
-
|
|
196
|
+
if (Array.isArray(m.content) && m.content.some((c) => c && (c.type === 'image_url' || c.type === 'image' || c.type === 'input_image'))) {
|
|
197
|
+
const textPart = m.content.find((c) => c && c.type === 'text')?.text || '';
|
|
198
|
+
return { role: 'user', content: `${textPart}\n\n[Содержимое скриншота]\n${cleaned}` };
|
|
188
199
|
}
|
|
189
200
|
return m;
|
|
190
201
|
}),
|
|
@@ -218,8 +229,10 @@ async function handleChatCompletion(req, res, body) {
|
|
|
218
229
|
const today = new Date().toISOString().slice(0, 10);
|
|
219
230
|
// 'ratelimited' providers are alive but temporarily limited — include them
|
|
220
231
|
// (weighted down) so the pool never looks empty when many limits are hot.
|
|
232
|
+
// Vision providers are ONLY used for image requests (two-stage pipeline),
|
|
233
|
+
// never for plain text — otherwise they dominate the weighted selection.
|
|
221
234
|
const healthyProviders = Object.entries(PROVIDERS)
|
|
222
|
-
.filter(([_, p]) => p.enabled && !isCircuitOpen(p.key) &&
|
|
235
|
+
.filter(([_, p]) => p.enabled && !isCircuitOpen(p.key) && p.vision !== true &&
|
|
223
236
|
(getHealth()[p.key]?.status === 'up' || getHealth()[p.key]?.status === 'ratelimited'));
|
|
224
237
|
|
|
225
238
|
// Prefer providers below 90% of their daily limit; only fall back to
|