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.
- package/README.de.md +85 -71
- package/README.es.md +95 -81
- package/README.fr.md +76 -62
- package/README.it.md +90 -76
- package/README.md +60 -50
- package/README.zh.md +144 -0
- package/dist/index.js +11 -7
- package/dist/locales/en.json +1 -1
- package/dist/locales/fr.json +1 -1
- package/dist/server/commands.js +8 -1
- package/dist/server/generate-config.js +5 -6
- package/dist/server/models/cache.js +1 -1
- package/dist/server/models/fetcher.js +25 -5
- package/dist/server/quota.js +10 -9
- package/dist/server/tier-info.d.ts +31 -0
- package/dist/server/tier-info.js +96 -0
- package/dist/tools/pollinations/beta_discovery.d.ts +11 -4
- package/dist/tools/pollinations/beta_discovery.js +288 -136
- package/dist/tools/pollinations/gen_video.js +2 -2
- package/package.json +2 -2
|
@@ -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
|
|
2
|
+
* beta_discovery Tool (API Explorer V4 โ Defense-in-Depth)
|
|
3
3
|
*
|
|
4
|
-
* Combines reading the official OpenAPI Specification with
|
|
5
|
-
* blackbox
|
|
6
|
-
*
|
|
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
|
|
2
|
+
* beta_discovery Tool (API Explorer V4 โ Defense-in-Depth)
|
|
3
3
|
*
|
|
4
|
-
* Combines reading the official OpenAPI Specification with
|
|
5
|
-
* blackbox
|
|
6
|
-
*
|
|
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
|
|
18
|
+
import { ModelRegistry } from '../../server/models/index.js';
|
|
12
19
|
import * as https from 'https';
|
|
13
|
-
//
|
|
20
|
+
// โโโ Constants โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
14
21
|
const OPENAPI_URL = 'https://enter.pollinations.ai/api/docs/open-api/generate-schema';
|
|
15
|
-
//
|
|
16
|
-
const
|
|
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
|
-
|
|
43
|
-
|
|
49
|
+
// โโโ HTTPS Helper (POST only for safety) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
50
|
+
function sendProbeRequest(endpointPath, payload) {
|
|
51
|
+
return new Promise((resolve) => {
|
|
44
52
|
const apiKey = getApiKey();
|
|
45
|
-
const
|
|
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:
|
|
63
|
+
method: 'POST', // ALWAYS POST โ never GET (GET on /image/ or /video/ triggers generation)
|
|
48
64
|
headers: {
|
|
49
|
-
'User-Agent': 'OpenCode-Probe-
|
|
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
|
|
91
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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(
|
|
101
|
+
resolve(result);
|
|
110
102
|
});
|
|
111
103
|
});
|
|
112
104
|
req.on('error', (e) => resolve(`โ Request Error: ${e.message}`));
|
|
113
|
-
|
|
114
|
-
req.
|
|
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: `
|
|
120
|
-
Use this tool ONLY to reverse
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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(['
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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', `
|
|
140
|
-
context.metadata({ title: `
|
|
314
|
+
emitStatusToast('info', `Explorer V4: ${args.command}...`, '๐ API Explorer');
|
|
315
|
+
context.metadata({ title: `Explorer: ${args.command}` });
|
|
141
316
|
try {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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', `
|
|
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,
|
|
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 && !
|
|
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.
|
|
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
|
+
}
|