freegate 0.6.3 → 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 +21 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "freegate",
3
- "version": "0.6.3",
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,6 +134,24 @@ async function handleChatCompletion(req, res, body) {
134
134
  }
135
135
  return false;
136
136
  });
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)) {
140
+ for (const m of body.messages) {
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,
150
+ });
151
+ break;
152
+ }
153
+ }
154
+ }
137
155
  if (hasImage) {
138
156
  // Two-stage pipeline: try vision providers in order until one extracts text.
139
157
  const visionChain = ['gemini-vision', 'nim-vision', 'deepseek-vision']
@@ -211,8 +229,10 @@ async function handleChatCompletion(req, res, body) {
211
229
  const today = new Date().toISOString().slice(0, 10);
212
230
  // 'ratelimited' providers are alive but temporarily limited — include them
213
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.
214
234
  const healthyProviders = Object.entries(PROVIDERS)
215
- .filter(([_, p]) => p.enabled && !isCircuitOpen(p.key) &&
235
+ .filter(([_, p]) => p.enabled && !isCircuitOpen(p.key) && p.vision !== true &&
216
236
  (getHealth()[p.key]?.status === 'up' || getHealth()[p.key]?.status === 'ratelimited'));
217
237
 
218
238
  // Prefer providers below 90% of their daily limit; only fall back to