opencode-pollinations-plugin 5.1.3
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/LICENSE.md +21 -0
- package/README.md +140 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +128 -0
- package/dist/provider.d.ts +1 -0
- package/dist/provider.js +135 -0
- package/dist/provider_v1.d.ts +1 -0
- package/dist/provider_v1.js +135 -0
- package/dist/server/commands.d.ts +10 -0
- package/dist/server/commands.js +302 -0
- package/dist/server/config.d.ts +49 -0
- package/dist/server/config.js +159 -0
- package/dist/server/generate-config.d.ts +13 -0
- package/dist/server/generate-config.js +154 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +120 -0
- package/dist/server/pollinations-api.d.ts +48 -0
- package/dist/server/pollinations-api.js +147 -0
- package/dist/server/proxy.d.ts +2 -0
- package/dist/server/proxy.js +588 -0
- package/dist/server/quota.d.ts +15 -0
- package/dist/server/quota.js +210 -0
- package/dist/server/router.d.ts +8 -0
- package/dist/server/router.js +122 -0
- package/dist/server/status.d.ts +3 -0
- package/dist/server/status.js +31 -0
- package/dist/server/toast.d.ts +6 -0
- package/dist/server/toast.js +78 -0
- package/package.json +53 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import * as http from 'http';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { getAggregatedModels } from './pollinations-api.js';
|
|
5
|
+
import { loadConfig, saveConfig } from './config.js';
|
|
6
|
+
import { handleChatCompletion } from './proxy.js';
|
|
7
|
+
const LOG_FILE = path.join(process.env.HOME || '/tmp', '.config/opencode/plugins/pollinations-v3.log');
|
|
8
|
+
// Simple file logger
|
|
9
|
+
function log(msg) {
|
|
10
|
+
const ts = new Date().toISOString();
|
|
11
|
+
try {
|
|
12
|
+
if (!fs.existsSync(path.dirname(LOG_FILE))) {
|
|
13
|
+
fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
fs.appendFileSync(LOG_FILE, `[${ts}] ${msg}\n`);
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
// silent fail
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const server = http.createServer(async (req, res) => {
|
|
22
|
+
log(`${req.method} ${req.url}`);
|
|
23
|
+
// CORS Headers
|
|
24
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
25
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
26
|
+
res.setHeader('Access-Control-Allow-Headers', '*');
|
|
27
|
+
if (req.method === 'OPTIONS') {
|
|
28
|
+
res.writeHead(204);
|
|
29
|
+
res.end();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// AUTH ENDPOINT (Kept for compatibility, though Native Auth is preferred)
|
|
33
|
+
if (req.method === 'POST' && req.url === '/v1/auth') {
|
|
34
|
+
const chunks = [];
|
|
35
|
+
req.on('data', chunk => chunks.push(chunk));
|
|
36
|
+
req.on('end', async () => {
|
|
37
|
+
try {
|
|
38
|
+
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
39
|
+
if (body && body.apiKey) {
|
|
40
|
+
saveConfig({ apiKey: body.apiKey, mode: 'pro' });
|
|
41
|
+
log(`[AUTH] Key saved via Server Endpoint`);
|
|
42
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
43
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
res.writeHead(400);
|
|
47
|
+
res.end(JSON.stringify({ error: "Missing apiKey" }));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
log(`[AUTH] Error: ${e}`);
|
|
52
|
+
res.writeHead(500);
|
|
53
|
+
res.end(JSON.stringify({ error: String(e) }));
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (req.method === 'GET' && req.url === '/health') {
|
|
59
|
+
const config = loadConfig();
|
|
60
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
61
|
+
res.end(JSON.stringify({
|
|
62
|
+
status: "ok",
|
|
63
|
+
version: "v3.0.0-phase3",
|
|
64
|
+
mode: config.mode,
|
|
65
|
+
hasKey: !!config.apiKey
|
|
66
|
+
}));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (req.method === 'GET' && req.url === '/v1/models') {
|
|
70
|
+
try {
|
|
71
|
+
const models = await getAggregatedModels();
|
|
72
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
73
|
+
res.end(JSON.stringify(models));
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
log(`Error fetching models: ${e}`);
|
|
77
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
78
|
+
res.end(JSON.stringify({ error: "Failed to fetch models" }));
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (req.method === 'POST' && req.url === '/v1/chat/completions') {
|
|
83
|
+
// Accumulate body for the proxy
|
|
84
|
+
const chunks = [];
|
|
85
|
+
req.on('data', chunk => chunks.push(chunk));
|
|
86
|
+
req.on('end', async () => {
|
|
87
|
+
try {
|
|
88
|
+
const bodyRaw = Buffer.concat(chunks).toString();
|
|
89
|
+
await handleChatCompletion(req, res, bodyRaw);
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
log(`Error in chat handler: ${e}`);
|
|
93
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
94
|
+
res.end(JSON.stringify({ error: "Internal Server Error in Chat Handler" }));
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
res.writeHead(404);
|
|
100
|
+
res.end("Not Found");
|
|
101
|
+
});
|
|
102
|
+
const PORT = parseInt(process.env.POLLINATIONS_PORT || '10001', 10);
|
|
103
|
+
// ANTI-ZOMBIE (Visible Logs Restored)
|
|
104
|
+
try {
|
|
105
|
+
const { execSync } = require('child_process');
|
|
106
|
+
try {
|
|
107
|
+
console.log(`[POLLINATIONS] Checking port ${PORT}...`);
|
|
108
|
+
execSync(`fuser -k ${PORT}/tcp || true`);
|
|
109
|
+
console.log(`[POLLINATIONS] Port ${PORT} cleared.`);
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
console.log(`[POLLINATIONS] Port check skipped (cmd missing?)`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (e) { }
|
|
116
|
+
server.listen(PORT, '127.0.0.1', () => {
|
|
117
|
+
const url = `http://127.0.0.1:${PORT}`;
|
|
118
|
+
log(`[SERVER] Started V3 Phase 3 (Auth Enabled) on port ${PORT}`);
|
|
119
|
+
console.log(`POLLINATIONS_V3_URL=${url}`);
|
|
120
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
interface OpenAIModel {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
object: "model";
|
|
5
|
+
created: number;
|
|
6
|
+
owned_by: string;
|
|
7
|
+
permission: any[];
|
|
8
|
+
capabilities: {
|
|
9
|
+
failure?: boolean;
|
|
10
|
+
completion?: boolean;
|
|
11
|
+
chat: boolean;
|
|
12
|
+
tools?: boolean;
|
|
13
|
+
};
|
|
14
|
+
context_window?: number;
|
|
15
|
+
description?: string;
|
|
16
|
+
modalities?: {
|
|
17
|
+
input: string[];
|
|
18
|
+
output: string[];
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export interface DetailedUsageEntry {
|
|
22
|
+
timestamp: string;
|
|
23
|
+
type: string;
|
|
24
|
+
model: string;
|
|
25
|
+
api_key: string;
|
|
26
|
+
api_key_type: string;
|
|
27
|
+
meter_source: 'tier' | 'pack';
|
|
28
|
+
input_text_tokens: number;
|
|
29
|
+
input_cached_tokens: number;
|
|
30
|
+
input_audio_tokens: number;
|
|
31
|
+
input_image_tokens: number;
|
|
32
|
+
output_text_tokens: number;
|
|
33
|
+
output_reasoning_tokens: number;
|
|
34
|
+
output_audio_tokens: number;
|
|
35
|
+
output_image_tokens: number;
|
|
36
|
+
cost_usd: number;
|
|
37
|
+
response_time_ms: number;
|
|
38
|
+
}
|
|
39
|
+
export interface DetailedUsageResponse {
|
|
40
|
+
usage: DetailedUsageEntry[];
|
|
41
|
+
count: number;
|
|
42
|
+
}
|
|
43
|
+
export declare function getDetailedUsage(apiKey: string): Promise<DetailedUsageResponse | null>;
|
|
44
|
+
export declare function getAggregatedModels(): Promise<{
|
|
45
|
+
object: string;
|
|
46
|
+
data: OpenAIModel[];
|
|
47
|
+
}>;
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { loadConfig } from './config.js';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
// Debug Helper
|
|
4
|
+
function logDebug(msg) {
|
|
5
|
+
try {
|
|
6
|
+
fs.appendFileSync('/tmp/pollinations-api-debug.log', `[${new Date().toISOString()}] ${msg}\n`);
|
|
7
|
+
}
|
|
8
|
+
catch (e) { }
|
|
9
|
+
}
|
|
10
|
+
const HEADERS = {
|
|
11
|
+
'User-Agent': 'curl/8.5.0',
|
|
12
|
+
'Origin': '',
|
|
13
|
+
'Referer': ''
|
|
14
|
+
};
|
|
15
|
+
function formatName(name, censored) {
|
|
16
|
+
let clean = name.replace(/^pollinations\//, '').replace(/-/g, ' ');
|
|
17
|
+
clean = clean.replace(/\b\w/g, l => l.toUpperCase());
|
|
18
|
+
if (!censored)
|
|
19
|
+
clean += " (Uncensored)";
|
|
20
|
+
return clean;
|
|
21
|
+
}
|
|
22
|
+
// Helper to guess context window if not provided by API
|
|
23
|
+
function getContextWindow(id) {
|
|
24
|
+
const n = id.toLowerCase();
|
|
25
|
+
if (n.includes('128k') || n.includes('gpt-4') || n.includes('turbo'))
|
|
26
|
+
return 128000;
|
|
27
|
+
if (n.includes('gemini') || n.includes('flash') || n.includes('pro'))
|
|
28
|
+
return 1048576;
|
|
29
|
+
return 32768; // Default
|
|
30
|
+
}
|
|
31
|
+
// Fetch Free Models (Public API)
|
|
32
|
+
async function fetchFreeModels() {
|
|
33
|
+
try {
|
|
34
|
+
logDebug("Fetching Free Models (Dynamic Inspection)...");
|
|
35
|
+
const response = await fetch('https://text.pollinations.ai/models', { headers: HEADERS });
|
|
36
|
+
if (!response.ok)
|
|
37
|
+
throw new Error(`${response.status}`);
|
|
38
|
+
const data = await response.json();
|
|
39
|
+
const models = Array.isArray(data) ? data : (data.data || []);
|
|
40
|
+
// Log sample for verification
|
|
41
|
+
if (models.length > 0)
|
|
42
|
+
logDebug(`Sample Free: ${JSON.stringify(models[0])}`);
|
|
43
|
+
return models
|
|
44
|
+
.filter((m) => m.tools === true) // FILTER: Tools Only
|
|
45
|
+
.map((m) => {
|
|
46
|
+
const id = m.name || m.id;
|
|
47
|
+
// Use Description if available, else generated name
|
|
48
|
+
const desc = m.description ? m.description : formatName(id, m.censored);
|
|
49
|
+
const displayName = `Pollinations Free: ${desc}`;
|
|
50
|
+
return {
|
|
51
|
+
id: `pollinations/free/${id}`,
|
|
52
|
+
name: displayName,
|
|
53
|
+
object: "model",
|
|
54
|
+
created: 1700000000,
|
|
55
|
+
owned_by: "pollinations-free",
|
|
56
|
+
permission: [],
|
|
57
|
+
capabilities: { chat: true, completion: true, tools: true },
|
|
58
|
+
context_window: m.context_window || getContextWindow(id),
|
|
59
|
+
description: m.description,
|
|
60
|
+
modalities: { input: ['text'], output: ['text'] } // Improve if 'vision' flag exists in API
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
logDebug(`Error Free: ${e}`);
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Fetch Enterprise Models
|
|
70
|
+
async function fetchEnterpriseModels(apiKey) {
|
|
71
|
+
if (!apiKey || apiKey === 'dummy' || apiKey.length < 5)
|
|
72
|
+
return [];
|
|
73
|
+
try {
|
|
74
|
+
logDebug(`Fetching Enter Models...`);
|
|
75
|
+
const response = await fetch('https://gen.pollinations.ai/models', {
|
|
76
|
+
headers: { 'Authorization': `Bearer ${apiKey}` }
|
|
77
|
+
});
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
logDebug(`Enter API Error: ${response.status}`);
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
const rawData = await response.json();
|
|
83
|
+
const rawModels = Array.isArray(rawData) ? rawData : (rawData.data || []);
|
|
84
|
+
logDebug(`Fetched ${rawModels.length} Enter models.`);
|
|
85
|
+
if (rawModels.length > 0)
|
|
86
|
+
logDebug(`Sample Enter: ${JSON.stringify(rawModels[0])}`);
|
|
87
|
+
return rawModels
|
|
88
|
+
.filter((m) => {
|
|
89
|
+
if (typeof m === 'string')
|
|
90
|
+
return true; // Strings = pass (cant check tools)
|
|
91
|
+
return m.tools === true; // Objects = check tools
|
|
92
|
+
})
|
|
93
|
+
.map((m) => {
|
|
94
|
+
// Enter models might be strings or objects.
|
|
95
|
+
// If string, we can't extract description dynamically -> Fallback formatted name
|
|
96
|
+
const isObj = typeof m === 'object';
|
|
97
|
+
const id = isObj ? (m.id || m.name) : m;
|
|
98
|
+
const desc = (isObj && m.description) ? m.description : formatName(id, true);
|
|
99
|
+
const displayName = `Pollinations Pro: ${desc}`;
|
|
100
|
+
return {
|
|
101
|
+
id: `pollinations/enter/${id}`,
|
|
102
|
+
name: displayName,
|
|
103
|
+
object: "model",
|
|
104
|
+
created: 1700000000,
|
|
105
|
+
owned_by: "pollinations-enter",
|
|
106
|
+
permission: [],
|
|
107
|
+
capabilities: { chat: true, completion: true, tools: true },
|
|
108
|
+
context_window: (isObj && m.context_window) ? m.context_window : getContextWindow(id),
|
|
109
|
+
modalities: { input: ['text'], output: ['text'] }
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
logDebug(`Error Enter: ${e}`);
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export async function getDetailedUsage(apiKey) {
|
|
119
|
+
if (!apiKey || apiKey.length < 10)
|
|
120
|
+
return null;
|
|
121
|
+
try {
|
|
122
|
+
logDebug("Fetching Detailed Usage...");
|
|
123
|
+
const response = await fetch('https://gen.pollinations.ai/account/usage', {
|
|
124
|
+
headers: { 'Authorization': `Bearer ${apiKey}` }
|
|
125
|
+
});
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
logDebug(`Usage API Error: ${response.status}`);
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const data = await response.json();
|
|
131
|
+
// Handle varying response structures if necessary -> Assuming { usage: [...] }
|
|
132
|
+
return data;
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
logDebug(`Error Usage: ${e}`);
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export async function getAggregatedModels() {
|
|
140
|
+
const config = loadConfig();
|
|
141
|
+
const [free, enter] = await Promise.all([
|
|
142
|
+
fetchFreeModels(),
|
|
143
|
+
fetchEnterpriseModels(config.apiKey || '')
|
|
144
|
+
]);
|
|
145
|
+
// Merge: Enter first
|
|
146
|
+
return { object: "list", data: [...enter, ...free] };
|
|
147
|
+
}
|