opencode-pollinations-plugin 6.0.0-beta.18 → 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.
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">
@@ -10,11 +10,9 @@
10
10
 
11
11
  <div align="center">
12
12
 
13
- ![Version](https://img.shields.io/badge/version-5.8.4--beta.15-orange.svg)
13
+ ![Version](https://img.shields.io/badge/version-5.6.0-blue.svg)
14
14
  ![License](https://img.shields.io/badge/license-MIT-green.svg)
15
- ![Status](https://img.shields.io/badge/status-Beta-yellow.svg)
16
-
17
- [📜 View Changelog](./CHANGELOG.md) | [🛣️ Roadmap](./ROADMAP.md)
15
+ ![Status](https://img.shields.io/badge/status-Stable-success.svg)
18
16
 
19
17
  </div>
20
18
 
@@ -136,8 +134,9 @@ OpenCode uses NPM as its registry. To publish:
136
134
 
137
135
  ### 1. The Basics (Free Mode)
138
136
  Just type in the chat. You are in **Manual Mode** by default.
139
- - Model: `openai` (GPT-4o Mini equivalent)
140
- - Model: `mistral` (Mistral Nemo)
137
+ - Model: `openai-fast` (GPT-OSS 20b)
138
+ - Model: `mistral` (Mistral Small 3.1)
139
+ - ...
141
140
 
142
141
  ### 🔑 Configuration (API Key)
143
142
 
@@ -157,6 +156,7 @@ Just type in the chat. You are in **Manual Mode** by default.
157
156
 
158
157
  ## 🔗 Links
159
158
 
159
+ - **Sign up Pollinations Beta (more and best free tiers access and paids models)**: [pollinations.ai](https://enter.pollinations.ai)
160
160
  - **Pollinations Website**: [pollinations.ai](https://pollinations.ai)
161
161
  - **Discord Community**: [Join us!](https://discord.gg/pollinations-ai-885844321461485618)
162
162
  - **OpenCode Ecosystem**: [opencode.ai](https://opencode.ai/docs/ecosystem#plugins)
package/dist/index.js CHANGED
@@ -1,11 +1,12 @@
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, saveConfig } from './server/config.js';
4
+ import { loadConfig } 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';
8
8
  import { createCommandHooks } from './server/commands.js';
9
+ import { createToolRegistry } from './tools/index.js';
9
10
  import { createRequire } from 'module';
10
11
  const require = createRequire(import.meta.url);
11
12
  const LOG_FILE = '/tmp/opencode_pollinations_v4.log';
@@ -15,10 +16,12 @@ function log(msg) {
15
16
  }
16
17
  catch (e) { }
17
18
  }
18
- // === PROXY SERVER ===
19
+ // Port killing removed: Using dynamic ports.
19
20
  const startProxy = () => {
20
21
  return new Promise((resolve) => {
21
22
  const server = http.createServer(async (req, res) => {
23
+ // ... (Request Handling) ...
24
+ // We reuse the existing logic structure but simplified startup
22
25
  log(`[Proxy] Request: ${req.method} ${req.url}`);
23
26
  res.setHeader('Access-Control-Allow-Origin', '*');
24
27
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
@@ -60,6 +63,7 @@ const startProxy = () => {
60
63
  res.writeHead(404);
61
64
  res.end("Not Found");
62
65
  });
66
+ // Listen on random port (0) to avoid conflicts (CLI/IDE)
63
67
  server.listen(0, '127.0.0.1', () => {
64
68
  // @ts-ignore
65
69
  const assignedPort = server.address().port;
@@ -72,81 +76,6 @@ const startProxy = () => {
72
76
  });
73
77
  });
74
78
  };
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
- });
150
79
  // === PLUGIN EXPORT ===
151
80
  export const PollinationsPlugin = async (ctx) => {
152
81
  log(`Plugin Initializing v${require('../package.json').version}...`);
@@ -156,12 +85,15 @@ export const PollinationsPlugin = async (ctx) => {
156
85
  setGlobalClient(ctx.client);
157
86
  const toastHooks = createToastHooks(ctx.client);
158
87
  const commandHooks = createCommandHooks();
88
+ // Build tool registry (conditional on API key presence)
89
+ const toolRegistry = createToolRegistry();
90
+ log(`[Tools] ${Object.keys(toolRegistry).length} tools registered`);
159
91
  return {
160
- // AUTH HOOK: Native /connect integration
161
- auth: createAuthHook(),
92
+ tool: toolRegistry,
162
93
  async config(config) {
163
94
  log("[Hook] config() called");
164
- // Generate models based on current auth state
95
+ // STARTUP only - No complex hot reload logic
96
+ // The user must restart OpenCode to refresh this list if they change keys.
165
97
  const modelsArray = await generatePollinationsConfig();
166
98
  const modelsObj = {};
167
99
  for (const m of modelsArray) {
@@ -169,14 +101,12 @@ export const PollinationsPlugin = async (ctx) => {
169
101
  }
170
102
  if (!config.provider)
171
103
  config.provider = {};
104
+ // Dynamic Provider Name
172
105
  const version = require('../package.json').version;
173
106
  config.provider['pollinations'] = {
174
- id: 'openai',
107
+ id: 'pollinations',
175
108
  name: `Pollinations AI (v${version})`,
176
- options: {
177
- baseURL: localBaseUrl,
178
- apiKey: 'plugin-managed', // Key is managed by auth hook
179
- },
109
+ options: { baseURL: localBaseUrl },
180
110
  models: modelsObj
181
111
  };
182
112
  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);
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) {
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) {
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 = realModels.filter(m => m.name.includes('💎')).length;
314
+ const diamondCount = enterpriseModels.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! (${realModels.length} modèles débloqués)`, 'Pollinations Config');
339
+ emitStatusToast('success', `Clé Valide! (${enterpriseModels.length} modèles Pro 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: ${realModels.length} (dont ${diamondCount} 💎 Paid)${forcedModeMsg}`
342
+ response: `✅ **Connexion Réussie!**\n- Clé: \`${masked}\`\n- Modèles Débloqués: ${enterpriseModels.length} (dont ${diamondCount} 💎 Paid)${forcedModeMsg}`
343
343
  };
344
344
  }
345
345
  else {
@@ -351,8 +351,18 @@ 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
- // 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.");
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é.");
356
366
  }
357
367
  }
358
368
  catch (e) {
@@ -1,34 +1,8 @@
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
- }
29
1
  interface OpenCodeModel {
30
2
  id: string;
31
3
  name: string;
4
+ object: string;
5
+ variants?: any;
32
6
  options?: any;
33
7
  limit?: {
34
8
  context?: number;
@@ -38,7 +12,6 @@ interface OpenCodeModel {
38
12
  input?: string[];
39
13
  output?: string[];
40
14
  };
41
- tool_call?: boolean;
42
15
  }
43
- export declare function generatePollinationsConfig(forceApiKey?: string): Promise<OpenCodeModel[]>;
16
+ export declare function generatePollinationsConfig(forceApiKey?: string, forceStrict?: boolean): Promise<OpenCodeModel[]>;
44
17
  export {};
@@ -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
  }