opencode-pollinations-plugin 6.2.5 โ†’ 6.2.7-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.
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Tier Information - Central Configuration
3
+ *
4
+ * Hourly quota system (Pollinations API v2026-03)
5
+ * Quotas reset every hour at :00
6
+ */
7
+ export const TIER_INFO = {
8
+ microbe: {
9
+ name: 'Microbe',
10
+ emoji: '๐Ÿฆ ',
11
+ hourlyPollen: 0.01,
12
+ dailyEstimate: 0.24,
13
+ condition: 'Just register!',
14
+ conditionKey: 'tier.condition.signup',
15
+ },
16
+ spore: {
17
+ name: 'Spore',
18
+ emoji: '๐Ÿ„',
19
+ hourlyPollen: 0.01,
20
+ dailyEstimate: 0.24,
21
+ condition: 'Automatic verification',
22
+ conditionKey: 'tier.condition.auto_verify',
23
+ },
24
+ seed: {
25
+ name: 'Seed',
26
+ emoji: '๐ŸŒฑ',
27
+ hourlyPollen: 0.15,
28
+ dailyEstimate: 3.6,
29
+ condition: '8+ dev points (weekly auto-upgrade)',
30
+ conditionKey: 'tier.condition.dev_points',
31
+ },
32
+ flower: {
33
+ name: 'Flower',
34
+ emoji: '๐ŸŒธ',
35
+ hourlyPollen: 0.4,
36
+ dailyEstimate: 9.6,
37
+ condition: 'Publish an app',
38
+ conditionKey: 'tier.condition.publish_app',
39
+ },
40
+ nectar: {
41
+ name: 'Nectar',
42
+ emoji: '๐Ÿฏ',
43
+ hourlyPollen: 0.8,
44
+ dailyEstimate: 19.2,
45
+ condition: 'Coming soon ๐Ÿ”ฎ',
46
+ conditionKey: 'tier.condition.coming_soon',
47
+ },
48
+ };
49
+ /**
50
+ * Get tier info by name
51
+ */
52
+ export function getTierInfo(tierName) {
53
+ return TIER_INFO[tierName.toLowerCase()];
54
+ }
55
+ /**
56
+ * Get all tiers as array (sorted by hourlyPollen)
57
+ */
58
+ export function getAllTiers() {
59
+ return Object.values(TIER_INFO).sort((a, b) => a.hourlyPollen - b.hourlyPollen);
60
+ }
61
+ /**
62
+ * Format tier list for display (markdown table)
63
+ */
64
+ export function formatTierTable(lang = 'en') {
65
+ const tiers = getAllTiers();
66
+ const headers = {
67
+ en: '| Tier | Hourly | Daily (est.) | Condition |',
68
+ fr: '| Palier | Horaire | Journalier (est.) | Condition |',
69
+ es: '| Nivel | Por hora | Diario (est.) | Condiciรณn |',
70
+ de: '| Stufe | Pro Stunde | Tรคglich (ca.) | Bedingung |',
71
+ it: '| Livello | Orario | Giornaliero (stima) | Condizione |',
72
+ };
73
+ const separator = '|------|---------|----------------|-----------|';
74
+ const rows = tiers.map(tier => {
75
+ const conditionText = lang === 'fr' ? tier.condition : tier.condition;
76
+ return `| ${tier.emoji} **${tier.name}** | **${tier.hourlyPollen} pollen/h** | ~${tier.dailyEstimate}/day | ${conditionText} |`;
77
+ });
78
+ return [headers[lang], separator, ...rows].join('\n');
79
+ }
80
+ /**
81
+ * Get dynamic tier description with hourly rates
82
+ */
83
+ export function getTierDescription(lang = 'en') {
84
+ const isFrench = lang === 'fr';
85
+ const pollenWord = isFrench ? 'Pollen' : 'Pollen';
86
+ const perHour = isFrench ? '/heure' : '/hour';
87
+ const perDay = isFrench ? '/jour (est.)' : '/day (est.)';
88
+ const tiers = getAllTiers();
89
+ const lines = tiers.map(tier => {
90
+ const conditionText = isFrench ?
91
+ (tier.conditionKey === 'tier.condition.publish_app' ? '**Publier une App** (comme ce plugin !)' : tier.condition) :
92
+ tier.condition;
93
+ return `- ${tier.emoji} **${tier.name}** (**${tier.hourlyPollen} ${pollenWord}${perHour}** โ‰ˆ ~${tier.dailyEstimate}${perDay}) : ${conditionText}`;
94
+ });
95
+ return lines.join('\n');
96
+ }
@@ -1,9 +1,16 @@
1
1
  /**
2
- * beta_discovery Tool (API Explorer V3 - Hybrid Probe)
2
+ * beta_discovery Tool (API Explorer V4 โ€” Defense-in-Depth)
3
3
  *
4
- * Combines reading the official OpenAPI Specification with active
5
- * blackbox probing (triggering HTTP 400/422 ValidationErrors) to
6
- * discover hidden or undocumented enums and parameters.
4
+ * Combines reading the official OpenAPI Specification with hardened
5
+ * blackbox fuzzing that GUARANTEES HTTP 400 responses by forcefully
6
+ * injecting invalid values. The AI agent NEVER controls the actual
7
+ * values sent to the API.
8
+ *
9
+ * Security Layers:
10
+ * 1. Command Whitelist โ€” only 4 commands exist
11
+ * 2. Endpoint Whitelist โ€” only 4 API routes are probeable
12
+ * 3. Value Injection โ€” fuzz values are hardcoded, AI cannot override
13
+ * 4. Payload Sabotage โ€” fuzz corrupts, probe_missing removes keys
7
14
  */
8
15
  import { type ToolDefinition } from '@opencode-ai/plugin/tool';
9
16
  export declare const polliBetaDiscoveryTool: ToolDefinition;
@@ -1,33 +1,40 @@
1
1
  /**
2
- * beta_discovery Tool (API Explorer V3 - Hybrid Probe)
2
+ * beta_discovery Tool (API Explorer V4 โ€” Defense-in-Depth)
3
3
  *
4
- * Combines reading the official OpenAPI Specification with active
5
- * blackbox probing (triggering HTTP 400/422 ValidationErrors) to
6
- * discover hidden or undocumented enums and parameters.
4
+ * Combines reading the official OpenAPI Specification with hardened
5
+ * blackbox fuzzing that GUARANTEES HTTP 400 responses by forcefully
6
+ * injecting invalid values. The AI agent NEVER controls the actual
7
+ * values sent to the API.
8
+ *
9
+ * Security Layers:
10
+ * 1. Command Whitelist โ€” only 4 commands exist
11
+ * 2. Endpoint Whitelist โ€” only 4 API routes are probeable
12
+ * 3. Value Injection โ€” fuzz values are hardcoded, AI cannot override
13
+ * 4. Payload Sabotage โ€” fuzz corrupts, probe_missing removes keys
7
14
  */
8
15
  import { tool } from '@opencode-ai/plugin/tool';
9
16
  import { emitStatusToast } from '../../server/toast.js';
10
17
  import { getApiKey } from './shared.js';
11
- import * as fs from 'fs';
18
+ import { ModelRegistry } from '../../server/models/index.js';
12
19
  import * as https from 'https';
13
- // Primary URL for the OpenAPI spec
20
+ // โ”€โ”€โ”€ Constants โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
14
21
  const OPENAPI_URL = 'https://enter.pollinations.ai/api/docs/open-api/generate-schema';
15
- // Fallback local path
16
- const LOCAL_FALLBACK_PATH = '/home/fkomp/Bureau/oracle/Documentations/API - Severals documentations for multiples api usages/pollinations/pollinations_enter_beta/PolinationsGenBeta_api.json';
22
+ // Hardcoded fuzz sentinel values โ€” AI cannot change these
23
+ const FUZZ_STRING = '@@_FUZZ_INTENTIONAL_INVALID_VALUE_@@';
24
+ const FUZZ_NUMBER = -9999999;
25
+ const FUZZ_BOOLEAN = 'NOT_A_BOOLEAN';
26
+ // Strict endpoint whitelist โ€” only these paths can be probed
27
+ const ALLOWED_PROBE_ENDPOINTS = new Set([
28
+ '/v1/chat/completions',
29
+ '/video/{prompt}',
30
+ '/image/{prompt}',
31
+ '/audio/{text}',
32
+ ]);
33
+ // โ”€โ”€โ”€ OpenAPI Schema Cache โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
17
34
  let cachedSchema = null;
18
35
  async function fetchOpenApiSchema() {
19
36
  if (cachedSchema)
20
37
  return cachedSchema;
21
- try {
22
- if (fs.existsSync(LOCAL_FALLBACK_PATH)) {
23
- const data = fs.readFileSync(LOCAL_FALLBACK_PATH, 'utf-8');
24
- cachedSchema = JSON.parse(data);
25
- return cachedSchema;
26
- }
27
- }
28
- catch (e) {
29
- // Fallthrough
30
- }
31
38
  try {
32
39
  const response = await fetch(OPENAPI_URL);
33
40
  if (!response.ok)
@@ -36,165 +43,310 @@ async function fetchOpenApiSchema() {
36
43
  return cachedSchema;
37
44
  }
38
45
  catch (e) {
39
- throw new Error(`Failed to load OpenAPI Schema: ${e.message}`);
46
+ throw new Error(`Failed to load OpenAPI Schema from ${OPENAPI_URL}: ${e.message}`);
40
47
  }
41
48
  }
42
- async function probeEndpoint(method, endpointUrl, payloadStr) {
43
- return new Promise((resolve, reject) => {
49
+ // โ”€โ”€โ”€ HTTPS Helper (POST only for safety) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
50
+ function sendProbeRequest(endpointPath, payload) {
51
+ return new Promise((resolve) => {
44
52
  const apiKey = getApiKey();
45
- const urlObj = new URL(endpointUrl.startsWith('http') ? endpointUrl : `https://gen.pollinations.ai${endpointUrl.startsWith('/') ? '' : '/'}${endpointUrl}`);
53
+ const fullUrl = `https://gen.pollinations.ai${endpointPath.startsWith('/') ? '' : '/'}${endpointPath}`;
54
+ let urlObj;
55
+ try {
56
+ urlObj = new URL(fullUrl);
57
+ }
58
+ catch (e) {
59
+ return resolve(`โŒ Invalid URL constructed: ${fullUrl}`);
60
+ }
61
+ const postData = JSON.stringify(payload);
46
62
  const options = {
47
- method: method,
63
+ method: 'POST', // ALWAYS POST โ€” never GET (GET on /image/ or /video/ triggers generation)
48
64
  headers: {
49
- 'User-Agent': 'OpenCode-Probe-Tool/3.0',
50
- 'Accept': 'application/json'
65
+ 'User-Agent': 'OpenCode-Probe-V4/1.0',
66
+ 'Accept': 'application/json',
67
+ 'Content-Type': 'application/json',
68
+ 'Content-Length': Buffer.byteLength(postData),
51
69
  }
52
70
  };
53
71
  if (apiKey) {
54
72
  options.headers['Authorization'] = `Bearer ${apiKey}`;
55
73
  }
56
- let postData;
57
- if (method === 'POST') {
58
- options.headers['Content-Type'] = 'application/json';
59
- if (payloadStr) {
60
- try {
61
- // Try to parse just to validate it's json, but send the string
62
- JSON.parse(payloadStr);
63
- postData = payloadStr;
64
- options.headers['Content-Length'] = Buffer.byteLength(postData);
65
- }
66
- catch (e) {
67
- return resolve(`โŒ Error: payload_json must be a valid JSON string. Parse error: ${e.message}`);
68
- }
69
- }
70
- else {
71
- postData = '{}';
72
- options.headers['Content-Length'] = Buffer.byteLength(postData);
73
- }
74
- }
75
- else if (method === 'GET' && payloadStr) {
76
- try {
77
- const queryParams = JSON.parse(payloadStr);
78
- for (const [key, value] of Object.entries(queryParams)) {
79
- urlObj.searchParams.append(key, String(value));
80
- }
81
- }
82
- catch (e) {
83
- return resolve(`โŒ Error: payload_json must be a valid JSON string representing query params. Parse error: ${e.message}`);
84
- }
85
- }
86
74
  const req = https.request(urlObj, options, (res) => {
87
75
  let data = '';
88
76
  res.on('data', chunk => data += chunk);
89
77
  res.on('end', () => {
90
- let formattedResult = `**HTTP Status:** \`${res.statusCode} ${res.statusMessage}\`\n`;
91
- formattedResult += `**Content-Type:** \`${res.headers['content-type']}\`\n\n`;
78
+ let result = `**HTTP Status:** \`${res.statusCode} ${res.statusMessage}\`\n`;
79
+ result += `**Method:** \`POST\` (forced)\n`;
80
+ result += `**URL:** \`${fullUrl}\`\n`;
81
+ result += `**Content-Type:** \`${res.headers['content-type']}\`\n\n`;
92
82
  try {
93
83
  const parsed = JSON.parse(data);
94
- // Highlight Validation Errors (The main goal of the probe)
95
84
  if (res.statusCode === 400 || res.statusCode === 422 || parsed.fieldErrors || parsed.error) {
96
- formattedResult += `### ๐Ÿšจ Validation Error Detected (Jackpot!)\n`;
97
- formattedResult += `\`\`\`json\n${JSON.stringify(parsed, null, 2)}\n\`\`\``;
85
+ result += `### ๐Ÿšจ Validation Error โ€” Constraints Revealed!\n`;
86
+ result += `\`\`\`json\n${JSON.stringify(parsed, null, 2)}\n\`\`\``;
87
+ }
88
+ else if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
89
+ result += `### โš ๏ธ Unexpected 2xx Response (probe may have triggered real processing)\n`;
90
+ result += `\`\`\`json\n${JSON.stringify(parsed, null, 2).substring(0, 1500)}${data.length > 1500 ? '\n... (truncated)' : ''}\n\`\`\``;
98
91
  }
99
92
  else {
100
- formattedResult += `### Response Body\n`;
101
- formattedResult += `\`\`\`json\n${JSON.stringify(parsed, null, 2).substring(0, 2000)}${data.length > 2000 ? '\n... (truncated)' : ''}\n\`\`\``;
93
+ result += `### Response Body\n`;
94
+ result += `\`\`\`json\n${JSON.stringify(parsed, null, 2).substring(0, 2000)}${data.length > 2000 ? '\n... (truncated)' : ''}\n\`\`\``;
102
95
  }
103
96
  }
104
97
  catch (e) {
105
- // Not JSON
106
- formattedResult += `### Raw Response Body\n`;
107
- formattedResult += `\`\`\`text\n${data.substring(0, 2000)}${data.length > 2000 ? '\n... (truncated)' : ''}\n\`\`\``;
98
+ result += `### Raw Response\n`;
99
+ result += `\`\`\`text\n${data.substring(0, 2000)}${data.length > 2000 ? '\n... (truncated)' : ''}\n\`\`\``;
108
100
  }
109
- resolve(formattedResult);
101
+ resolve(result);
110
102
  });
111
103
  });
112
104
  req.on('error', (e) => resolve(`โŒ Request Error: ${e.message}`));
113
- if (postData)
114
- req.write(postData);
105
+ req.setTimeout(10000, () => {
106
+ req.destroy();
107
+ resolve('โŒ Request Timeout (10s)');
108
+ });
109
+ req.write(postData);
115
110
  req.end();
116
111
  });
117
112
  }
113
+ // โ”€โ”€โ”€ Security: Endpoint Validation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
114
+ function validateEndpoint(path) {
115
+ if (!ALLOWED_PROBE_ENDPOINTS.has(path)) {
116
+ const allowed = Array.from(ALLOWED_PROBE_ENDPOINTS).join(', ');
117
+ return `โŒ SECURITY: Endpoint \`${path}\` is NOT in the whitelist.\nAllowed endpoints: ${allowed}\n\nUse \`search_schema\` to explore the OpenAPI spec instead.`;
118
+ }
119
+ return null; // OK
120
+ }
121
+ // โ”€โ”€โ”€ Command: search_schema โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
122
+ async function cmdSearchSchema(query) {
123
+ const schema = await fetchOpenApiSchema();
124
+ const results = [];
125
+ const queryLower = query.toLowerCase();
126
+ const search = (obj, currentPath) => {
127
+ if (typeof obj !== 'object' || obj === null)
128
+ return;
129
+ // Check current node
130
+ if (typeof obj === 'object') {
131
+ // Check enum values
132
+ if (obj.enum && Array.isArray(obj.enum)) {
133
+ const enumStr = obj.enum.join(', ');
134
+ if (currentPath.toLowerCase().includes(queryLower) || enumStr.toLowerCase().includes(queryLower)) {
135
+ results.push({
136
+ path: currentPath,
137
+ type: 'enum',
138
+ detail: `Values: [${enumStr}]${obj.description ? ` โ€” ${obj.description}` : ''}`
139
+ });
140
+ }
141
+ }
142
+ // Check description
143
+ if (typeof obj.description === 'string' && obj.description.toLowerCase().includes(queryLower)) {
144
+ if (!results.find(r => r.path === currentPath)) {
145
+ results.push({
146
+ path: currentPath,
147
+ type: 'description',
148
+ detail: obj.description.substring(0, 300)
149
+ });
150
+ }
151
+ }
152
+ // Check parameter names
153
+ if (typeof obj.name === 'string' && obj.name.toLowerCase().includes(queryLower)) {
154
+ results.push({
155
+ path: currentPath,
156
+ type: 'parameter',
157
+ detail: `Name: ${obj.name}, Type: ${obj.schema?.type || obj.type || '?'}${obj.required ? ' (REQUIRED)' : ''}${obj.description ? ` โ€” ${obj.description.substring(0, 200)}` : ''}`
158
+ });
159
+ }
160
+ }
161
+ // Recurse
162
+ for (const [key, value] of Object.entries(obj)) {
163
+ search(value, currentPath ? `${currentPath}.${key}` : key);
164
+ }
165
+ };
166
+ search(schema, '');
167
+ if (results.length === 0) {
168
+ return `โ„น๏ธ No matches found for "${query}" in the OpenAPI specification.\nTry broader terms like "model", "voice", "duration", "aspect", "format".`;
169
+ }
170
+ // Deduplicate and cap at 30 results
171
+ const unique = results.slice(0, 30);
172
+ let output = `### ๐Ÿ” OpenAPI Search Results for "${query}" (${unique.length} matches)\n\n`;
173
+ for (const r of unique) {
174
+ output += `- **\`${r.path}\`** [${r.type}]\n ${r.detail}\n\n`;
175
+ }
176
+ if (results.length > 30) {
177
+ output += `\n_... and ${results.length - 30} more matches. Refine your query._`;
178
+ }
179
+ return output;
180
+ }
181
+ // โ”€โ”€โ”€ Command: fuzz_parameter โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
182
+ async function cmdFuzzParameter(endpointPath, basePayloadStr, fuzzTarget) {
183
+ // Security Layer 2: Endpoint whitelist
184
+ const endpointError = validateEndpoint(endpointPath);
185
+ if (endpointError)
186
+ return endpointError;
187
+ // Parse the base payload
188
+ let payload;
189
+ try {
190
+ payload = JSON.parse(basePayloadStr);
191
+ }
192
+ catch (e) {
193
+ return `โŒ Error: base_payload_json is not valid JSON: ${e.message}`;
194
+ }
195
+ if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
196
+ return 'โŒ Error: base_payload_json must be a JSON object (not array or primitive)';
197
+ }
198
+ // Security Layer 3: Forced value injection
199
+ // Detect the current type to inject the right sabotage value
200
+ const currentValue = payload[fuzzTarget];
201
+ let injectedValue;
202
+ if (typeof currentValue === 'number') {
203
+ injectedValue = FUZZ_NUMBER;
204
+ }
205
+ else if (typeof currentValue === 'boolean') {
206
+ injectedValue = FUZZ_BOOLEAN;
207
+ }
208
+ else {
209
+ // Default to string fuzz for unknown/string/undefined types
210
+ injectedValue = FUZZ_STRING;
211
+ }
212
+ // Force-inject the fuzz value โ€” AI CANNOT override this
213
+ payload[fuzzTarget] = injectedValue;
214
+ const header = `### ๐Ÿงช Fuzzing \`${fuzzTarget}\` on \`${endpointPath}\`\n`;
215
+ const info = `**Injected value:** \`${JSON.stringify(injectedValue)}\` (forced by security layer)\n`;
216
+ const payloadPreview = `**Payload sent:**\n\`\`\`json\n${JSON.stringify(payload, null, 2)}\n\`\`\`\n\n`;
217
+ const response = await sendProbeRequest(endpointPath, payload);
218
+ return header + info + payloadPreview + '---\n\n' + response;
219
+ }
220
+ // โ”€โ”€โ”€ Command: probe_missing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
221
+ async function cmdProbeMissing(endpointPath, basePayloadStr, removeKey) {
222
+ // Security Layer 2: Endpoint whitelist
223
+ const endpointError = validateEndpoint(endpointPath);
224
+ if (endpointError)
225
+ return endpointError;
226
+ // Parse the base payload
227
+ let payload;
228
+ try {
229
+ payload = JSON.parse(basePayloadStr);
230
+ }
231
+ catch (e) {
232
+ return `โŒ Error: base_payload_json is not valid JSON: ${e.message}`;
233
+ }
234
+ if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
235
+ return 'โŒ Error: base_payload_json must be a JSON object (not array or primitive)';
236
+ }
237
+ // Security Layer 4: Payload sabotage โ€” REMOVE the key entirely
238
+ const hadKey = removeKey in payload;
239
+ delete payload[removeKey];
240
+ const header = `### ๐Ÿ”ฌ Testing if \`${removeKey}\` is required on \`${endpointPath}\`\n`;
241
+ const info = `**Key "${removeKey}" ${hadKey ? 'existed and was REMOVED' : 'was NOT present'}**\n`;
242
+ const payloadPreview = `**Payload sent (without "${removeKey}"):**\n\`\`\`json\n${JSON.stringify(payload, null, 2)}\n\`\`\`\n\n`;
243
+ const response = await sendProbeRequest(endpointPath, payload);
244
+ return header + info + payloadPreview + '---\n\n' + response;
245
+ }
246
+ // โ”€โ”€โ”€ Command: list_models_registry โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
247
+ function cmdListModelsRegistry(category) {
248
+ if (!ModelRegistry.isReady()) {
249
+ return 'โš ๏ธ ModelRegistry is not yet initialized. The plugin may still be loading. Try again in a few seconds.';
250
+ }
251
+ const validCategories = ['image', 'video', 'audio', 'text'];
252
+ if (category && !validCategories.includes(category)) {
253
+ return `โŒ Invalid category "${category}". Valid: ${validCategories.join(', ')}`;
254
+ }
255
+ const models = category
256
+ ? ModelRegistry.list(category)
257
+ : ModelRegistry.all();
258
+ if (models.length === 0) {
259
+ return `โ„น๏ธ No models found${category ? ` in category "${category}"` : ''}.`;
260
+ }
261
+ let output = `### ๐Ÿ“ฆ Local Model Registry${category ? ` โ€” ${category}` : ' โ€” All Categories'} (${models.length} models)\n\n`;
262
+ // Group by category
263
+ const grouped = new Map();
264
+ for (const m of models) {
265
+ const cat = m.category;
266
+ if (!grouped.has(cat))
267
+ grouped.set(cat, []);
268
+ grouped.get(cat).push(m);
269
+ }
270
+ for (const [cat, catModels] of grouped) {
271
+ output += `#### ${cat.toUpperCase()} (${catModels.length})\n\n`;
272
+ output += `| Model | Description | Paid | I2X | Tools | Modalities |\n`;
273
+ output += `|-------|-------------|------|-----|-------|------------|\n`;
274
+ for (const m of catModels) {
275
+ const desc = (m.description || '').substring(0, 40);
276
+ const paid = m.paid_only ? '๐Ÿ’Ž' : '๐Ÿ†“';
277
+ const i2x = m.supportsI2X ? 'โœ…' : 'โ€”';
278
+ const tools = m.tools ? 'โœ…' : 'โ€”';
279
+ const mods = `${m.input_modalities.join(',')}โ†’${m.output_modalities.join(',')}`;
280
+ output += `| \`${m.name}\` | ${desc} | ${paid} | ${i2x} | ${tools} | ${mods} |\n`;
281
+ }
282
+ output += '\n';
283
+ }
284
+ return output;
285
+ }
286
+ // โ”€โ”€โ”€ Tool Export โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
118
287
  export const polliBetaDiscoveryTool = tool({
119
- description: `Explore the Pollinations API using Hybrid Discovery (OpenAPI + Active Probing).
120
- Use this tool ONLY to reverse engineer and fill the manual registry. Do not use this tool lightly for general operations.
288
+ description: `API Explorer V4 (Defense-in-Depth) โ€” Safely explore the Pollinations API without risking billing.
289
+ Use this tool ONLY to reverse-engineer API parameters and fill the manual registry.
290
+
291
+ ๐Ÿ›ก๏ธ SECURITY: This tool uses 4 layers of protection to prevent accidental billing:
292
+ - All probe values are force-injected (you cannot send valid values)
293
+ - Only whitelisted endpoints can be probed
294
+ - All probes use POST method (prevents GET-based generation)
295
+ - The payload is always sabotaged (invalid value OR missing required key)
121
296
 
122
- ๐Ÿ”ฅ CRITICAL RULES FOR AI AGENTS:
123
- 1. The EXACT and ONLY base URL for any generative media endpoints (image, video, audio) is: **https://gen.pollinations.ai**
124
- 2. The URL 'enter.pollinations.ai' is strictly reserved for the OpenAPI schema documentation and account management. Do NOT hallucinate target endpoints on it!
297
+ Commands:
298
+ - 'search_schema': Search the OpenAPI spec for keywords (e.g. "aspectRatio", "voice", "duration"). Pure read, zero network requests.
299
+ - 'fuzz_parameter': Send a request with an intentionally INVALID value for a specific parameter. The tool force-injects garbage to trigger Zod validation errors that reveal real constraints. YOU DO NOT CONTROL THE VALUE SENT.
300
+ - 'probe_missing': Remove a key from the payload to test if it's required. Sends an incomplete request to trigger "Required" errors.
301
+ - 'list_models_registry': Show all models from the local ModelRegistry cache. Zero network requests. Use this first to understand what the plugin already knows.
125
302
 
126
- Commands available:
127
- - 'list_endpoints': (Whitebox) Returns all routes from OpenAPI.
128
- - 'get_endpoint': (Whitebox) Returns param schema for a route from OpenAPI.
129
- - 'get_enums': (Whitebox) Recursively searches the OpenAPI spec for a parameter enum (e.g. 'voice', 'model').
130
- - 'probe_endpoint': (Blackbox) Sends a real HTTP request (GET or POST) to trigger API validation errors (HTTP 400). Use this to reverse-engineer undocumented enums by sending invalid values. The tool auto-injects your API key.`,
303
+ Allowed probe endpoints: /v1/chat/completions, /video/{prompt}, /image/{prompt}, /audio/{text}`,
131
304
  args: {
132
- command: tool.schema.enum(['list_endpoints', 'get_endpoint', 'get_enums', 'probe_endpoint']).describe('The action to perform'),
133
- endpoint_path: tool.schema.string().optional().describe('OpenAPI path (e.g. "/v1/chat/completions") or full URL for probe_endpoint'),
134
- parameter_name: tool.schema.string().optional().describe('Required for get_enums (e.g. "voice", "model")'),
135
- probe_method: tool.schema.enum(['GET', 'POST']).optional().describe('Required for probe_endpoint'),
136
- probe_payload_json: tool.schema.string().optional().describe('JSON string of query params (GET) or body (POST) to send during probe_endpoint. e.g. "{\\"model\\":\\"fake-model\\"}" to trigger an error showing valid models.'),
305
+ command: tool.schema.enum(['search_schema', 'fuzz_parameter', 'probe_missing', 'list_models_registry']).describe('Action to perform'),
306
+ query: tool.schema.string().optional().describe('For search_schema: keyword to search (e.g. "aspectRatio", "voice", "model")'),
307
+ endpoint_path: tool.schema.string().optional().describe('For fuzz_parameter/probe_missing: API path (e.g. "/v1/chat/completions")'),
308
+ base_payload_json: tool.schema.string().optional().describe('For fuzz_parameter/probe_missing: base JSON payload (will be sabotaged by security layer)'),
309
+ fuzz_target: tool.schema.string().optional().describe('For fuzz_parameter: parameter name to fuzz (e.g. "model", "aspectRatio")'),
310
+ remove_key: tool.schema.string().optional().describe('For probe_missing: key name to remove from payload to test if required'),
311
+ category: tool.schema.string().optional().describe('For list_models_registry: filter by category (image/video/audio/text)'),
137
312
  },
138
313
  async execute(args, context) {
139
- emitStatusToast('info', `Discovery: ${args.command}...`, '๐Ÿ”Ž API Probe');
140
- context.metadata({ title: `Probe: ${args.command}` });
314
+ emitStatusToast('info', `Explorer V4: ${args.command}...`, '๐Ÿ”Ž API Explorer');
315
+ context.metadata({ title: `Explorer: ${args.command}` });
141
316
  try {
142
- if (args.command === 'probe_endpoint') {
143
- if (!args.endpoint_path || !args.probe_method) {
144
- return 'โŒ Error: `endpoint_path` and `probe_method` are required for command `probe_endpoint`';
317
+ switch (args.command) {
318
+ case 'search_schema': {
319
+ if (!args.query)
320
+ return 'โŒ Error: `query` is required for search_schema';
321
+ return await cmdSearchSchema(args.query);
145
322
  }
146
- return await probeEndpoint(args.probe_method, args.endpoint_path, args.probe_payload_json);
147
- }
148
- // Whitebox commands need OpenAPI
149
- const schema = await fetchOpenApiSchema();
150
- if (args.command === 'list_endpoints') {
151
- const paths = Object.keys(schema.paths || {});
152
- return `**Available API Endpoints (OpenAPI):**\n\n\`\`\`json\n${JSON.stringify(paths, null, 2)}\n\`\`\``;
153
- }
154
- if (args.command === 'get_endpoint') {
155
- if (!args.endpoint_path)
156
- return 'โŒ Error: `endpoint_path` is required';
157
- const details = schema.paths[args.endpoint_path];
158
- if (!details)
159
- return `โŒ Endpoint '${args.endpoint_path}' not found in OpenAPI schema.`;
160
- return `**Endpoint Details for \`${args.endpoint_path}\`:**\n\n\`\`\`json\n${JSON.stringify(details, null, 2)}\n\`\`\``;
161
- }
162
- if (args.command === 'get_enums') {
163
- if (!args.parameter_name)
164
- return 'โŒ Error: `parameter_name` is required';
165
- const results = [];
166
- const findEnums = (obj, path = '') => {
167
- if (typeof obj !== 'object' || obj === null)
168
- return;
169
- if (obj.enum && Array.isArray(obj.enum) && path.includes(args.parameter_name)) {
170
- results.push({
171
- path: path,
172
- enum: obj.enum,
173
- description: obj.description
174
- });
175
- }
176
- for (const [key, value] of Object.entries(obj)) {
177
- findEnums(value, path ? `${path}.${key}` : key);
178
- }
179
- };
180
- findEnums(schema.components?.schemas, 'components.schemas');
181
- findEnums(schema.paths, 'paths');
182
- if (results.length === 0) {
183
- return `โ„น๏ธ No enums found matching parameter '${args.parameter_name}' in OpenAPI. You should use 'probe_endpoint' to test it manually!`;
323
+ case 'fuzz_parameter': {
324
+ if (!args.endpoint_path)
325
+ return 'โŒ Error: `endpoint_path` is required for fuzz_parameter';
326
+ if (!args.base_payload_json)
327
+ return 'โŒ Error: `base_payload_json` is required for fuzz_parameter';
328
+ if (!args.fuzz_target)
329
+ return 'โŒ Error: `fuzz_target` is required for fuzz_parameter';
330
+ return await cmdFuzzParameter(args.endpoint_path, args.base_payload_json, args.fuzz_target);
184
331
  }
185
- let output = `**Discovered Enums for '${args.parameter_name}':**\n\n`;
186
- results.forEach(res => {
187
- output += `- **Found at**: \`${res.path}\`\n`;
188
- if (res.description)
189
- output += ` - **Desc**: ${res.description}\n`;
190
- output += ` - **Values**: ${res.enum.join(', ')}\n\n`;
191
- });
192
- return output;
332
+ case 'probe_missing': {
333
+ if (!args.endpoint_path)
334
+ return 'โŒ Error: `endpoint_path` is required for probe_missing';
335
+ if (!args.base_payload_json)
336
+ return 'โŒ Error: `base_payload_json` is required for probe_missing';
337
+ if (!args.remove_key)
338
+ return 'โŒ Error: `remove_key` is required for probe_missing';
339
+ return await cmdProbeMissing(args.endpoint_path, args.base_payload_json, args.remove_key);
340
+ }
341
+ case 'list_models_registry': {
342
+ return cmdListModelsRegistry(args.category);
343
+ }
344
+ default:
345
+ return 'โŒ Unknown command';
193
346
  }
194
- return 'โŒ Unknown command';
195
347
  }
196
348
  catch (err) {
197
- emitStatusToast('error', `Discovery failed: ${err.message}`, '๐Ÿ”Ž API Probe');
349
+ emitStatusToast('error', `Explorer failed: ${err.message}`, '๐Ÿ”Ž API Explorer');
198
350
  return `โŒ Critical Error: ${err.message}`;
199
351
  }
200
352
  }
@@ -15,7 +15,7 @@
15
15
  import { tool } from '@opencode-ai/plugin/tool';
16
16
  import * as fs from 'fs';
17
17
  import * as path from 'path';
18
- import { getApiKey, httpsGet, ensureDir, generateFilename, getDefaultOutputDir, formatCost, formatFileSize, estimateVideoCost, extractCostFromHeaders, isCostEstimatorEnabled, supportsI2V, requiresI2V, validateAspectRatio, getDurationRange, getVideoModels, fetchEnterBalance, sanitizeFilename, validateHttpUrl, } from './shared.js';
18
+ import { getApiKey, httpsGet, ensureDir, generateFilename, getDefaultOutputDir, formatCost, formatFileSize, estimateVideoCost, extractCostFromHeaders, isCostEstimatorEnabled, supportsI2V, requiresI2V, getDurationRange, getVideoModels, fetchEnterBalance, sanitizeFilename, validateHttpUrl, } from './shared.js';
19
19
  import { loadConfig } from '../../server/config.js';
20
20
  import { checkCostControl, isTokenBased } from './cost-guard.js';
21
21
  import { emitStatusToast } from '../../server/toast.js';
@@ -58,7 +58,7 @@ export const polliGenVideoTool = tool({
58
58
  return t('tools.polli_gen_video.invalid_duration', { model, duration, min: minDuration, max: maxDuration });
59
59
  }
60
60
  // Validate aspect ratio (for known models; beta models accept any)
61
- if (!isBetaModel && !validateAspectRatio(model, aspectRatio)) {
61
+ if (!isBetaModel && !modelConfig.aspectRatios.includes(aspectRatio)) {
62
62
  return t('tools.polli_gen_video.invalid_aspect', { model, aspect: aspectRatio, supported: modelConfig.aspectRatios.join(', ') });
63
63
  }
64
64
  // Check I2V requirements & validation
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-pollinations-plugin",
3
3
  "displayName": "Pollinations",
4
- "version": "6.2.5",
4
+ "version": "6.2.7-1",
5
5
  "description": "Native Pollinations.ai Provider Plugin for OpenCode",
6
6
  "publisher": "pollinations",
7
7
  "repository": {
@@ -61,4 +61,4 @@
61
61
  "@types/qrcode": "^1.5.6",
62
62
  "typescript": "^5.0.0"
63
63
  }
64
- }
64
+ }