opencode-pollinations-plugin 5.9.0 → 6.0.0-beta.18

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
@@ -10,9 +10,11 @@
10
10
 
11
11
  <div align="center">
12
12
 
13
- ![Version](https://img.shields.io/badge/version-5.6.0-blue.svg)
13
+ ![Version](https://img.shields.io/badge/version-5.8.4--beta.15-orange.svg)
14
14
  ![License](https://img.shields.io/badge/license-MIT-green.svg)
15
- ![Status](https://img.shields.io/badge/status-Stable-success.svg)
15
+ ![Status](https://img.shields.io/badge/status-Beta-yellow.svg)
16
+
17
+ [📜 View Changelog](./CHANGELOG.md) | [🛣️ Roadmap](./ROADMAP.md)
16
18
 
17
19
  </div>
18
20
 
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as http from 'http';
2
2
  import * as fs from 'fs';
3
3
  import { generatePollinationsConfig } from './server/generate-config.js';
4
- import { loadConfig } from './server/config.js';
4
+ import { loadConfig, saveConfig } from './server/config.js';
5
5
  import { handleChatCompletion } from './server/proxy.js';
6
6
  import { createToastHooks, setGlobalClient } from './server/toast.js';
7
7
  import { createStatusHooks } from './server/status.js';
@@ -15,12 +15,10 @@ function log(msg) {
15
15
  }
16
16
  catch (e) { }
17
17
  }
18
- // Port killing removed: Using dynamic ports.
18
+ // === PROXY SERVER ===
19
19
  const startProxy = () => {
20
20
  return new Promise((resolve) => {
21
21
  const server = http.createServer(async (req, res) => {
22
- // ... (Request Handling) ...
23
- // We reuse the existing logic structure but simplified startup
24
22
  log(`[Proxy] Request: ${req.method} ${req.url}`);
25
23
  res.setHeader('Access-Control-Allow-Origin', '*');
26
24
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
@@ -62,7 +60,6 @@ const startProxy = () => {
62
60
  res.writeHead(404);
63
61
  res.end("Not Found");
64
62
  });
65
- // Listen on random port (0) to avoid conflicts (CLI/IDE)
66
63
  server.listen(0, '127.0.0.1', () => {
67
64
  // @ts-ignore
68
65
  const assignedPort = server.address().port;
@@ -75,6 +72,81 @@ const startProxy = () => {
75
72
  });
76
73
  });
77
74
  };
75
+ // === AUTH HOOK: Native /connect Integration ===
76
+ const createAuthHook = () => ({
77
+ provider: 'pollinations',
78
+ // LOADER: Called by OpenCode when it needs credentials
79
+ // This enables HOT RELOAD - called before each request that needs auth
80
+ loader: async (auth, provider) => {
81
+ log('[AuthHook] loader() called - fetching credentials');
82
+ try {
83
+ const authData = await auth();
84
+ if (authData && 'key' in authData && authData.key) {
85
+ log(`[AuthHook] Got key from OpenCode auth: ${authData.key.substring(0, 8)}...`);
86
+ // Sync to our config for other parts of the plugin
87
+ saveConfig({ apiKey: authData.key });
88
+ return { apiKey: authData.key };
89
+ }
90
+ }
91
+ catch (e) {
92
+ log(`[AuthHook] loader() error: ${e}`);
93
+ }
94
+ // Fallback to our own config
95
+ const config = loadConfig();
96
+ if (config.apiKey) {
97
+ log(`[AuthHook] Using key from plugin config: ${config.apiKey.substring(0, 8)}...`);
98
+ return { apiKey: config.apiKey };
99
+ }
100
+ log('[AuthHook] No API key available');
101
+ return {};
102
+ },
103
+ // METHODS: Define how user can authenticate
104
+ methods: [{
105
+ type: 'api',
106
+ label: 'API Key',
107
+ prompts: [{
108
+ type: 'text',
109
+ key: 'apiKey',
110
+ message: 'Enter your Pollinations API Key',
111
+ placeholder: 'sk_...',
112
+ validate: (value) => {
113
+ if (!value || value.length < 10) {
114
+ return 'API key must be at least 10 characters';
115
+ }
116
+ if (!value.startsWith('sk_') && !value.startsWith('sk-')) {
117
+ return 'API key should start with sk_ or sk-';
118
+ }
119
+ return undefined; // Valid
120
+ }
121
+ }],
122
+ authorize: async (inputs) => {
123
+ log(`[AuthHook] authorize() called with key: ${inputs?.apiKey?.substring(0, 8)}...`);
124
+ if (!inputs?.apiKey) {
125
+ return { type: 'failed' };
126
+ }
127
+ // Validate key by testing API
128
+ try {
129
+ const response = await fetch('https://gen.pollinations.ai/text/models', {
130
+ headers: { 'Authorization': `Bearer ${inputs.apiKey}` }
131
+ });
132
+ if (response.ok) {
133
+ log('[AuthHook] Key validated successfully');
134
+ // Save to our config for immediate use
135
+ saveConfig({ apiKey: inputs.apiKey });
136
+ return { type: 'success', key: inputs.apiKey };
137
+ }
138
+ else {
139
+ log(`[AuthHook] Key validation failed: ${response.status}`);
140
+ return { type: 'failed' };
141
+ }
142
+ }
143
+ catch (e) {
144
+ log(`[AuthHook] Key validation error: ${e}`);
145
+ return { type: 'failed' };
146
+ }
147
+ }
148
+ }]
149
+ });
78
150
  // === PLUGIN EXPORT ===
79
151
  export const PollinationsPlugin = async (ctx) => {
80
152
  log(`Plugin Initializing v${require('../package.json').version}...`);
@@ -85,10 +157,11 @@ export const PollinationsPlugin = async (ctx) => {
85
157
  const toastHooks = createToastHooks(ctx.client);
86
158
  const commandHooks = createCommandHooks();
87
159
  return {
160
+ // AUTH HOOK: Native /connect integration
161
+ auth: createAuthHook(),
88
162
  async config(config) {
89
163
  log("[Hook] config() called");
90
- // STARTUP only - No complex hot reload logic
91
- // The user must restart OpenCode to refresh this list if they change keys.
164
+ // Generate models based on current auth state
92
165
  const modelsArray = await generatePollinationsConfig();
93
166
  const modelsObj = {};
94
167
  for (const m of modelsArray) {
@@ -96,12 +169,14 @@ export const PollinationsPlugin = async (ctx) => {
96
169
  }
97
170
  if (!config.provider)
98
171
  config.provider = {};
99
- // Dynamic Provider Name
100
172
  const version = require('../package.json').version;
101
173
  config.provider['pollinations'] = {
102
- id: 'pollinations',
174
+ id: 'openai',
103
175
  name: `Pollinations AI (v${version})`,
104
- options: { baseURL: localBaseUrl },
176
+ options: {
177
+ baseURL: localBaseUrl,
178
+ apiKey: 'plugin-managed', // Key is managed by auth hook
179
+ },
105
180
  models: modelsObj
106
181
  };
107
182
  log(`[Hook] Registered ${Object.keys(modelsObj).length} models.`);
@@ -303,15 +303,15 @@ async function handleConnectCommand(args) {
303
303
  // 1. Universal Validation (No Syntax Check) - Functional Check
304
304
  emitStatusToast('info', 'Vérification de la clé...', 'Pollinations Config');
305
305
  try {
306
- const models = await generatePollinationsConfig(key, true);
307
- // 2. Check if we got Enterprise models
308
- const enterpriseModels = models.filter(m => m.id.startsWith('enter/'));
309
- if (enterpriseModels.length > 0) {
306
+ const models = await generatePollinationsConfig(key);
307
+ // 2. Check if we got real models (not just connect placeholder)
308
+ const realModels = models.filter(m => m.id !== 'connect');
309
+ if (realModels.length > 0) {
310
310
  // SUCCESS
311
311
  saveConfig({ apiKey: key }); // Don't force mode 'pro'. Let user decide.
312
312
  const masked = key.substring(0, 6) + '...';
313
313
  // Count Paid Only models found
314
- const diamondCount = enterpriseModels.filter(m => m.name.includes('💎')).length;
314
+ const diamondCount = realModels.filter(m => m.name.includes('💎')).length;
315
315
  // CHECK RESTRICTIONS: Strict Check (Usage + Profile + Balance)
316
316
  let forcedModeMsg = "";
317
317
  let isLimited = false;
@@ -336,10 +336,10 @@ async function handleConnectCommand(args) {
336
336
  else {
337
337
  saveConfig({ apiKey: key, keyHasAccessToProfile: true }); // Let user keep current mode or default
338
338
  }
339
- emitStatusToast('success', `Clé Valide! (${enterpriseModels.length} modèles Pro débloqués)`, 'Pollinations Config');
339
+ emitStatusToast('success', `Clé Valide! (${realModels.length} modèles débloqués)`, 'Pollinations Config');
340
340
  return {
341
341
  handled: true,
342
- response: `✅ **Connexion Réussie!**\n- Clé: \`${masked}\`\n- Modèles Débloqués: ${enterpriseModels.length} (dont ${diamondCount} 💎 Paid)${forcedModeMsg}`
342
+ response: `✅ **Connexion Réussie!**\n- Clé: \`${masked}\`\n- Modèles Débloqués: ${realModels.length} (dont ${diamondCount} 💎 Paid)${forcedModeMsg}`
343
343
  };
344
344
  }
345
345
  else {
@@ -351,18 +351,8 @@ async function handleConnectCommand(args) {
351
351
  // Wait, generate-config falls back to providing a list containing "[Enter] GPT-4o (Fallback)" if fetch failed.
352
352
  // So we need to detect if it's a "REAL" fetch or a "FALLBACK" fetch.
353
353
  // The fallback models have `variants: {}` usually, but real ones might too.
354
- // A better check: The fallback list is hardcoded in generate-config.ts catch block.
355
- // Let's modify generate-config to return EMPTY list on error?
356
- // Or just check if the returned models work?
357
- // Simplest: If `generatePollinationsConfig` returns any model starting with `enter/` that includes "(Fallback)" in name, we assume failure?
358
- // "GPT-4o (Fallback)" is the name.
359
- const isFallback = models.some(m => m.name.includes('(Fallback)') && m.id.startsWith('enter/'));
360
- if (isFallback) {
361
- throw new Error("Clé rejetée par l'API (Accès refusé ou invalide).");
362
- }
363
- // If we are here, we got no enter models, or empty list?
364
- // If key is valid but has no access?
365
- throw new Error("Aucun modèle Enterprise détecté pour cette clé.");
354
+ // v6.0: No fallback prefix check needed. If models is empty (only connect), key is invalid.
355
+ throw new Error("Aucun modèle détecté pour cette clé. Clé invalide ou expirée.");
366
356
  }
367
357
  }
368
358
  catch (e) {
@@ -1,8 +1,34 @@
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
+ export interface PollinationsModel {
8
+ name: string;
9
+ description?: string;
10
+ type?: string;
11
+ tools?: boolean;
12
+ reasoning?: boolean;
13
+ context?: number;
14
+ context_window?: number;
15
+ input_modalities?: string[];
16
+ output_modalities?: string[];
17
+ paid_only?: boolean;
18
+ vision?: boolean;
19
+ audio?: boolean;
20
+ pricing?: {
21
+ promptTextTokens?: number;
22
+ completionTextTokens?: number;
23
+ promptImageTokens?: number;
24
+ promptAudioTokens?: number;
25
+ completionAudioTokens?: number;
26
+ };
27
+ [key: string]: any;
28
+ }
1
29
  interface OpenCodeModel {
2
30
  id: string;
3
31
  name: string;
4
- object: string;
5
- variants?: any;
6
32
  options?: any;
7
33
  limit?: {
8
34
  context?: number;
@@ -12,6 +38,7 @@ interface OpenCodeModel {
12
38
  input?: string[];
13
39
  output?: string[];
14
40
  };
41
+ tool_call?: boolean;
15
42
  }
16
- export declare function generatePollinationsConfig(forceApiKey?: string, forceStrict?: boolean): Promise<OpenCodeModel[]>;
43
+ export declare function generatePollinationsConfig(forceApiKey?: string): Promise<OpenCodeModel[]>;
17
44
  export {};
@@ -1,11 +1,12 @@
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
+ */
1
7
  import * as https from 'https';
2
8
  import * as fs from 'fs';
3
- import * as os from 'os';
4
- import * as path from 'path';
5
9
  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');
9
10
  // --- LOGGING ---
10
11
  const LOG_FILE = '/tmp/opencode_pollinations_config.log';
11
12
  function log(msg) {
@@ -13,15 +14,18 @@ function log(msg) {
13
14
  const ts = new Date().toISOString();
14
15
  if (!fs.existsSync(LOG_FILE))
15
16
  fs.writeFileSync(LOG_FILE, '');
16
- fs.appendFileSync(LOG_FILE, `[ConfigGen] ${ts} ${msg}\n`);
17
+ fs.appendFileSync(LOG_FILE, `[ConfigGen v6.0] ${ts} ${msg}\n`);
17
18
  }
18
19
  catch (e) { }
19
- // Force output to stderr for CLI visibility if needed, but clean.
20
20
  }
21
- // Fetch Helper
21
+ // --- NETWORK HELPER ---
22
22
  function fetchJson(url, headers = {}) {
23
23
  return new Promise((resolve, reject) => {
24
- const req = https.get(url, { headers }, (res) => {
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) => {
25
29
  let data = '';
26
30
  res.on('data', chunk => data += chunk);
27
31
  res.on('end', () => {
@@ -31,7 +35,7 @@ function fetchJson(url, headers = {}) {
31
35
  }
32
36
  catch (e) {
33
37
  log(`JSON Parse Error for ${url}: ${e}`);
34
- resolve([]); // Fail safe -> empty list
38
+ resolve([]);
35
39
  }
36
40
  });
37
41
  });
@@ -39,193 +43,132 @@ function fetchJson(url, headers = {}) {
39
43
  log(`Network Error for ${url}: ${e.message}`);
40
44
  reject(e);
41
45
  });
42
- req.setTimeout(5000, () => {
46
+ req.setTimeout(15000, () => {
43
47
  req.destroy();
44
48
  reject(new Error('Timeout'));
45
49
  });
46
50
  });
47
51
  }
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) {
52
+ // --- GENERATOR LOGIC ---
53
+ export async function generatePollinationsConfig(forceApiKey) {
58
54
  const config = loadConfig();
59
55
  const modelsOutput = [];
60
- log(`Starting Configuration (V5.1.22 Hot-Reload)...`);
61
- // Use forced key (from Hook) or cached key
62
56
  const effectiveKey = forceApiKey || config.apiKey;
63
- // 1. FREE UNIVERSE
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
64
72
  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] ');
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);
70
86
  modelsOutput.push(mapped);
71
87
  });
72
- log(`Fetched ${modelsOutput.length} Free models.`);
88
+ log(`Total models registered: ${modelsOutput.length}`);
89
+ log(`Model IDs: ${modelsOutput.map(m => m.id).join(', ')}`);
73
90
  }
74
91
  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
- // }
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
- }
92
+ log(`Error fetching models: ${e.message}`);
93
+ // Return connect placeholder only on error
135
94
  }
136
95
  return modelsOutput;
137
96
  }
138
- // --- CAPABILITY ICONS ---
97
+ // --- UTILS ---
139
98
  function getCapabilityIcons(raw) {
140
99
  const icons = [];
141
- // Vision: accepts images
142
- if (raw.input_modalities?.includes('image'))
100
+ // Vision: check both input_modalities and legacy vision flag
101
+ if (raw.input_modalities?.includes('image') || raw.vision === true) {
143
102
  icons.push('👁️');
144
- // Audio Input
145
- if (raw.input_modalities?.includes('audio'))
103
+ }
104
+ // Audio input
105
+ if (raw.input_modalities?.includes('audio') || raw.audio === true) {
146
106
  icons.push('🎙️');
147
- // Audio Output
148
- if (raw.output_modalities?.includes('audio'))
107
+ }
108
+ // Audio output
109
+ if (raw.output_modalities?.includes('audio')) {
149
110
  icons.push('🔊');
150
- // Reasoning capability
151
- if (raw.reasoning === true)
111
+ }
112
+ // Reasoning
113
+ if (raw.reasoning === true) {
152
114
  icons.push('🧠');
153
- // Web Search (from description)
154
- if (raw.description?.toLowerCase().includes('search') ||
155
- raw.name?.includes('search') ||
156
- raw.name?.includes('perplexity')) {
115
+ }
116
+ // Search capability
117
+ if (raw.description?.toLowerCase().includes('search') || raw.name?.includes('search')) {
157
118
  icons.push('🔍');
158
119
  }
159
- // Tool/Function calling
160
- if (raw.tools === true)
161
- icons.push('💻');
120
+ // Tools
121
+ if (raw.tools === true) {
122
+ icons.push('🛠️');
123
+ }
124
+ // Paid only
125
+ if (raw.paid_only === true) {
126
+ icons.push('💎');
127
+ }
162
128
  return icons.length > 0 ? ` ${icons.join('')}` : '';
163
129
  }
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/")
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
168
138
  let baseName = raw.description;
169
139
  if (!baseName || baseName === rawId) {
170
- baseName = formatName(rawId, raw.censored !== false);
140
+ baseName = formatName(rawId);
171
141
  }
172
- // CLEANUP: Simple Truncation Rule (Requested by User)
173
- // "Start from left, find ' - ', delete everything after."
142
+ // Truncate after first " - "
174
143
  if (baseName && baseName.includes(' - ')) {
175
144
  baseName = baseName.split(' - ')[0].trim();
176
145
  }
177
- let namePrefixFinal = namePrefix;
178
- if (raw.paid_only) {
179
- namePrefixFinal = namePrefix.replace('[Enter]', '[💎 Paid]');
180
- }
181
- // Get capability icons from API metadata
182
146
  const capabilityIcons = getCapabilityIcons(raw);
183
- const finalName = `${namePrefixFinal}${baseName}${capabilityIcons}`;
147
+ const finalName = `${baseName}${capabilityIcons}`;
148
+ // Determine modalities for OpenCode
149
+ const inputMods = raw.input_modalities || ['text'];
150
+ const outputMods = raw.output_modalities || ['text'];
184
151
  const modelObj = {
185
- id: fullId,
152
+ id: rawId, // No prefix! Direct model ID
186
153
  name: finalName,
187
- object: 'model',
188
- variants: {},
189
- // Declare modalities for OpenCode vision support
190
154
  modalities: {
191
- input: raw.input_modalities || ['text'],
192
- output: raw.output_modalities || ['text']
193
- }
155
+ input: inputMods,
156
+ output: outputMods
157
+ },
158
+ tool_call: raw.tools === true && rawId !== 'nomnom' // NomNom: no tools
194
159
  };
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.
160
+ // Model-specific limits
209
161
  if (rawId.includes('nova')) {
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
- // NOMNOM FIX: User reported error if max_tokens is missing.
218
- // Also it is a 'Gemini-scrape' model, so we treat it similar to Gemini but with strict limit.
219
- if (rawId.includes('nomnom') || rawId.includes('scrape')) {
220
- modelObj.limit = {
221
- output: 2048, // User used 1500 successfully
222
- context: 32768
223
- };
162
+ modelObj.limit = { output: 8000, context: 128000 };
163
+ }
164
+ if (rawId === 'nomnom') {
165
+ modelObj.limit = { output: 2048, context: 32768 };
166
+ modelObj.tool_call = false; // NomNom is a router, no external tools
224
167
  }
225
- if (rawId.includes('fast') || rawId.includes('flash') || rawId.includes('lite')) {
226
- if (!rawId.includes('gemini')) {
227
- modelObj.variants.speed = { options: { thinking: { disabled: true } } };
228
- }
168
+ if (rawId.includes('chicky') || rawId.includes('mistral')) {
169
+ modelObj.limit = { output: 4096, context: 8192 };
170
+ modelObj.options = { maxTokens: 4096 };
229
171
  }
172
+ log(`[Mapped] ${modelObj.id} → ${modelObj.name} | tools=${modelObj.tool_call} | modalities=${JSON.stringify(modelObj.modalities)}`);
230
173
  return modelObj;
231
174
  }
@@ -0,0 +1,18 @@
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[];
@@ -0,0 +1,55 @@
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
+ ];
@@ -153,6 +153,11 @@ export async function handleChatCompletion(req, res, bodyRaw) {
153
153
  const config = loadConfig();
154
154
  // DEBUG: Trace Config State for Hot Reload verification
155
155
  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) { }
156
161
  // 0. COMMAND HANDLING
157
162
  if (body.messages && body.messages.length > 0) {
158
163
  const lastMsg = body.messages[body.messages.length - 1];
@@ -211,15 +216,16 @@ export async function handleChatCompletion(req, res, bodyRaw) {
211
216
  // LOAD QUOTA FOR SAFETY CHECKS
212
217
  const { getQuotaStatus, formatQuotaForToast } = await import('./quota.js');
213
218
  const quota = await getQuotaStatus(false);
214
- // A. Resolve Base Target
215
- if (actualModel.startsWith('enter/')) {
216
- isEnterprise = true;
217
- actualModel = actualModel.replace('enter/', '');
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[-/]/, '');
218
223
  }
219
- else if (actualModel.startsWith('free/')) {
220
- isEnterprise = false;
221
- actualModel = actualModel.replace('free/', '');
224
+ else if (actualModel.startsWith('free/') || actualModel.startsWith('free-')) {
225
+ actualModel = actualModel.replace(/^free[-/]/, '');
222
226
  }
227
+ // v6.0: Everything is enterprise now (requires API key)
228
+ isEnterprise = true;
223
229
  // A.1 PAID MODEL ENFORCEMENT (V5.5 Strategy)
224
230
  // Check dynamic list saved by generate-config.ts
225
231
  if (isEnterprise) {
@@ -331,24 +337,16 @@ export async function handleChatCompletion(req, res, bodyRaw) {
331
337
  }
332
338
  }
333
339
  }
334
- // C. Construct URL & Headers
335
- if (isEnterprise) {
336
- if (!config.apiKey) {
337
- emitLogToast('error', "Missing API Key for Enterprise Model", 'Proxy Error');
338
- res.writeHead(401, { 'Content-Type': 'application/json' });
339
- res.end(JSON.stringify({ error: { message: "API Key required for Enterprise models." } }));
340
- return;
341
- }
342
- targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
343
- authHeader = `Bearer ${config.apiKey}`;
344
- log(`Routing to ENTERPRISE: ${actualModel}`);
345
- }
346
- else {
347
- targetUrl = 'https://text.pollinations.ai/openai/chat/completions';
348
- authHeader = undefined;
349
- log(`Routing to FREE: ${actualModel} ${isFallbackActive ? '(FALLBACK)' : ''}`);
350
- // emitLogToast('info', `Routing to: FREE UNIVERSE (${actualModel})`, 'Pollinations Routing'); // Too noisy
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;
351
346
  }
347
+ targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
348
+ authHeader = `Bearer ${config.apiKey}`;
349
+ log(`Routing to gen.pollinations.ai: ${actualModel}`);
352
350
  // NOTIFY SWITCH
353
351
  if (isFallbackActive) {
354
352
  emitStatusToast('warning', `⚠️ Safety Net: ${actualModel} (${fallbackReason})`, 'Pollinations Safety');
@@ -416,13 +414,32 @@ export async function handleChatCompletion(req, res, bodyRaw) {
416
414
  // Restore Tools but REMOVE conflicting ones (Search)
417
415
  // B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
418
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
+ }
419
434
  // B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
420
435
  // Fixes "Multiple tools" error (Vertex) and "JSON body validation failed" (v5.3.5 regression)
421
- else if (actualModel.includes("gemini")) {
436
+ // Added ChickyTutor (Claude/Gemini based) to fix "toolConfig must be defined" error.
437
+ else if (actualModel.includes("gemini") || actualModel.includes("chickytutor")) {
422
438
  let hasFunctions = false;
423
439
  if (proxyBody.tools && Array.isArray(proxyBody.tools)) {
424
440
  hasFunctions = proxyBody.tools.some((t) => t.type === 'function' || t.function);
425
441
  }
442
+ // Old Shim logic removed (moved up)
426
443
  if (hasFunctions) {
427
444
  // 1. Strict cleanup of 'google_search' tool
428
445
  proxyBody.tools = proxyBody.tools.filter((t) => {
@@ -453,6 +470,14 @@ export async function handleChatCompletion(req, res, bodyRaw) {
453
470
  log(`[Proxy] Gemini Logic: Tools=${proxyBody.tools ? proxyBody.tools.length : 'REMOVED'}, Stops NOT Injected.`);
454
471
  }
455
472
  }
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
+ }
456
481
  // C. GEMINI ID BACKTRACKING & SIGNATURE
457
482
  if ((actualModel.includes("gemini") || actualModel === "nomnom") && proxyBody.messages) {
458
483
  const lastMsg = proxyBody.messages[proxyBody.messages.length - 1];
@@ -561,10 +586,10 @@ export async function handleChatCompletion(req, res, bodyRaw) {
561
586
  // 2. Notify
562
587
  emitStatusToast('warning', `⚠️ Safety Net: ${actualModel} (${fallbackReason})`, 'Pollinations Safety');
563
588
  emitLogToast('warning', `Recovering from ${fetchRes.status} -> Switching to ${actualModel}`, 'Safety Net');
564
- // 3. Re-Prepare Request
565
- targetUrl = 'https://text.pollinations.ai/openai/chat/completions';
589
+ // 3. Re-Prepare Request - v6.0: Stay on gen.pollinations.ai
590
+ targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
566
591
  const retryHeaders = { ...headers };
567
- delete retryHeaders['Authorization']; // Free = No Auth
592
+ // Keep Authorization for gen.pollinations.ai
568
593
  const retryBody = { ...proxyBody, model: actualModel };
569
594
  // 4. Retry Fetch
570
595
  const retryRes = await fetchWithRetry(targetUrl, {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-pollinations-plugin",
3
3
  "displayName": "Pollinations AI (V5.6)",
4
- "version": "5.9.0",
4
+ "version": "6.0.0-beta.18",
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,36 +0,0 @@
1
- import * as https from 'https';
2
-
3
- function checkEndpoint(ep, key) {
4
- return new Promise((resolve) => {
5
- console.log(`Checking ${ep}...`);
6
- const req = https.request({
7
- hostname: 'gen.pollinations.ai',
8
- path: ep,
9
- method: 'GET',
10
- headers: { 'Authorization': `Bearer ${key}` }
11
- }, (res) => {
12
- console.log(`Status Code: ${res.statusCode}`);
13
- let data = '';
14
- res.on('data', chunk => data += chunk);
15
- res.on('end', () => {
16
- console.log(`Headers:`, res.headers);
17
- console.log(`Body Full: ${data}`);
18
- if (res.statusCode === 200) resolve({ ok: true, body: data });
19
- else resolve({ ok: false, status: res.statusCode, body: data });
20
- });
21
- });
22
- req.on('error', (e) => {
23
- console.log(`Error: ${e.message}`);
24
- resolve({ ok: false, status: e.message || 'Error' });
25
- });
26
- req.setTimeout(10000, () => req.destroy());
27
- req.end();
28
- });
29
- }
30
-
31
- const KEY = "plln_sk_F7a4RcBG4AVCeBSo6lnS36EKwm0nPn1O";
32
-
33
- (async () => {
34
- const res = await checkEndpoint('/account/profile', KEY);
35
- console.log('Result:', res);
36
- })();
@@ -1 +0,0 @@
1
- export declare const createPollinationsFetch: (apiKey: string) => (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
package/dist/provider.js DELETED
@@ -1,135 +0,0 @@
1
- // Removed invalid imports
2
- import * as fs from 'fs';
3
- // --- Sanitization Helpers (Ported from Gateway/Upstream) ---
4
- function safeId(id) {
5
- if (!id)
6
- return id;
7
- if (id.length > 30)
8
- return id.substring(0, 30);
9
- return id;
10
- }
11
- function logDebug(message, data) {
12
- try {
13
- const timestamp = new Date().toISOString();
14
- let logMsg = `[${timestamp}] ${message}`;
15
- if (data) {
16
- logMsg += `\n${JSON.stringify(data, null, 2)}`;
17
- }
18
- fs.appendFileSync('/tmp/opencode_pollinations_debug.log', logMsg + '\n\n');
19
- }
20
- catch (e) {
21
- // ignore logging errors
22
- }
23
- }
24
- function sanitizeTools(tools) {
25
- if (!Array.isArray(tools))
26
- return tools;
27
- const cleanSchema = (schema) => {
28
- if (!schema || typeof schema !== "object")
29
- return;
30
- if (schema.optional !== undefined)
31
- delete schema.optional;
32
- if (schema.ref !== undefined)
33
- delete schema.ref;
34
- if (schema["$ref"] !== undefined)
35
- delete schema["$ref"];
36
- if (schema.properties) {
37
- for (const key in schema.properties)
38
- cleanSchema(schema.properties[key]);
39
- }
40
- if (schema.items)
41
- cleanSchema(schema.items);
42
- };
43
- return tools.map((tool) => {
44
- const newTool = { ...tool };
45
- if (newTool.function && newTool.function.parameters) {
46
- cleanSchema(newTool.function.parameters);
47
- }
48
- return newTool;
49
- });
50
- }
51
- function filterTools(tools, maxCount = 120) {
52
- if (!Array.isArray(tools))
53
- return [];
54
- if (tools.length <= maxCount)
55
- return tools;
56
- const priorities = [
57
- "bash", "read", "write", "edit", "webfetch", "glob", "grep",
58
- "searxng_remote_search", "deepsearch_deep_search", "google_search",
59
- "task", "todowrite"
60
- ];
61
- const priorityTools = tools.filter((t) => priorities.includes(t.function.name));
62
- const otherTools = tools.filter((t) => !priorities.includes(t.function.name));
63
- const slotsLeft = maxCount - priorityTools.length;
64
- const othersKept = otherTools.slice(0, Math.max(0, slotsLeft));
65
- logDebug(`[POLLI-PLUGIN] Filtering tools: ${tools.length} -> ${priorityTools.length + othersKept.length}`);
66
- return [...priorityTools, ...othersKept];
67
- }
68
- // --- Fetch Implementation ---
69
- export const createPollinationsFetch = (apiKey) => async (input, init) => {
70
- let url = input.toString();
71
- const options = init || {};
72
- let body = null;
73
- if (options.body && typeof options.body === "string") {
74
- try {
75
- body = JSON.parse(options.body);
76
- }
77
- catch (e) {
78
- // Not JSON, ignore
79
- }
80
- }
81
- // --- INTERCEPTION & SANITIZATION ---
82
- if (body) {
83
- let model = body.model || "";
84
- // 0. Model Name Normalization
85
- if (typeof model === "string" && model.startsWith("pollinations/enter/")) {
86
- body.model = model.replace("pollinations/enter/", "");
87
- model = body.model;
88
- }
89
- // FIX: Remove stream_options (causes 400 on some OpenAI proxies)
90
- if (body.stream_options) {
91
- delete body.stream_options;
92
- }
93
- // 1. Azure Tool Limit Fix
94
- if ((model.includes("openai") || model.includes("gpt")) && body.tools) {
95
- if (body.tools.length > 120) {
96
- body.tools = filterTools(body.tools, 120);
97
- }
98
- }
99
- // 2. Vertex/Gemini Schema Fix
100
- if (model.includes("gemini") && body.tools) {
101
- body.tools = sanitizeTools(body.tools);
102
- }
103
- // Re-serialize body
104
- options.body = JSON.stringify(body);
105
- }
106
- // Ensure Headers
107
- const headers = new Headers(options.headers || {});
108
- headers.set("Authorization", `Bearer ${apiKey}`);
109
- headers.set("Content-Type", "application/json");
110
- options.headers = headers;
111
- logDebug(`Req: ${url}`, body);
112
- try {
113
- const response = await global.fetch(url, options);
114
- // Log response status
115
- // We clone to read text for debugging errors
116
- if (!response.ok) {
117
- try {
118
- const clone = response.clone();
119
- const text = await clone.text();
120
- logDebug(`Res (Error): ${response.status}`, text);
121
- }
122
- catch (e) {
123
- logDebug(`Res (Error): ${response.status} (Read failed)`);
124
- }
125
- }
126
- else {
127
- logDebug(`Res (OK): ${response.status}`);
128
- }
129
- return response;
130
- }
131
- catch (e) {
132
- logDebug(`Fetch Error: ${e.message}`);
133
- throw e;
134
- }
135
- };
@@ -1 +0,0 @@
1
- export declare const createPollinationsFetch: (apiKey: string) => (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
@@ -1,135 +0,0 @@
1
- // Removed invalid imports
2
- import * as fs from 'fs';
3
- // --- Sanitization Helpers (Ported from Gateway/Upstream) ---
4
- function safeId(id) {
5
- if (!id)
6
- return id;
7
- if (id.length > 30)
8
- return id.substring(0, 30);
9
- return id;
10
- }
11
- function logDebug(message, data) {
12
- try {
13
- const timestamp = new Date().toISOString();
14
- let logMsg = `[${timestamp}] ${message}`;
15
- if (data) {
16
- logMsg += `\n${JSON.stringify(data, null, 2)}`;
17
- }
18
- fs.appendFileSync('/tmp/opencode_pollinations_debug.log', logMsg + '\n\n');
19
- }
20
- catch (e) {
21
- // ignore logging errors
22
- }
23
- }
24
- function sanitizeTools(tools) {
25
- if (!Array.isArray(tools))
26
- return tools;
27
- const cleanSchema = (schema) => {
28
- if (!schema || typeof schema !== "object")
29
- return;
30
- if (schema.optional !== undefined)
31
- delete schema.optional;
32
- if (schema.ref !== undefined)
33
- delete schema.ref;
34
- if (schema["$ref"] !== undefined)
35
- delete schema["$ref"];
36
- if (schema.properties) {
37
- for (const key in schema.properties)
38
- cleanSchema(schema.properties[key]);
39
- }
40
- if (schema.items)
41
- cleanSchema(schema.items);
42
- };
43
- return tools.map((tool) => {
44
- const newTool = { ...tool };
45
- if (newTool.function && newTool.function.parameters) {
46
- cleanSchema(newTool.function.parameters);
47
- }
48
- return newTool;
49
- });
50
- }
51
- function filterTools(tools, maxCount = 120) {
52
- if (!Array.isArray(tools))
53
- return [];
54
- if (tools.length <= maxCount)
55
- return tools;
56
- const priorities = [
57
- "bash", "read", "write", "edit", "webfetch", "glob", "grep",
58
- "searxng_remote_search", "deepsearch_deep_search", "google_search",
59
- "task", "todowrite"
60
- ];
61
- const priorityTools = tools.filter((t) => priorities.includes(t.function.name));
62
- const otherTools = tools.filter((t) => !priorities.includes(t.function.name));
63
- const slotsLeft = maxCount - priorityTools.length;
64
- const othersKept = otherTools.slice(0, Math.max(0, slotsLeft));
65
- logDebug(`[POLLI-PLUGIN] Filtering tools: ${tools.length} -> ${priorityTools.length + othersKept.length}`);
66
- return [...priorityTools, ...othersKept];
67
- }
68
- // --- Fetch Implementation ---
69
- export const createPollinationsFetch = (apiKey) => async (input, init) => {
70
- let url = input.toString();
71
- const options = init || {};
72
- let body = null;
73
- if (options.body && typeof options.body === "string") {
74
- try {
75
- body = JSON.parse(options.body);
76
- }
77
- catch (e) {
78
- // Not JSON, ignore
79
- }
80
- }
81
- // --- INTERCEPTION & SANITIZATION ---
82
- if (body) {
83
- let model = body.model || "";
84
- // 0. Model Name Normalization
85
- if (typeof model === "string" && model.startsWith("pollinations/enter/")) {
86
- body.model = model.replace("pollinations/enter/", "");
87
- model = body.model;
88
- }
89
- // FIX: Remove stream_options (causes 400 on some OpenAI proxies)
90
- if (body.stream_options) {
91
- delete body.stream_options;
92
- }
93
- // 1. Azure Tool Limit Fix
94
- if ((model.includes("openai") || model.includes("gpt")) && body.tools) {
95
- if (body.tools.length > 120) {
96
- body.tools = filterTools(body.tools, 120);
97
- }
98
- }
99
- // 2. Vertex/Gemini Schema Fix
100
- if (model.includes("gemini") && body.tools) {
101
- body.tools = sanitizeTools(body.tools);
102
- }
103
- // Re-serialize body
104
- options.body = JSON.stringify(body);
105
- }
106
- // Ensure Headers
107
- const headers = new Headers(options.headers || {});
108
- headers.set("Authorization", `Bearer ${apiKey}`);
109
- headers.set("Content-Type", "application/json");
110
- options.headers = headers;
111
- logDebug(`Req: ${url}`, body);
112
- try {
113
- const response = await global.fetch(url, options);
114
- // Log response status
115
- // We clone to read text for debugging errors
116
- if (!response.ok) {
117
- try {
118
- const clone = response.clone();
119
- const text = await clone.text();
120
- logDebug(`Res (Error): ${response.status}`, text);
121
- }
122
- catch (e) {
123
- logDebug(`Res (Error): ${response.status} (Read failed)`);
124
- }
125
- }
126
- else {
127
- logDebug(`Res (OK): ${response.status}`);
128
- }
129
- return response;
130
- }
131
- catch (e) {
132
- logDebug(`Fetch Error: ${e.message}`);
133
- throw e;
134
- }
135
- };
@@ -1,9 +0,0 @@
1
- import { createRequire } from 'module';
2
- const require = createRequire(import.meta.url);
3
- try {
4
- const pkg = require('../package.json');
5
- console.log("SUCCESS: Loaded version " + pkg.version);
6
- } catch (e) {
7
- console.error("FAILURE:", e.message);
8
- process.exit(1);
9
- }