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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/server.js +23 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "freegate",
3
- "version": "0.6.2",
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
- // Debug: log raw message keys if any mention image/attachment/file
138
- if (!hasImage && Array.isArray(body.messages)) {
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
- if (m && typeof m === 'object' && (m.image || m.attachments || m.file || (Array.isArray(m.content) && m.content.some(c => c && c.image)))) {
141
- logger.info('Vision debug: обнаружено изображение в другом формате', {
142
- keys: Object.keys(m).slice(0, 8),
143
- contentTypes: Array.isArray(m.content) ? m.content.map(c => c && c.type) : null,
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
- return { role: 'user', content: `${m.content.find((c) => c.type === 'text')?.text || ''}\n\n[Содержимое скриншота]\n${cleaned}` };
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