opencode-pollinations-plugin 6.0.0-beta.19 → 6.0.0-beta.2

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.
@@ -1,12 +1,11 @@
1
- /**
2
- * generate-config.ts - v6.0 Simplified
3
- *
4
- * Single endpoint: gen.pollinations.ai/text/models
5
- * No more Free tier, no cache ETag, no prefixes
6
- */
7
1
  import * as https from 'https';
8
2
  import * as fs from 'fs';
3
+ import * as os from 'os';
4
+ import * as path from 'path';
9
5
  import { loadConfig } from './config.js';
6
+ const HOMEDIR = os.homedir();
7
+ const CONFIG_DIR_POLLI = path.join(HOMEDIR, '.pollinations');
8
+ const CONFIG_FILE = path.join(CONFIG_DIR_POLLI, 'config.json');
10
9
  // --- LOGGING ---
11
10
  const LOG_FILE = '/tmp/opencode_pollinations_config.log';
12
11
  function log(msg) {
@@ -14,18 +13,15 @@ function log(msg) {
14
13
  const ts = new Date().toISOString();
15
14
  if (!fs.existsSync(LOG_FILE))
16
15
  fs.writeFileSync(LOG_FILE, '');
17
- fs.appendFileSync(LOG_FILE, `[ConfigGen v6.0] ${ts} ${msg}\n`);
16
+ fs.appendFileSync(LOG_FILE, `[ConfigGen] ${ts} ${msg}\n`);
18
17
  }
19
18
  catch (e) { }
19
+ // Force output to stderr for CLI visibility if needed, but clean.
20
20
  }
21
- // --- NETWORK HELPER ---
21
+ // Fetch Helper
22
22
  function fetchJson(url, headers = {}) {
23
23
  return new Promise((resolve, reject) => {
24
- const finalHeaders = {
25
- ...headers,
26
- 'User-Agent': 'Mozilla/5.0 (compatible; OpenCode-Pollinations/6.0; +https://opencode.ai)'
27
- };
28
- const req = https.get(url, { headers: finalHeaders }, (res) => {
24
+ const req = https.get(url, { headers }, (res) => {
29
25
  let data = '';
30
26
  res.on('data', chunk => data += chunk);
31
27
  res.on('end', () => {
@@ -35,7 +31,7 @@ function fetchJson(url, headers = {}) {
35
31
  }
36
32
  catch (e) {
37
33
  log(`JSON Parse Error for ${url}: ${e}`);
38
- resolve([]);
34
+ resolve([]); // Fail safe -> empty list
39
35
  }
40
36
  });
41
37
  });
@@ -43,132 +39,200 @@ function fetchJson(url, headers = {}) {
43
39
  log(`Network Error for ${url}: ${e.message}`);
44
40
  reject(e);
45
41
  });
46
- req.setTimeout(15000, () => {
42
+ req.setTimeout(5000, () => {
47
43
  req.destroy();
48
44
  reject(new Error('Timeout'));
49
45
  });
50
46
  });
51
47
  }
52
- // --- GENERATOR LOGIC ---
53
- export async function generatePollinationsConfig(forceApiKey) {
48
+ function formatName(id, censored = true) {
49
+ let clean = id.replace(/^pollinations\//, '').replace(/-/g, ' ');
50
+ clean = clean.replace(/\b\w/g, l => l.toUpperCase());
51
+ if (!censored)
52
+ clean += " (Uncensored)";
53
+ return clean;
54
+ }
55
+ // --- MAIN GENERATOR logic ---
56
+ // --- MAIN GENERATOR logic ---
57
+ export async function generatePollinationsConfig(forceApiKey, forceStrict = false) {
54
58
  const config = loadConfig();
55
59
  const modelsOutput = [];
60
+ log(`Starting Configuration (V5.1.22 Hot-Reload)...`);
61
+ // Use forced key (from Hook) or cached key
56
62
  const effectiveKey = forceApiKey || config.apiKey;
57
- log(`Starting Configuration v6.0...`);
58
- log(`API Key present: ${!!effectiveKey}`);
59
- // 1. ALWAYS add "connect" placeholder model
60
- modelsOutput.push({
61
- id: 'connect',
62
- name: '🔑 Connect your Pollinations Account',
63
- modalities: { input: ['text'], output: ['text'] },
64
- tool_call: false
65
- });
66
- // 2. If no API key, return only connect placeholder
67
- if (!effectiveKey || effectiveKey.length < 5 || effectiveKey === 'dummy') {
68
- log('No API key configured. Returning connect placeholder only.');
69
- return modelsOutput;
70
- }
71
- // 3. Fetch models from Enterprise endpoint
63
+ // 1. FREE UNIVERSE
72
64
  try {
73
- log('Fetching models from gen.pollinations.ai/text/models...');
74
- const rawList = await fetchJson('https://gen.pollinations.ai/text/models', {
75
- 'Authorization': `Bearer ${effectiveKey}`
76
- });
77
- const modelsList = Array.isArray(rawList) ? rawList : (rawList.data || []);
78
- log(`Received ${modelsList.length} models from API`);
79
- modelsList.forEach((m) => {
80
- // Skip models without tools support (except nomnom - special handling)
81
- if (m.tools === false && m.name !== 'nomnom') {
82
- log(`Skipping ${m.name} (no tools)`);
83
- return;
84
- }
85
- const mapped = mapModel(m);
65
+ // Switch to main models endpoint (User provided curl confirms it has 'description')
66
+ const freeList = await fetchJson('https://text.pollinations.ai/models');
67
+ const list = Array.isArray(freeList) ? freeList : (freeList.data || []);
68
+ list.forEach((m) => {
69
+ const mapped = mapModel(m, 'free/', '[Free] ');
86
70
  modelsOutput.push(mapped);
87
71
  });
88
- log(`Total models registered: ${modelsOutput.length}`);
89
- log(`Model IDs: ${modelsOutput.map(m => m.id).join(', ')}`);
72
+ log(`Fetched ${modelsOutput.length} Free models.`);
90
73
  }
91
74
  catch (e) {
92
- log(`Error fetching models: ${e.message}`);
93
- // Return connect placeholder only on error
75
+ log(`Error fetching Free models: ${e}`);
76
+ // Fallback Robust (Offline support)
77
+ modelsOutput.push({ id: "free/mistral", name: "[Free] Mistral Nemo (Fallback)", object: "model", variants: {} });
78
+ modelsOutput.push({ id: "free/openai", name: "[Free] OpenAI (Fallback)", object: "model", variants: {} });
79
+ modelsOutput.push({ id: "free/gemini", name: "[Free] Gemini Flash (Fallback)", object: "model", variants: {} });
80
+ }
81
+ // 1.5 FORCE ENSURE CRITICAL MODELS
82
+ // Sometimes the API list changes or is cached weirdly. We force vital models.
83
+ const hasGemini = modelsOutput.find(m => m.id === 'free/gemini');
84
+ if (!hasGemini) {
85
+ log(`[ConfigGen] Force-injecting free/gemini.`);
86
+ modelsOutput.push({ id: "free/gemini", name: "[Free] Gemini Flash (Force)", object: "model", variants: {} });
87
+ }
88
+ // ALIAS Removed for Clean Config
89
+ // const hasGeminiAlias = modelsOutput.find(m => m.id === 'pollinations/free/gemini');
90
+ // if (!hasGeminiAlias) {
91
+ // modelsOutput.push({ id: "pollinations/free/gemini", name: "[Free] Gemini Flash (Alias)", object: "model", variants: {} });
92
+ // }
93
+ // 2. ENTERPRISE UNIVERSE
94
+ if (effectiveKey && effectiveKey.length > 5 && effectiveKey !== 'dummy') {
95
+ try {
96
+ // Use /text/models for full metadata (input_modalities, tools, reasoning, pricing)
97
+ const enterListRaw = await fetchJson('https://gen.pollinations.ai/text/models', {
98
+ 'Authorization': `Bearer ${effectiveKey}`
99
+ });
100
+ const enterList = Array.isArray(enterListRaw) ? enterListRaw : (enterListRaw.data || []);
101
+ const paidModels = [];
102
+ enterList.forEach((m) => {
103
+ if (m.tools === false)
104
+ return;
105
+ const mapped = mapModel(m, 'enter/', '[Enter] ');
106
+ modelsOutput.push(mapped);
107
+ if (m.paid_only) {
108
+ paidModels.push(mapped.id.replace('enter/', '')); // Store bare ID "gemini-large"
109
+ }
110
+ });
111
+ log(`Total models (Free+Pro): ${modelsOutput.length}`);
112
+ // Save Paid Models List for Proxy
113
+ try {
114
+ const paidListPath = path.join(config.gui ? path.dirname(CONFIG_FILE) : '/tmp', 'pollinations-paid-models.json');
115
+ // Ensure dir exists (re-use config dir logic from config.ts if possible, or just assume it exists since config loaded)
116
+ if (fs.existsSync(path.dirname(paidListPath))) {
117
+ fs.writeFileSync(paidListPath, JSON.stringify(paidModels));
118
+ }
119
+ }
120
+ catch (e) {
121
+ log(`Error saving paid models list: ${e}`);
122
+ }
123
+ }
124
+ catch (e) {
125
+ log(`Error fetching Enterprise models: ${e}`);
126
+ // STRICT MODE (Validation): Do not return fake fallback models.
127
+ if (forceStrict)
128
+ throw e;
129
+ // Fallback Robust for Enterprise (User has Key but discovery failed)
130
+ modelsOutput.push({ id: "enter/gpt-4o", name: "[Enter] GPT-4o (Fallback)", object: "model", variants: {} });
131
+ // ...
132
+ modelsOutput.push({ id: "enter/claude-3-5-sonnet", name: "[Enter] Claude 3.5 Sonnet (Fallback)", object: "model", variants: {} });
133
+ modelsOutput.push({ id: "enter/deepseek-reasoner", name: "[Enter] DeepSeek R1 (Fallback)", object: "model", variants: {} });
134
+ }
94
135
  }
95
136
  return modelsOutput;
96
137
  }
97
- // --- UTILS ---
138
+ // --- CAPABILITY ICONS ---
98
139
  function getCapabilityIcons(raw) {
99
140
  const icons = [];
100
- // Vision: check both input_modalities and legacy vision flag
101
- if (raw.input_modalities?.includes('image') || raw.vision === true) {
141
+ // Vision: accepts images
142
+ if (raw.input_modalities?.includes('image'))
102
143
  icons.push('👁️');
103
- }
104
- // Audio input
105
- if (raw.input_modalities?.includes('audio') || raw.audio === true) {
144
+ // Audio Input
145
+ if (raw.input_modalities?.includes('audio'))
106
146
  icons.push('🎙️');
107
- }
108
- // Audio output
109
- if (raw.output_modalities?.includes('audio')) {
147
+ // Audio Output
148
+ if (raw.output_modalities?.includes('audio'))
110
149
  icons.push('🔊');
111
- }
112
- // Reasoning
113
- if (raw.reasoning === true) {
150
+ // Reasoning capability
151
+ if (raw.reasoning === true)
114
152
  icons.push('🧠');
115
- }
116
- // Search capability
117
- if (raw.description?.toLowerCase().includes('search') || raw.name?.includes('search')) {
153
+ // Web Search (from description)
154
+ if (raw.description?.toLowerCase().includes('search') ||
155
+ raw.name?.includes('search') ||
156
+ raw.name?.includes('perplexity')) {
118
157
  icons.push('🔍');
119
158
  }
120
- // Tools
121
- if (raw.tools === true) {
122
- icons.push('🛠️');
123
- }
124
- // Paid only
125
- if (raw.paid_only === true) {
126
- icons.push('💎');
127
- }
159
+ // Tool/Function calling
160
+ if (raw.tools === true)
161
+ icons.push('💻');
128
162
  return icons.length > 0 ? ` ${icons.join('')}` : '';
129
163
  }
130
- function formatName(id) {
131
- let clean = id.replace(/-/g, ' ');
132
- clean = clean.replace(/\b\w/g, l => l.toUpperCase());
133
- return clean;
134
- }
135
- function mapModel(raw) {
136
- const rawId = raw.name;
137
- // Build display name
164
+ // --- MAPPING ENGINE ---
165
+ function mapModel(raw, prefix, namePrefix) {
166
+ const rawId = raw.id || raw.name;
167
+ const fullId = prefix + rawId; // ex: "free/gemini" or "enter/nomnom" (prefix passed is "enter/")
138
168
  let baseName = raw.description;
139
169
  if (!baseName || baseName === rawId) {
140
- baseName = formatName(rawId);
170
+ baseName = formatName(rawId, raw.censored !== false);
141
171
  }
142
- // Truncate after first " - "
172
+ // CLEANUP: Simple Truncation Rule (Requested by User)
173
+ // "Start from left, find ' - ', delete everything after."
143
174
  if (baseName && baseName.includes(' - ')) {
144
175
  baseName = baseName.split(' - ')[0].trim();
145
176
  }
177
+ let namePrefixFinal = namePrefix;
178
+ if (raw.paid_only) {
179
+ namePrefixFinal = namePrefix.replace('[Enter]', '[💎 Paid]');
180
+ }
181
+ // Get capability icons from API metadata
146
182
  const capabilityIcons = getCapabilityIcons(raw);
147
- const finalName = `${baseName}${capabilityIcons}`;
148
- // Determine modalities for OpenCode
149
- const inputMods = raw.input_modalities || ['text'];
150
- const outputMods = raw.output_modalities || ['text'];
183
+ const finalName = `${namePrefixFinal}${baseName}${capabilityIcons}`;
151
184
  const modelObj = {
152
- id: rawId, // No prefix! Direct model ID
185
+ id: fullId,
153
186
  name: finalName,
187
+ object: 'model',
188
+ variants: {},
189
+ // Declare modalities for OpenCode vision support
154
190
  modalities: {
155
- input: inputMods,
156
- output: outputMods
157
- },
158
- tool_call: raw.tools === true && rawId !== 'nomnom' // NomNom: no tools
191
+ input: raw.input_modalities || ['text'],
192
+ output: raw.output_modalities || ['text']
193
+ }
159
194
  };
160
- // Model-specific limits
195
+ // --- ENRICHISSEMENT ---
196
+ if (raw.reasoning === true || rawId.includes('thinking') || rawId.includes('reasoning')) {
197
+ modelObj.variants = { ...modelObj.variants, high_reasoning: { options: { reasoningEffort: "high", budgetTokens: 16000 } } };
198
+ }
199
+ if (rawId.includes('gemini') && !rawId.includes('fast')) {
200
+ if (!modelObj.variants.high_reasoning && (rawId === 'gemini' || rawId === 'gemini-large')) {
201
+ modelObj.variants.high_reasoning = { options: { reasoningEffort: "high", budgetTokens: 16000 } };
202
+ }
203
+ }
204
+ if (rawId.includes('claude') || rawId.includes('mistral') || rawId.includes('llama')) {
205
+ modelObj.variants.safe_tokens = { options: { maxTokens: 8000 } };
206
+ }
207
+ // NOVA FIX: Bedrock limit ~10k (User reported error > 10000)
208
+ // We MUST set the limit on the model object itself so OpenCode respects it by default.
161
209
  if (rawId.includes('nova')) {
162
- modelObj.limit = { output: 8000, context: 128000 };
210
+ modelObj.limit = {
211
+ output: 8000,
212
+ context: 128000 // Nova Micro/Lite/Pro usually 128k
213
+ };
214
+ // Also keep variant just in case
215
+ modelObj.variants.bedrock_safe = { options: { maxTokens: 8000 } };
216
+ }
217
+ // BEDROCK/ENTERPRISE LIMITS (Chickytutor only)
218
+ if (rawId.includes('chickytutor')) {
219
+ modelObj.limit = {
220
+ output: 8192,
221
+ context: 128000
222
+ };
163
223
  }
164
- if (rawId === 'nomnom') {
165
- modelObj.limit = { output: 2048, context: 32768 };
166
- modelObj.tool_call = false; // NomNom is a router, no external tools
224
+ // NOMNOM FIX: User reported error if max_tokens is missing.
225
+ // Also it is a 'Gemini-scrape' model, so we treat it similar to Gemini but with strict limit.
226
+ if (rawId.includes('nomnom') || rawId.includes('scrape')) {
227
+ modelObj.limit = {
228
+ output: 2048, // User used 1500 successfully
229
+ context: 32768
230
+ };
167
231
  }
168
- if (rawId.includes('chicky') || rawId.includes('mistral')) {
169
- modelObj.limit = { output: 4096, context: 8192 };
170
- modelObj.options = { maxTokens: 4096 };
232
+ if (rawId.includes('fast') || rawId.includes('flash') || rawId.includes('lite')) {
233
+ if (!rawId.includes('gemini')) {
234
+ modelObj.variants.speed = { options: { thinking: { disabled: true } } };
235
+ }
171
236
  }
172
- log(`[Mapped] ${modelObj.id} → ${modelObj.name} | tools=${modelObj.tool_call} | modalities=${JSON.stringify(modelObj.modalities)}`);
173
237
  return modelObj;
174
238
  }
@@ -78,6 +78,23 @@ function dereferenceSchema(schema, rootDefs) {
78
78
  schema.description = (schema.description || "") + " [Ref Failed]";
79
79
  }
80
80
  }
81
+ // VERTEX FIX: 'const' not supported -> convert to 'enum'
82
+ if (schema.const !== undefined) {
83
+ schema.enum = [schema.const];
84
+ delete schema.const;
85
+ }
86
+ // VERTEX FIX: 'anyOf' must be exclusive (no other siblings)
87
+ if (schema.anyOf || schema.oneOf) {
88
+ // Vertex demands strict exclusivity.
89
+ // We keep 'definitions'/'$defs' if present at root (though unlikely here)
90
+ // But for a property node, we must strip EVERYTHING else.
91
+ const keys = Object.keys(schema);
92
+ keys.forEach(k => {
93
+ if (k !== 'anyOf' && k !== 'oneOf' && k !== 'definitions' && k !== '$defs') {
94
+ delete schema[k];
95
+ }
96
+ });
97
+ }
81
98
  if (schema.properties) {
82
99
  for (const key in schema.properties) {
83
100
  schema.properties[key] = dereferenceSchema(schema.properties[key], rootDefs);
@@ -86,6 +103,15 @@ function dereferenceSchema(schema, rootDefs) {
86
103
  if (schema.items) {
87
104
  schema.items = dereferenceSchema(schema.items, rootDefs);
88
105
  }
106
+ if (schema.anyOf) {
107
+ schema.anyOf = schema.anyOf.map((s) => dereferenceSchema(s, rootDefs));
108
+ }
109
+ if (schema.oneOf) {
110
+ schema.oneOf = schema.oneOf.map((s) => dereferenceSchema(s, rootDefs));
111
+ }
112
+ if (schema.allOf) {
113
+ schema.allOf = schema.allOf.map((s) => dereferenceSchema(s, rootDefs));
114
+ }
89
115
  if (schema.optional !== undefined)
90
116
  delete schema.optional;
91
117
  if (schema.title)
@@ -107,6 +133,38 @@ function sanitizeToolsForVertex(tools) {
107
133
  return tool;
108
134
  });
109
135
  }
136
+ function sanitizeToolsForBedrock(tools) {
137
+ return tools.map(tool => {
138
+ if (tool.function) {
139
+ if (!tool.function.description || tool.function.description.length === 0) {
140
+ tool.function.description = " "; // Force non-empty string
141
+ }
142
+ }
143
+ return tool;
144
+ });
145
+ }
146
+ function sanitizeSchemaForKimi(schema) {
147
+ if (!schema || typeof schema !== 'object')
148
+ return schema;
149
+ // Kimi Fixes
150
+ if (schema.title)
151
+ delete schema.title;
152
+ // Fix empty objects "{}" which Kimi hates.
153
+ // If it's an empty object without type, assume string or object?
154
+ // Often happens with "additionalProperties: {}"
155
+ if (Object.keys(schema).length === 0) {
156
+ schema.type = "string"; // Fallback to safe type
157
+ schema.description = "Any value";
158
+ }
159
+ if (schema.properties) {
160
+ for (const key in schema.properties) {
161
+ schema.properties[key] = sanitizeSchemaForKimi(schema.properties[key]);
162
+ }
163
+ }
164
+ if (schema.items)
165
+ sanitizeSchemaForKimi(schema.items);
166
+ return schema;
167
+ }
110
168
  function truncateTools(tools, limit = 120) {
111
169
  if (!tools || tools.length <= limit)
112
170
  return tools;
@@ -114,12 +172,16 @@ function truncateTools(tools, limit = 120) {
114
172
  }
115
173
  const MAX_RETRIES = 3;
116
174
  const RETRY_DELAY_MS = 1000;
175
+ const FETCH_TIMEOUT_MS = 600000; // 10 Minutes global timeout
117
176
  function sleep(ms) {
118
177
  return new Promise(resolve => setTimeout(resolve, ms));
119
178
  }
120
179
  async function fetchWithRetry(url, options, retries = MAX_RETRIES) {
121
180
  try {
122
- const response = await fetch(url, options);
181
+ const controller = new AbortController();
182
+ const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
183
+ const response = await fetch(url, { ...options, signal: controller.signal });
184
+ clearTimeout(timeoutId);
123
185
  if (response.ok)
124
186
  return response;
125
187
  if (response.status === 404 || response.status === 401 || response.status === 400) {
@@ -153,11 +215,6 @@ export async function handleChatCompletion(req, res, bodyRaw) {
153
215
  const config = loadConfig();
154
216
  // DEBUG: Trace Config State for Hot Reload verification
155
217
  log(`[Proxy Request] Config Loaded. Mode: ${config.mode}, HasKey: ${!!config.apiKey}, KeyLength: ${config.apiKey ? config.apiKey.length : 0}`);
156
- // SPY LOGGING
157
- try {
158
- fs.appendFileSync('/tmp/opencode_spy.log', `\n\n=== REQUEST ${new Date().toISOString()} ===\nMODEL: ${body.model}\nBODY:\n${JSON.stringify(body, null, 2)}\n==========================\n`);
159
- }
160
- catch (e) { }
161
218
  // 0. COMMAND HANDLING
162
219
  if (body.messages && body.messages.length > 0) {
163
220
  const lastMsg = body.messages[body.messages.length - 1];
@@ -216,16 +273,15 @@ export async function handleChatCompletion(req, res, bodyRaw) {
216
273
  // LOAD QUOTA FOR SAFETY CHECKS
217
274
  const { getQuotaStatus, formatQuotaForToast } = await import('./quota.js');
218
275
  const quota = await getQuotaStatus(false);
219
- // A. Resolve Base Target - v6.0: All models go to gen.pollinations.ai
220
- // Strip any legacy prefixes for backwards compatibility
221
- if (actualModel.startsWith('enter/') || actualModel.startsWith('enter-')) {
222
- actualModel = actualModel.replace(/^enter[-/]/, '');
276
+ // A. Resolve Base Target
277
+ if (actualModel.startsWith('enter/')) {
278
+ isEnterprise = true;
279
+ actualModel = actualModel.replace('enter/', '');
223
280
  }
224
- else if (actualModel.startsWith('free/') || actualModel.startsWith('free-')) {
225
- actualModel = actualModel.replace(/^free[-/]/, '');
281
+ else if (actualModel.startsWith('free/')) {
282
+ isEnterprise = false;
283
+ actualModel = actualModel.replace('free/', '');
226
284
  }
227
- // v6.0: Everything is enterprise now (requires API key)
228
- isEnterprise = true;
229
285
  // A.1 PAID MODEL ENFORCEMENT (V5.5 Strategy)
230
286
  // Check dynamic list saved by generate-config.ts
231
287
  if (isEnterprise) {
@@ -337,16 +393,24 @@ export async function handleChatCompletion(req, res, bodyRaw) {
337
393
  }
338
394
  }
339
395
  }
340
- // C. Construct URL & Headers - v6.0: Always gen.pollinations.ai
341
- if (!config.apiKey) {
342
- emitLogToast('error', "Missing API Key - Use /pollinations connect <key>", 'Proxy Error');
343
- res.writeHead(401, { 'Content-Type': 'application/json' });
344
- res.end(JSON.stringify({ error: { message: "API Key required. Use /pollinations connect <your_key> to connect." } }));
345
- return;
396
+ // C. Construct URL & Headers
397
+ if (isEnterprise) {
398
+ if (!config.apiKey) {
399
+ emitLogToast('error', "Missing API Key for Enterprise Model", 'Proxy Error');
400
+ res.writeHead(401, { 'Content-Type': 'application/json' });
401
+ res.end(JSON.stringify({ error: { message: "API Key required for Enterprise models." } }));
402
+ return;
403
+ }
404
+ targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
405
+ authHeader = `Bearer ${config.apiKey}`;
406
+ log(`Routing to ENTERPRISE: ${actualModel}`);
407
+ }
408
+ else {
409
+ targetUrl = 'https://text.pollinations.ai/openai/chat/completions';
410
+ authHeader = undefined;
411
+ log(`Routing to FREE: ${actualModel} ${isFallbackActive ? '(FALLBACK)' : ''}`);
412
+ // emitLogToast('info', `Routing to: FREE UNIVERSE (${actualModel})`, 'Pollinations Routing'); // Too noisy
346
413
  }
347
- targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
348
- authHeader = `Bearer ${config.apiKey}`;
349
- log(`Routing to gen.pollinations.ai: ${actualModel}`);
350
414
  // NOTIFY SWITCH
351
415
  if (isFallbackActive) {
352
416
  emitStatusToast('warning', `⚠️ Safety Net: ${actualModel} (${fallbackReason})`, 'Pollinations Safety');
@@ -378,17 +442,24 @@ export async function handleChatCompletion(req, res, bodyRaw) {
378
442
  // LOGIC BLOCK: MODEL SPECIFIC ADAPTATIONS
379
443
  // =========================================================
380
444
  if (proxyBody.tools && Array.isArray(proxyBody.tools) && proxyBody.tools.length > 0) {
381
- // B0. KIMI / MOONSHOT SURGICAL FIX (Restored for Debug)
382
- // Tools are ENABLED. We rely on penalties and strict stops to fight loops.
445
+ // B0. KIMI / MOONSHOT SURGICAL FIX
383
446
  if (actualModel.includes("kimi") || actualModel.includes("moonshot")) {
384
- log(`[Proxy] Kimi: Tools ENABLED. Applying penalties/stops.`);
447
+ log(`[Proxy] Kimi: Tools ENABLED. Applying penalties/stops/sanitization.`);
385
448
  proxyBody.frequency_penalty = 1.1;
386
449
  proxyBody.presence_penalty = 0.4;
387
450
  proxyBody.stop = ["<|endoftext|>", "User:", "\nUser", "User :"];
451
+ // KIMI FIX: Remove 'title' from schema
452
+ proxyBody.tools = proxyBody.tools.map((t) => {
453
+ if (t.function && t.function.parameters) {
454
+ t.function.parameters = sanitizeSchemaForKimi(t.function.parameters);
455
+ }
456
+ return t;
457
+ });
388
458
  }
389
- // A. AZURE/OPENAI FIXES
390
- if (actualModel.includes("gpt") || actualModel.includes("openai") || actualModel.includes("azure")) {
391
- proxyBody.tools = truncateTools(proxyBody.tools, 120);
459
+ // A. AZURE/OPENAI FIXES + MIDJOURNEY + GROK
460
+ if (actualModel.includes("gpt") || actualModel.includes("openai") || actualModel.includes("azure") || actualModel.includes("midijourney") || actualModel.includes("grok")) {
461
+ const limit = (actualModel.includes("midijourney") || actualModel.includes("grok")) ? 128 : 120;
462
+ proxyBody.tools = truncateTools(proxyBody.tools, limit);
392
463
  if (proxyBody.messages) {
393
464
  proxyBody.messages.forEach((m) => {
394
465
  if (m.tool_calls) {
@@ -403,6 +474,11 @@ export async function handleChatCompletion(req, res, bodyRaw) {
403
474
  });
404
475
  }
405
476
  }
477
+ // BEDROCK FIX (Claude / Nova / ChickyTutor)
478
+ if (actualModel.includes("claude") || actualModel.includes("nova") || actualModel.includes("bedrock") || actualModel.includes("chickytutor")) {
479
+ log(`[Proxy] Bedrock: Sanitizing tools description.`);
480
+ proxyBody.tools = sanitizeToolsForBedrock(proxyBody.tools);
481
+ }
406
482
  // B1. NOMNOM SPECIAL (Disable Grounding, KEEP Search Tool)
407
483
  if (actualModel === "nomnom") {
408
484
  proxyBody.tools_config = { google_search_retrieval: { disable: true } };
@@ -410,36 +486,12 @@ export async function handleChatCompletion(req, res, bodyRaw) {
410
486
  proxyBody.tools = sanitizeToolsForVertex(proxyBody.tools || []);
411
487
  log(`[Proxy] Nomnom Fix: Grounding Disabled, Search Tool KEPT.`);
412
488
  }
413
- // B2. GEMINI FREE / FAST (CRASH FIX: STRICT SANITIZATION)
414
- // Restore Tools but REMOVE conflicting ones (Search)
415
489
  // B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
416
- // Handles: "tools" vs "grounding" conflicts, and "infinite loops" via Stop Sequences.
417
- // GLOBAL BEDROCK FIX (All Models)
418
- // Check if history has tools but current request misses tools definition.
419
- // This happens when OpenCode sends the Tool Result (optimisation),
420
- // but Bedrock requires toolConfig to validate the history.
421
- const hasToolHistory = proxyBody.messages?.some((m) => m.role === 'tool' || m.tool_calls);
422
- if (hasToolHistory && (!proxyBody.tools || proxyBody.tools.length === 0)) {
423
- // Inject Shim Tool to satisfy Bedrock
424
- proxyBody.tools = [{
425
- type: 'function',
426
- function: {
427
- name: '_bedrock_compatibility_shim',
428
- description: 'Internal system tool to satisfy Bedrock strict toolConfig requirement. Do not use.',
429
- parameters: { type: 'object', properties: {} }
430
- }
431
- }];
432
- log(`[Proxy] Bedrock Fix: Injected shim tool for ${actualModel} (History has tools, Request missing tools)`);
433
- }
434
- // B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
435
- // Fixes "Multiple tools" error (Vertex) and "JSON body validation failed" (v5.3.5 regression)
436
- // Added ChickyTutor (Claude/Gemini based) to fix "toolConfig must be defined" error.
437
- else if (actualModel.includes("gemini") || actualModel.includes("chickytutor")) {
490
+ else if (actualModel.includes("gemini")) {
438
491
  let hasFunctions = false;
439
492
  if (proxyBody.tools && Array.isArray(proxyBody.tools)) {
440
493
  hasFunctions = proxyBody.tools.some((t) => t.type === 'function' || t.function);
441
494
  }
442
- // Old Shim logic removed (moved up)
443
495
  if (hasFunctions) {
444
496
  // 1. Strict cleanup of 'google_search' tool
445
497
  proxyBody.tools = proxyBody.tools.filter((t) => {
@@ -470,14 +522,6 @@ export async function handleChatCompletion(req, res, bodyRaw) {
470
522
  log(`[Proxy] Gemini Logic: Tools=${proxyBody.tools ? proxyBody.tools.length : 'REMOVED'}, Stops NOT Injected.`);
471
523
  }
472
524
  }
473
- // B5. BEDROCK TOKEN LIMIT FIX
474
- if (actualModel.includes("chicky") || actualModel.includes("mistral")) {
475
- // Force max_tokens if not present or too high (Bedrock outputs usually max 4k, context 8k+ but strict check)
476
- if (!proxyBody.max_tokens || proxyBody.max_tokens > 4096) {
477
- proxyBody.max_tokens = 4096;
478
- log(`[Proxy] Enforcing max_tokens=4096 for ${actualModel} (Bedrock Limit)`);
479
- }
480
- }
481
525
  // C. GEMINI ID BACKTRACKING & SIGNATURE
482
526
  if ((actualModel.includes("gemini") || actualModel === "nomnom") && proxyBody.messages) {
483
527
  const lastMsg = proxyBody.messages[proxyBody.messages.length - 1];
@@ -586,10 +630,10 @@ export async function handleChatCompletion(req, res, bodyRaw) {
586
630
  // 2. Notify
587
631
  emitStatusToast('warning', `⚠️ Safety Net: ${actualModel} (${fallbackReason})`, 'Pollinations Safety');
588
632
  emitLogToast('warning', `Recovering from ${fetchRes.status} -> Switching to ${actualModel}`, 'Safety Net');
589
- // 3. Re-Prepare Request - v6.0: Stay on gen.pollinations.ai
590
- targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
633
+ // 3. Re-Prepare Request
634
+ targetUrl = 'https://text.pollinations.ai/openai/chat/completions';
591
635
  const retryHeaders = { ...headers };
592
- // Keep Authorization for gen.pollinations.ai
636
+ delete retryHeaders['Authorization']; // Free = No Auth
593
637
  const retryBody = { ...proxyBody, model: actualModel };
594
638
  // 4. Retry Fetch
595
639
  const retryRes = await fetchWithRetry(targetUrl, {
@@ -0,0 +1,2 @@
1
+ import { type ToolDefinition } from '@opencode-ai/plugin/tool';
2
+ export declare const genDiagramTool: ToolDefinition;