opencode-pollinations-plugin 5.8.4-beta.9 → 5.9.1

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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # 🌸 Pollinations AI Plugin for OpenCode (v5.6.0)
1
+ # 🌸 Pollinations AI Plugin for OpenCode (v5.9.0)
2
2
 
3
3
  <div align="center">
4
4
  <img src="https://avatars.githubusercontent.com/u/88394740?s=400&v=4" alt="Pollinations.ai Logo" width="200">
@@ -134,8 +134,9 @@ OpenCode uses NPM as its registry. To publish:
134
134
 
135
135
  ### 1. The Basics (Free Mode)
136
136
  Just type in the chat. You are in **Manual Mode** by default.
137
- - Model: `openai` (GPT-4o Mini equivalent)
138
- - Model: `mistral` (Mistral Nemo)
137
+ - Model: `openai-fast` (GPT-OSS 20b)
138
+ - Model: `mistral` (Mistral Small 3.1)
139
+ - ...
139
140
 
140
141
  ### 🔑 Configuration (API Key)
141
142
 
@@ -155,6 +156,7 @@ Just type in the chat. You are in **Manual Mode** by default.
155
156
 
156
157
  ## 🔗 Links
157
158
 
159
+ - **Sign up Pollinations Beta (more and best free tiers access and paids models)**: [pollinations.ai](https://enter.pollinations.ai)
158
160
  - **Pollinations Website**: [pollinations.ai](https://pollinations.ai)
159
161
  - **Discord Community**: [Join us!](https://discord.gg/pollinations-ai-885844321461485618)
160
162
  - **OpenCode Ecosystem**: [opencode.ai](https://opencode.ai/docs/ecosystem#plugins)
package/dist/index.js CHANGED
@@ -100,13 +100,11 @@ export const PollinationsPlugin = async (ctx) => {
100
100
  const version = require('../package.json').version;
101
101
  config.provider['pollinations'] = {
102
102
  id: 'pollinations',
103
- npm: require('../package.json').name,
104
103
  name: `Pollinations AI (v${version})`,
105
104
  options: { baseURL: localBaseUrl },
106
105
  models: modelsObj
107
106
  };
108
107
  log(`[Hook] Registered ${Object.keys(modelsObj).length} models.`);
109
- log(`[Hook] Keys: ${Object.keys(modelsObj).join(', ')}`);
110
108
  },
111
109
  ...toastHooks,
112
110
  ...createStatusHooks(ctx.client),
@@ -1,6 +1,8 @@
1
1
  interface OpenCodeModel {
2
2
  id: string;
3
3
  name: string;
4
+ object: string;
5
+ variants?: any;
4
6
  options?: any;
5
7
  limit?: {
6
8
  context?: number;
@@ -10,7 +12,6 @@ interface OpenCodeModel {
10
12
  input?: string[];
11
13
  output?: string[];
12
14
  };
13
- tool_call?: boolean;
14
15
  }
15
16
  export declare function generatePollinationsConfig(forceApiKey?: string, forceStrict?: boolean): Promise<OpenCodeModel[]>;
16
17
  export {};
@@ -5,10 +5,7 @@ import * as path from 'path';
5
5
  import { loadConfig } from './config.js';
6
6
  const HOMEDIR = os.homedir();
7
7
  const CONFIG_DIR_POLLI = path.join(HOMEDIR, '.pollinations');
8
- const CACHE_FILE = path.join(CONFIG_DIR_POLLI, 'models-cache.json');
9
- // --- CONSTANTS ---
10
- // Seed from models-seed.ts
11
- import { FREE_MODELS_SEED } from './models-seed.js';
8
+ const CONFIG_FILE = path.join(CONFIG_DIR_POLLI, 'config.json');
12
9
  // --- LOGGING ---
13
10
  const LOG_FILE = '/tmp/opencode_pollinations_config.log';
14
11
  function log(msg) {
@@ -19,41 +16,22 @@ function log(msg) {
19
16
  fs.appendFileSync(LOG_FILE, `[ConfigGen] ${ts} ${msg}\n`);
20
17
  }
21
18
  catch (e) { }
19
+ // Force output to stderr for CLI visibility if needed, but clean.
22
20
  }
23
- // --- NETWORK HELPER ---
24
- function fetchHead(url) {
25
- return new Promise((resolve) => {
26
- // Use Node.js native https check for minimal overhead
27
- const req = https.request(url, { method: 'HEAD', timeout: 5000 }, (res) => {
28
- resolve(res.headers['etag'] || null);
29
- });
30
- req.on('error', () => resolve(null));
31
- req.on('timeout', () => { req.destroy(); resolve(null); });
32
- req.end();
33
- });
34
- }
21
+ // Fetch Helper
35
22
  function fetchJson(url, headers = {}) {
36
23
  return new Promise((resolve, reject) => {
37
- const finalHeaders = {
38
- ...headers,
39
- 'User-Agent': 'Mozilla/5.0 (compatible; OpenCode/5.8.4; +https://opencode.ai)'
40
- };
41
- const req = https.get(url, { headers: finalHeaders }, (res) => {
42
- const etag = res.headers['etag'];
24
+ const req = https.get(url, { headers }, (res) => {
43
25
  let data = '';
44
26
  res.on('data', chunk => data += chunk);
45
27
  res.on('end', () => {
46
28
  try {
47
29
  const json = JSON.parse(data);
48
- // HACK: Attach ETag to the object to pass it up
49
- if (etag && typeof json === 'object') {
50
- Object.defineProperty(json, '_etag', { value: etag, enumerable: false, writable: true });
51
- }
52
30
  resolve(json);
53
31
  }
54
32
  catch (e) {
55
33
  log(`JSON Parse Error for ${url}: ${e}`);
56
- resolve([]); // Fail safe
34
+ resolve([]); // Fail safe -> empty list
57
35
  }
58
36
  });
59
37
  });
@@ -61,167 +39,138 @@ function fetchJson(url, headers = {}) {
61
39
  log(`Network Error for ${url}: ${e.message}`);
62
40
  reject(e);
63
41
  });
64
- req.setTimeout(15000, () => {
42
+ req.setTimeout(5000, () => {
65
43
  req.destroy();
66
44
  reject(new Error('Timeout'));
67
45
  });
68
46
  });
69
47
  }
70
- function loadCache() {
71
- try {
72
- if (fs.existsSync(CACHE_FILE)) {
73
- const content = fs.readFileSync(CACHE_FILE, 'utf-8');
74
- return JSON.parse(content);
75
- }
76
- }
77
- catch (e) {
78
- log(`Error loading cache: ${e}`);
79
- }
80
- return null;
81
- }
82
- function saveCache(models, etag) {
83
- try {
84
- const data = {
85
- timestamp: Date.now(),
86
- etag: etag,
87
- models: models
88
- };
89
- if (!fs.existsSync(CONFIG_DIR_POLLI))
90
- fs.mkdirSync(CONFIG_DIR_POLLI, { recursive: true });
91
- fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2));
92
- }
93
- catch (e) {
94
- log(`Error saving cache: ${e}`);
95
- }
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;
96
54
  }
97
- // --- GENERATOR LOGIC ---
55
+ // --- MAIN GENERATOR logic ---
56
+ // --- MAIN GENERATOR logic ---
98
57
  export async function generatePollinationsConfig(forceApiKey, forceStrict = false) {
99
58
  const config = loadConfig();
100
59
  const modelsOutput = [];
101
- log(`Starting Configuration (v5.8.4-Debug-Tools)...`);
60
+ log(`Starting Configuration (V5.1.22 Hot-Reload)...`);
61
+ // Use forced key (from Hook) or cached key
102
62
  const effectiveKey = forceApiKey || config.apiKey;
103
- // 1. FREE UNIVERSE (Smart Cache System)
104
- let freeModelsList = [];
105
- let isOffline = false;
106
- let cache = loadCache();
107
- const CACHE_TTL = 7 * 24 * 3600 * 1000; // 7 days
108
- // Decision Logic
109
- const now = Date.now();
110
- let shouldFetch = !cache || (now - cache.timestamp > CACHE_TTL);
111
- // ETag Check: If cache is valid but we want to be proactive
112
- if (!shouldFetch && cache && cache.etag) {
113
- try {
114
- log('Smart Refresh: Checking for updates (HEAD)...');
115
- const remoteEtag = await fetchHead('https://text.pollinations.ai/models');
116
- if (remoteEtag && remoteEtag !== cache.etag) {
117
- log(`Update Detected! (Remote: ${remoteEtag} != Local: ${cache.etag}). Forcing refresh.`);
118
- shouldFetch = true;
119
- }
120
- else {
121
- log('Cache is clean (ETag match). No refresh needed.');
122
- }
123
- }
124
- catch (e) {
125
- log(`Smart Refresh check failed: ${e}. Ignoring.`);
126
- }
127
- }
128
- if (shouldFetch) {
129
- log('Fetching fresh Free models...');
130
- try {
131
- const raw = await fetchJson('https://text.pollinations.ai/models');
132
- const list = Array.isArray(raw) ? raw : (raw.data || []);
133
- const newEtag = raw._etag; // Get hidden ETag
134
- if (list.length > 0) {
135
- freeModelsList = list;
136
- saveCache(list, newEtag);
137
- log(`Fetched and cached ${list.length} models (ETag: ${newEtag || 'N/A'}).`);
138
- }
139
- else {
140
- throw new Error('API returned empty list');
141
- }
142
- }
143
- catch (e) {
144
- log(`Fetch failed: ${e}.`);
145
- isOffline = true;
146
- // Fallback Logic
147
- if (cache && cache.models.length > 0) {
148
- log('Using cached models (Offline).');
149
- freeModelsList = cache.models;
150
- }
151
- else {
152
- log('Using DEFAULT SEED models (Offline + No Cache).');
153
- freeModelsList = FREE_MODELS_SEED;
154
- }
155
- }
156
- }
157
- else {
158
- log('Using cached models (Skipped fetch).');
159
- freeModelsList = cache.models;
63
+ // 1. FREE UNIVERSE
64
+ try {
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] ');
70
+ modelsOutput.push(mapped);
71
+ });
72
+ log(`Fetched ${modelsOutput.length} Free models.`);
160
73
  }
161
- // Map Free Models
162
- freeModelsList.forEach((m) => {
163
- // Tag (Offline) only if we explicitly failed a fetch attempt or are using Fallback SEED when fetch failed.
164
- // If we use cache because it's valid (Skipped fetch), we don't tag (Offline).
165
- const suffix = isOffline ? ' (Offline)' : '';
166
- const mapped = mapModel(m, 'free-', `[Free] `, suffix);
167
- modelsOutput.push(mapped);
168
- });
74
+ catch (e) {
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
+ // }
169
93
  // 2. ENTERPRISE UNIVERSE
170
94
  if (effectiveKey && effectiveKey.length > 5 && effectiveKey !== 'dummy') {
171
95
  try {
96
+ // Use /text/models for full metadata (input_modalities, tools, reasoning, pricing)
172
97
  const enterListRaw = await fetchJson('https://gen.pollinations.ai/text/models', {
173
98
  'Authorization': `Bearer ${effectiveKey}`
174
99
  });
175
100
  const enterList = Array.isArray(enterListRaw) ? enterListRaw : (enterListRaw.data || []);
101
+ const paidModels = [];
176
102
  enterList.forEach((m) => {
177
103
  if (m.tools === false)
178
104
  return;
179
- const mapped = mapModel(m, 'enter-', '[Enter] ');
105
+ const mapped = mapModel(m, 'enter/', '[Enter] ');
180
106
  modelsOutput.push(mapped);
107
+ if (m.paid_only) {
108
+ paidModels.push(mapped.id.replace('enter/', '')); // Store bare ID "gemini-large"
109
+ }
181
110
  });
182
111
  log(`Total models (Free+Pro): ${modelsOutput.length}`);
183
- log(`Generated IDs: ${modelsOutput.map(m => m.id).join(', ')}`);
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
+ }
184
123
  }
185
124
  catch (e) {
186
125
  log(`Error fetching Enterprise models: ${e}`);
126
+ // STRICT MODE (Validation): Do not return fake fallback models.
187
127
  if (forceStrict)
188
128
  throw e;
189
- // STRICT: No Fallback for Enterprise. If API is down, we have 0 Enter models.
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: {} });
190
134
  }
191
135
  }
192
136
  return modelsOutput;
193
137
  }
194
- // --- UTILS ---
138
+ // --- CAPABILITY ICONS ---
195
139
  function getCapabilityIcons(raw) {
196
140
  const icons = [];
197
- if (raw.input_modalities?.includes('image') || raw.vision === true)
141
+ // Vision: accepts images
142
+ if (raw.input_modalities?.includes('image'))
198
143
  icons.push('👁️');
199
- if (raw.input_modalities?.includes('audio') || raw.audio === true)
144
+ // Audio Input
145
+ if (raw.input_modalities?.includes('audio'))
200
146
  icons.push('🎙️');
147
+ // Audio Output
201
148
  if (raw.output_modalities?.includes('audio'))
202
149
  icons.push('🔊');
150
+ // Reasoning capability
203
151
  if (raw.reasoning === true)
204
152
  icons.push('🧠');
205
- 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')) {
206
157
  icons.push('🔍');
158
+ }
159
+ // Tool/Function calling
207
160
  if (raw.tools === true)
208
161
  icons.push('💻');
209
162
  return icons.length > 0 ? ` ${icons.join('')}` : '';
210
163
  }
211
- function formatName(id, censored = true) {
212
- let clean = id.replace(/^pollinations\//, '').replace(/-/g, ' ');
213
- clean = clean.replace(/\b\w/g, l => l.toUpperCase());
214
- if (!censored)
215
- clean += " (Uncensored)";
216
- return clean;
217
- }
218
- function mapModel(raw, prefix, namePrefix, nameSuffix = '') {
164
+ // --- MAPPING ENGINE ---
165
+ function mapModel(raw, prefix, namePrefix) {
219
166
  const rawId = raw.id || raw.name;
220
- const fullId = prefix + rawId;
167
+ const fullId = prefix + rawId; // ex: "free/gemini" or "enter/nomnom" (prefix passed is "enter/")
221
168
  let baseName = raw.description;
222
169
  if (!baseName || baseName === rawId) {
223
170
  baseName = formatName(rawId, raw.censored !== false);
224
171
  }
172
+ // CLEANUP: Simple Truncation Rule (Requested by User)
173
+ // "Start from left, find ' - ', delete everything after."
225
174
  if (baseName && baseName.includes(' - ')) {
226
175
  baseName = baseName.split(' - ')[0].trim();
227
176
  }
@@ -229,21 +178,21 @@ function mapModel(raw, prefix, namePrefix, nameSuffix = '') {
229
178
  if (raw.paid_only) {
230
179
  namePrefixFinal = namePrefix.replace('[Enter]', '[💎 Paid]');
231
180
  }
181
+ // Get capability icons from API metadata
232
182
  const capabilityIcons = getCapabilityIcons(raw);
233
- const finalName = `${namePrefixFinal}${baseName}${nameSuffix}${capabilityIcons}`;
183
+ const finalName = `${namePrefixFinal}${baseName}${capabilityIcons}`;
234
184
  const modelObj = {
235
185
  id: fullId,
236
186
  name: finalName,
237
- // object: 'model',
238
- // variants: {}, // POTENTIAL SCHEMA VIOLATION
187
+ object: 'model',
188
+ variants: {},
189
+ // Declare modalities for OpenCode vision support
239
190
  modalities: {
240
191
  input: raw.input_modalities || ['text'],
241
192
  output: raw.output_modalities || ['text']
242
- },
243
- tool_call: false // FORCE DEBUG DISABLE
193
+ }
244
194
  };
245
- // Enrichissements
246
- /*
195
+ // --- ENRICHISSEMENT ---
247
196
  if (raw.reasoning === true || rawId.includes('thinking') || rawId.includes('reasoning')) {
248
197
  modelObj.variants = { ...modelObj.variants, high_reasoning: { options: { reasoningEffort: "high", budgetTokens: 16000 } } };
249
198
  }
@@ -255,22 +204,35 @@ function mapModel(raw, prefix, namePrefix, nameSuffix = '') {
255
204
  if (rawId.includes('claude') || rawId.includes('mistral') || rawId.includes('llama')) {
256
205
  modelObj.variants.safe_tokens = { options: { maxTokens: 8000 } };
257
206
  }
258
- */
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.
259
209
  if (rawId.includes('nova')) {
260
- 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
+ };
261
223
  }
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.
262
226
  if (rawId.includes('nomnom') || rawId.includes('scrape')) {
263
- modelObj.limit = { output: 2048, context: 32768 };
264
- }
265
- if (rawId.includes('chicky')) {
266
- modelObj.limit = { output: 8192, context: 8192 };
227
+ modelObj.limit = {
228
+ output: 2048, // User used 1500 successfully
229
+ context: 32768
230
+ };
267
231
  }
268
- /*
269
- if (rawId.includes('fast') || rawId.includes('flash')) {
270
- if (!rawId.includes('gemini')) {
271
- modelObj.variants.speed = { options: { thinking: { disabled: true } } };
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
+ }
272
236
  }
273
- }
274
- */
275
237
  return modelObj;
276
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];
@@ -385,17 +442,24 @@ export async function handleChatCompletion(req, res, bodyRaw) {
385
442
  // LOGIC BLOCK: MODEL SPECIFIC ADAPTATIONS
386
443
  // =========================================================
387
444
  if (proxyBody.tools && Array.isArray(proxyBody.tools) && proxyBody.tools.length > 0) {
388
- // B0. KIMI / MOONSHOT SURGICAL FIX (Restored for Debug)
389
- // Tools are ENABLED. We rely on penalties and strict stops to fight loops.
445
+ // B0. KIMI / MOONSHOT SURGICAL FIX
390
446
  if (actualModel.includes("kimi") || actualModel.includes("moonshot")) {
391
- log(`[Proxy] Kimi: Tools ENABLED. Applying penalties/stops.`);
447
+ log(`[Proxy] Kimi: Tools ENABLED. Applying penalties/stops/sanitization.`);
392
448
  proxyBody.frequency_penalty = 1.1;
393
449
  proxyBody.presence_penalty = 0.4;
394
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
+ });
395
458
  }
396
- // A. AZURE/OPENAI FIXES
397
- if (actualModel.includes("gpt") || actualModel.includes("openai") || actualModel.includes("azure")) {
398
- 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);
399
463
  if (proxyBody.messages) {
400
464
  proxyBody.messages.forEach((m) => {
401
465
  if (m.tool_calls) {
@@ -410,6 +474,11 @@ export async function handleChatCompletion(req, res, bodyRaw) {
410
474
  });
411
475
  }
412
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
+ }
413
482
  // B1. NOMNOM SPECIAL (Disable Grounding, KEEP Search Tool)
414
483
  if (actualModel === "nomnom") {
415
484
  proxyBody.tools_config = { google_search_retrieval: { disable: true } };
@@ -417,36 +486,12 @@ export async function handleChatCompletion(req, res, bodyRaw) {
417
486
  proxyBody.tools = sanitizeToolsForVertex(proxyBody.tools || []);
418
487
  log(`[Proxy] Nomnom Fix: Grounding Disabled, Search Tool KEPT.`);
419
488
  }
420
- // B2. GEMINI FREE / FAST (CRASH FIX: STRICT SANITIZATION)
421
- // Restore Tools but REMOVE conflicting ones (Search)
422
- // B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
423
- // Handles: "tools" vs "grounding" conflicts, and "infinite loops" via Stop Sequences.
424
- // GLOBAL BEDROCK FIX (All Models)
425
- // Check if history has tools but current request misses tools definition.
426
- // This happens when OpenCode sends the Tool Result (optimisation),
427
- // but Bedrock requires toolConfig to validate the history.
428
- const hasToolHistory = proxyBody.messages?.some((m) => m.role === 'tool' || m.tool_calls);
429
- if (hasToolHistory && (!proxyBody.tools || proxyBody.tools.length === 0)) {
430
- // Inject Shim Tool to satisfy Bedrock
431
- proxyBody.tools = [{
432
- type: 'function',
433
- function: {
434
- name: '_bedrock_compatibility_shim',
435
- description: 'Internal system tool to satisfy Bedrock strict toolConfig requirement. Do not use.',
436
- parameters: { type: 'object', properties: {} }
437
- }
438
- }];
439
- log(`[Proxy] Bedrock Fix: Injected shim tool for ${actualModel} (History has tools, Request missing tools)`);
440
- }
441
489
  // B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
442
- // Fixes "Multiple tools" error (Vertex) and "JSON body validation failed" (v5.3.5 regression)
443
- // Added ChickyTutor (Claude/Gemini based) to fix "toolConfig must be defined" error.
444
- else if (actualModel.includes("gemini") || actualModel.includes("chickytutor")) {
490
+ else if (actualModel.includes("gemini")) {
445
491
  let hasFunctions = false;
446
492
  if (proxyBody.tools && Array.isArray(proxyBody.tools)) {
447
493
  hasFunctions = proxyBody.tools.some((t) => t.type === 'function' || t.function);
448
494
  }
449
- // Old Shim logic removed (moved up)
450
495
  if (hasFunctions) {
451
496
  // 1. Strict cleanup of 'google_search' tool
452
497
  proxyBody.tools = proxyBody.tools.filter((t) => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-pollinations-plugin",
3
- "displayName": "Pollinations AI (V5.6)",
4
- "version": "5.8.4-beta.9",
3
+ "displayName": "Pollinations AI (V5.9)",
4
+ "version": "5.9.1",
5
5
  "description": "Native Pollinations.ai Provider Plugin for OpenCode",
6
6
  "publisher": "pollinations",
7
7
  "repository": {
@@ -55,4 +55,4 @@
55
55
  "@types/node": "^20.0.0",
56
56
  "typescript": "^5.0.0"
57
57
  }
58
- }
58
+ }
@@ -1,18 +0,0 @@
1
- export interface PollinationsModel {
2
- name: string;
3
- description?: string;
4
- type?: string;
5
- tools?: boolean;
6
- reasoning?: boolean;
7
- context?: number;
8
- context_window?: number;
9
- input_modalities?: string[];
10
- output_modalities?: string[];
11
- paid_only?: boolean;
12
- vision?: boolean;
13
- audio?: boolean;
14
- community?: boolean;
15
- censored?: boolean;
16
- [key: string]: any;
17
- }
18
- export declare const FREE_MODELS_SEED: PollinationsModel[];
@@ -1,55 +0,0 @@
1
- export const FREE_MODELS_SEED = [
2
- {
3
- "name": "gemini",
4
- "description": "Gemini 2.5 Flash Lite",
5
- "tier": "anonymous",
6
- "tools": true,
7
- "input_modalities": ["text", "image"],
8
- "output_modalities": ["text"],
9
- "vision": true
10
- },
11
- {
12
- "name": "mistral",
13
- "description": "Mistral Small 3.2 24B",
14
- "tier": "anonymous",
15
- "tools": true,
16
- "input_modalities": ["text"],
17
- "output_modalities": ["text"],
18
- "vision": false
19
- },
20
- {
21
- "name": "openai-fast",
22
- "description": "GPT-OSS 20B Reasoning LLM (OVH)",
23
- "tier": "anonymous",
24
- "tools": true,
25
- "input_modalities": ["text"],
26
- "output_modalities": ["text"],
27
- "vision": false,
28
- "reasoning": true
29
- },
30
- {
31
- "name": "bidara",
32
- "description": "BIDARA (Biomimetic Designer)",
33
- "tier": "anonymous",
34
- "community": true,
35
- "input_modalities": ["text", "image"],
36
- "output_modalities": ["text"],
37
- "vision": true
38
- },
39
- {
40
- "name": "chickytutor",
41
- "description": "ChickyTutor AI Language Tutor",
42
- "tier": "anonymous",
43
- "community": true,
44
- "input_modalities": ["text"],
45
- "output_modalities": ["text"]
46
- },
47
- {
48
- "name": "midijourney",
49
- "description": "MIDIjourney",
50
- "tier": "anonymous",
51
- "community": true,
52
- "input_modalities": ["text"],
53
- "output_modalities": ["text"]
54
- }
55
- ];