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
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pollinations.ai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# 🌸 Pollinations AI Plugin for OpenCode (v5.1)
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
<img src="https://avatars.githubusercontent.com/u/88394740?s=400&v=4" alt="Pollinations.ai Logo" width="200">
|
|
5
|
+
<br>
|
|
6
|
+
<b>The Bridge between OpenCode and the Pollinations.ai Ecosystem.</b>
|
|
7
|
+
<br>
|
|
8
|
+
Access unlimited free AI models or premium enterprise models directly within your editor.
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<div align="center">
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+

|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
## 📖 Philosophy: Open AI for Creators
|
|
20
|
+
|
|
21
|
+
> **"No closed doors, no corporate hoops — just good tools and good people."**
|
|
22
|
+
|
|
23
|
+
Pollinations.ai is an open-source platform built by and for the community. We provide a unified API for image, text, audio, and video generation.
|
|
24
|
+
- **Transparent**: Our code, roadmap, and discussions are open.
|
|
25
|
+
- **Community Driven**: Features are prioritized based on what *you* need.
|
|
26
|
+
- **Fair**: One single currency (**Pollen**) for all models. No complex subscriptions.
|
|
27
|
+
|
|
28
|
+
## 📸 Gallery
|
|
29
|
+
|
|
30
|
+
<p align="center">
|
|
31
|
+
<img src="https://github.com/fkom13/opencode-pollinations-plugin/raw/main/docs/images/connect.png" alt="Connect Command" width="800">
|
|
32
|
+
<br>
|
|
33
|
+
<em>Easy Connection with /connect or /pollinations config apiKey</em>
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
<p align="center">
|
|
37
|
+
<img src="https://github.com/fkom13/opencode-pollinations-plugin/raw/main/docs/images/usage_dashboard.png" alt="Usage Dashboard" width="800">
|
|
38
|
+
<br>
|
|
39
|
+
<em>Integrated Usage Dashboard (/pollinations usage)</em>
|
|
40
|
+
</p>
|
|
41
|
+
|
|
42
|
+
<p align="center">
|
|
43
|
+
<img src="https://github.com/fkom13/opencode-pollinations-plugin/raw/main/docs/images/models.png" alt="Models" width="800">
|
|
44
|
+
<br>
|
|
45
|
+
<em>Wide Range of Models (Mistral, OpenAI, Gemini, Claude)</em>
|
|
46
|
+
</p>
|
|
47
|
+
|
|
48
|
+
## ✨ Features
|
|
49
|
+
|
|
50
|
+
- **🌍 Free Universe**: Access generic models (`openai`, `mistral`, `gemini`) for **FREE**, unlimited time, no API key required.
|
|
51
|
+
- **🚀 Pro Mode**: Connect your Pollinations API Key to access Premium Models (`claude-3-opus`, `gpt-4o`, `deepseek-coder`).
|
|
52
|
+
- **🛡️ Safety Net V5**: never get blocked.
|
|
53
|
+
- **Transparent Fallback**: If your Pro quota runs out mid-chat, the plugin automatically switches to a free model instantly. No errors, just a seamless experience.
|
|
54
|
+
- **📊 Real-time Dashboard**: Track your **Pollen** usage, Tier Status, and Wallet Balance inside OpenCode.
|
|
55
|
+
|
|
56
|
+
## 🐝 Understanding Pollen & Tiers
|
|
57
|
+
|
|
58
|
+
**Pollen** is our unified credit system. $1 ≈ 1 Pollen.
|
|
59
|
+
You spend it to verify API calls on premium models.
|
|
60
|
+
|
|
61
|
+
### Tiers (Free Daily Grants during Beta)
|
|
62
|
+
|
|
63
|
+
| Tier | Grant | Requirement |
|
|
64
|
+
| :--- | :--- | :--- |
|
|
65
|
+
| **🦠 Spore** | **1 Pollen/day** | Just Sign Up! |
|
|
66
|
+
| **🌱 Seed** | **3 Pollen/day** | Active GitHub Developer (8+ points) |
|
|
67
|
+
| **🌸 Flower** | **10 Pollen/day** | **Publish an App** (Like this Plugin!) |
|
|
68
|
+
| **🍯 Nectar** | **20 Pollen/day** | Major Contributors (Coming Soon) |
|
|
69
|
+
|
|
70
|
+
> 🎁 **Beta Bonus**: Buy one Pollen pack, get one free!
|
|
71
|
+
|
|
72
|
+
## 📦 Installation & Ecology
|
|
73
|
+
|
|
74
|
+
This plugin is part of the **OpenCode Ecosystem**.
|
|
75
|
+
|
|
76
|
+
### Option 1: NPM (Standard Installation)
|
|
77
|
+
This is the native method for OpenCode CLI.
|
|
78
|
+
|
|
79
|
+
1. Install the plugin globally:
|
|
80
|
+
```bash
|
|
81
|
+
npm install -g opencode-pollinations-plugin
|
|
82
|
+
```
|
|
83
|
+
2. Register it in your OpenCode configuration (e.g., `~/.config/opencode/opencode.json`):
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"plugin": [
|
|
87
|
+
"opencode-pollinations-plugin"
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Option 2: Local Development (Source)
|
|
93
|
+
If you are developing the plugin:
|
|
94
|
+
1. Clone the repo.
|
|
95
|
+
2. Link it globally:
|
|
96
|
+
```bash
|
|
97
|
+
cd opencode-pollinations-plugin
|
|
98
|
+
npm link
|
|
99
|
+
```
|
|
100
|
+
3. Use the provided script (`run_dev.sh`) which automatically injects the plugin for testing.
|
|
101
|
+
|
|
102
|
+
## 🚀 Publication (The "Registry")
|
|
103
|
+
OpenCode uses NPM as its registry. To publish:
|
|
104
|
+
|
|
105
|
+
1. **Publish to NPM**:
|
|
106
|
+
```bash
|
|
107
|
+
npm login
|
|
108
|
+
npm publish
|
|
109
|
+
```
|
|
110
|
+
2. **Join Ecosystem**: Submit a Pull Request to [OpenCode Ecosystem](https://github.com/opencode-ai/ecosystem) to list your plugin officially.
|
|
111
|
+
*Once accepted, users can find it via documentation or future registry commands.*
|
|
112
|
+
|
|
113
|
+
## 🚀 Getting Started
|
|
114
|
+
|
|
115
|
+
### 1. The Basics (Free Mode)
|
|
116
|
+
Just type in the chat. You are in **Manual Mode** by default.
|
|
117
|
+
- Model: `openai` (GPT-4o Mini equivalent)
|
|
118
|
+
- Model: `mistral` (Mistral Nemo)
|
|
119
|
+
|
|
120
|
+
### 2. Going Pro
|
|
121
|
+
1. Get your API Key from [pollinations.ai](https://pollinations.ai).
|
|
122
|
+
2. Run command:
|
|
123
|
+
```bash
|
|
124
|
+
/pollinations config apiKey sk_YourSecretKey
|
|
125
|
+
```
|
|
126
|
+
3. Set mode to Pro:
|
|
127
|
+
```bash
|
|
128
|
+
/pollinations mode pro
|
|
129
|
+
```
|
|
130
|
+
4. **Reload Window**.
|
|
131
|
+
|
|
132
|
+
## 🔗 Links
|
|
133
|
+
|
|
134
|
+
- **Pollinations Website**: [pollinations.ai](https://pollinations.ai)
|
|
135
|
+
- **Discord Community**: [Join us!](https://discord.gg/pollinations-ai-885844321461485618)
|
|
136
|
+
- **OpenCode Ecosystem**: [opencode.ai](https://opencode.ai/docs/ecosystem#plugins)
|
|
137
|
+
|
|
138
|
+
## 📜 License
|
|
139
|
+
|
|
140
|
+
MIT License. Created by [fkom13](https://github.com/fkom13) & The Pollinations Community.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import * as http from 'http';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { generatePollinationsConfig } from './server/generate-config.js';
|
|
5
|
+
import { loadConfig } from './server/config.js';
|
|
6
|
+
import { handleChatCompletion } from './server/proxy.js';
|
|
7
|
+
import { createToastHooks, setGlobalClient } from './server/toast.js';
|
|
8
|
+
import { createStatusHooks } from './server/status.js';
|
|
9
|
+
import { createCommandHooks } from './server/commands.js';
|
|
10
|
+
const LOG_FILE = '/tmp/opencode_pollinations_v4.log';
|
|
11
|
+
function log(msg) {
|
|
12
|
+
try {
|
|
13
|
+
fs.appendFileSync(LOG_FILE, `[${new Date().toISOString()}] ${msg}\n`);
|
|
14
|
+
}
|
|
15
|
+
catch (e) { }
|
|
16
|
+
}
|
|
17
|
+
const TRACKING_PORT = 10001;
|
|
18
|
+
// === ANTI-ZOMBIE ATOMIC CLEANUP ===
|
|
19
|
+
try {
|
|
20
|
+
log(`[Init] Checking port ${TRACKING_PORT} for zombies...`);
|
|
21
|
+
execSync(`fuser -k ${TRACKING_PORT}/tcp || true`);
|
|
22
|
+
log(`[Init] Port ${TRACKING_PORT} cleaned.`);
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
log(`[Init] Zombie cleanup warning: ${e}`);
|
|
26
|
+
}
|
|
27
|
+
// === GESTION DU CYCLE DE VIE PROXY ===
|
|
28
|
+
const startProxy = () => {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
const server = http.createServer(async (req, res) => {
|
|
31
|
+
log(`[Proxy] Request: ${req.method} ${req.url}`);
|
|
32
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
33
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
34
|
+
res.setHeader('Access-Control-Allow-Headers', '*');
|
|
35
|
+
if (req.method === 'OPTIONS') {
|
|
36
|
+
res.writeHead(204);
|
|
37
|
+
res.end();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (req.method === 'GET' && req.url === '/health') {
|
|
41
|
+
const config = loadConfig();
|
|
42
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
43
|
+
res.end(JSON.stringify({
|
|
44
|
+
status: "ok",
|
|
45
|
+
version: "v4.0.5",
|
|
46
|
+
mode: config.mode
|
|
47
|
+
}));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// SUPPORT FLEXIBLE DES PATHS
|
|
51
|
+
// Le SDK peut envoyer /v1/chat/completions ou juste /chat/completions
|
|
52
|
+
if (req.method === 'POST' && (req.url === '/v1/chat/completions' || req.url === '/chat/completions')) {
|
|
53
|
+
const chunks = [];
|
|
54
|
+
req.on('data', chunk => chunks.push(chunk));
|
|
55
|
+
req.on('end', async () => {
|
|
56
|
+
try {
|
|
57
|
+
const bodyRaw = Buffer.concat(chunks).toString();
|
|
58
|
+
await handleChatCompletion(req, res, bodyRaw);
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
log(`Error: ${e}`);
|
|
62
|
+
if (!res.headersSent) {
|
|
63
|
+
res.writeHead(500);
|
|
64
|
+
res.end(JSON.stringify({ error: String(e) }));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
log(`[Proxy] 404 Not Found for ${req.url}`);
|
|
71
|
+
res.writeHead(404);
|
|
72
|
+
res.end("Not Found");
|
|
73
|
+
});
|
|
74
|
+
server.listen(TRACKING_PORT, '127.0.0.1', () => {
|
|
75
|
+
log(`[Proxy] Started V4 on port ${TRACKING_PORT}`);
|
|
76
|
+
resolve(TRACKING_PORT);
|
|
77
|
+
});
|
|
78
|
+
server.on('error', (e) => {
|
|
79
|
+
log(`[Proxy] Fatal Error: ${e}`);
|
|
80
|
+
resolve(0);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
// === PLUGIN EXPORT ===
|
|
85
|
+
export const PollinationsPlugin = async (ctx) => {
|
|
86
|
+
log("Plugin Initializing V4.0.5 (Path Fix)...");
|
|
87
|
+
try {
|
|
88
|
+
log(`[DEBUG] CTX Keys: ${Object.keys(ctx).join(', ')}`);
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
log(`[DEBUG] Error inspecting ctx: ${e}`);
|
|
92
|
+
}
|
|
93
|
+
const port = await startProxy();
|
|
94
|
+
// IMPORTANT: On ajoute /v1 à la base URL pour guider le SDK,
|
|
95
|
+
// mais le proxy accepte aussi sans.
|
|
96
|
+
const localBaseUrl = `http://127.0.0.1:${port}/v1`;
|
|
97
|
+
setGlobalClient(ctx.client); // REGISTER CLIENT FOR IMMEDIATE TOASTS
|
|
98
|
+
const toastHooks = createToastHooks(ctx.client);
|
|
99
|
+
const commandHooks = createCommandHooks();
|
|
100
|
+
return {
|
|
101
|
+
async config(config) {
|
|
102
|
+
log("[Hook] config() called");
|
|
103
|
+
const modelsArray = await generatePollinationsConfig();
|
|
104
|
+
const modelsObj = {};
|
|
105
|
+
for (const m of modelsArray) {
|
|
106
|
+
// Ensure ID is relative for mapping ("free/gemini")
|
|
107
|
+
// BUT Provider needs full ID ? No, the object key is the relative ID
|
|
108
|
+
// OpenCode: provider.models[id]
|
|
109
|
+
// id comes from generatePollinationsConfig which returns "free/gemini"
|
|
110
|
+
// So modelsObj["free/gemini"] = ... matches.
|
|
111
|
+
modelsObj[m.id] = m;
|
|
112
|
+
}
|
|
113
|
+
if (!config.provider)
|
|
114
|
+
config.provider = {};
|
|
115
|
+
config.provider['pollinations'] = {
|
|
116
|
+
id: 'pollinations',
|
|
117
|
+
name: 'Pollinations V5.1 (Native)',
|
|
118
|
+
options: { baseURL: localBaseUrl },
|
|
119
|
+
models: modelsObj
|
|
120
|
+
};
|
|
121
|
+
log(`[Hook] Registered ${Object.keys(modelsObj).length} models.`);
|
|
122
|
+
},
|
|
123
|
+
...toastHooks,
|
|
124
|
+
...createStatusHooks(ctx.client), // New Status Bar Logic
|
|
125
|
+
...commandHooks
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
export default PollinationsPlugin;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const createPollinationsFetch: (apiKey: string) => (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Removed invalid imports
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
// --- Sanitization Helpers (Ported from Gateway/Upstream) ---
|
|
4
|
+
function safeId(id) {
|
|
5
|
+
if (!id)
|
|
6
|
+
return id;
|
|
7
|
+
if (id.length > 30)
|
|
8
|
+
return id.substring(0, 30);
|
|
9
|
+
return id;
|
|
10
|
+
}
|
|
11
|
+
function logDebug(message, data) {
|
|
12
|
+
try {
|
|
13
|
+
const timestamp = new Date().toISOString();
|
|
14
|
+
let logMsg = `[${timestamp}] ${message}`;
|
|
15
|
+
if (data) {
|
|
16
|
+
logMsg += `\n${JSON.stringify(data, null, 2)}`;
|
|
17
|
+
}
|
|
18
|
+
fs.appendFileSync('/tmp/opencode_pollinations_debug.log', logMsg + '\n\n');
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
// ignore logging errors
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function sanitizeTools(tools) {
|
|
25
|
+
if (!Array.isArray(tools))
|
|
26
|
+
return tools;
|
|
27
|
+
const cleanSchema = (schema) => {
|
|
28
|
+
if (!schema || typeof schema !== "object")
|
|
29
|
+
return;
|
|
30
|
+
if (schema.optional !== undefined)
|
|
31
|
+
delete schema.optional;
|
|
32
|
+
if (schema.ref !== undefined)
|
|
33
|
+
delete schema.ref;
|
|
34
|
+
if (schema["$ref"] !== undefined)
|
|
35
|
+
delete schema["$ref"];
|
|
36
|
+
if (schema.properties) {
|
|
37
|
+
for (const key in schema.properties)
|
|
38
|
+
cleanSchema(schema.properties[key]);
|
|
39
|
+
}
|
|
40
|
+
if (schema.items)
|
|
41
|
+
cleanSchema(schema.items);
|
|
42
|
+
};
|
|
43
|
+
return tools.map((tool) => {
|
|
44
|
+
const newTool = { ...tool };
|
|
45
|
+
if (newTool.function && newTool.function.parameters) {
|
|
46
|
+
cleanSchema(newTool.function.parameters);
|
|
47
|
+
}
|
|
48
|
+
return newTool;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function filterTools(tools, maxCount = 120) {
|
|
52
|
+
if (!Array.isArray(tools))
|
|
53
|
+
return [];
|
|
54
|
+
if (tools.length <= maxCount)
|
|
55
|
+
return tools;
|
|
56
|
+
const priorities = [
|
|
57
|
+
"bash", "read", "write", "edit", "webfetch", "glob", "grep",
|
|
58
|
+
"searxng_remote_search", "deepsearch_deep_search", "google_search",
|
|
59
|
+
"task", "todowrite"
|
|
60
|
+
];
|
|
61
|
+
const priorityTools = tools.filter((t) => priorities.includes(t.function.name));
|
|
62
|
+
const otherTools = tools.filter((t) => !priorities.includes(t.function.name));
|
|
63
|
+
const slotsLeft = maxCount - priorityTools.length;
|
|
64
|
+
const othersKept = otherTools.slice(0, Math.max(0, slotsLeft));
|
|
65
|
+
logDebug(`[POLLI-PLUGIN] Filtering tools: ${tools.length} -> ${priorityTools.length + othersKept.length}`);
|
|
66
|
+
return [...priorityTools, ...othersKept];
|
|
67
|
+
}
|
|
68
|
+
// --- Fetch Implementation ---
|
|
69
|
+
export const createPollinationsFetch = (apiKey) => async (input, init) => {
|
|
70
|
+
let url = input.toString();
|
|
71
|
+
const options = init || {};
|
|
72
|
+
let body = null;
|
|
73
|
+
if (options.body && typeof options.body === "string") {
|
|
74
|
+
try {
|
|
75
|
+
body = JSON.parse(options.body);
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
// Not JSON, ignore
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// --- INTERCEPTION & SANITIZATION ---
|
|
82
|
+
if (body) {
|
|
83
|
+
let model = body.model || "";
|
|
84
|
+
// 0. Model Name Normalization
|
|
85
|
+
if (typeof model === "string" && model.startsWith("pollinations/enter/")) {
|
|
86
|
+
body.model = model.replace("pollinations/enter/", "");
|
|
87
|
+
model = body.model;
|
|
88
|
+
}
|
|
89
|
+
// FIX: Remove stream_options (causes 400 on some OpenAI proxies)
|
|
90
|
+
if (body.stream_options) {
|
|
91
|
+
delete body.stream_options;
|
|
92
|
+
}
|
|
93
|
+
// 1. Azure Tool Limit Fix
|
|
94
|
+
if ((model.includes("openai") || model.includes("gpt")) && body.tools) {
|
|
95
|
+
if (body.tools.length > 120) {
|
|
96
|
+
body.tools = filterTools(body.tools, 120);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// 2. Vertex/Gemini Schema Fix
|
|
100
|
+
if (model.includes("gemini") && body.tools) {
|
|
101
|
+
body.tools = sanitizeTools(body.tools);
|
|
102
|
+
}
|
|
103
|
+
// Re-serialize body
|
|
104
|
+
options.body = JSON.stringify(body);
|
|
105
|
+
}
|
|
106
|
+
// Ensure Headers
|
|
107
|
+
const headers = new Headers(options.headers || {});
|
|
108
|
+
headers.set("Authorization", `Bearer ${apiKey}`);
|
|
109
|
+
headers.set("Content-Type", "application/json");
|
|
110
|
+
options.headers = headers;
|
|
111
|
+
logDebug(`Req: ${url}`, body);
|
|
112
|
+
try {
|
|
113
|
+
const response = await global.fetch(url, options);
|
|
114
|
+
// Log response status
|
|
115
|
+
// We clone to read text for debugging errors
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
try {
|
|
118
|
+
const clone = response.clone();
|
|
119
|
+
const text = await clone.text();
|
|
120
|
+
logDebug(`Res (Error): ${response.status}`, text);
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
logDebug(`Res (Error): ${response.status} (Read failed)`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
logDebug(`Res (OK): ${response.status}`);
|
|
128
|
+
}
|
|
129
|
+
return response;
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
logDebug(`Fetch Error: ${e.message}`);
|
|
133
|
+
throw e;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const createPollinationsFetch: (apiKey: string) => (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Removed invalid imports
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
// --- Sanitization Helpers (Ported from Gateway/Upstream) ---
|
|
4
|
+
function safeId(id) {
|
|
5
|
+
if (!id)
|
|
6
|
+
return id;
|
|
7
|
+
if (id.length > 30)
|
|
8
|
+
return id.substring(0, 30);
|
|
9
|
+
return id;
|
|
10
|
+
}
|
|
11
|
+
function logDebug(message, data) {
|
|
12
|
+
try {
|
|
13
|
+
const timestamp = new Date().toISOString();
|
|
14
|
+
let logMsg = `[${timestamp}] ${message}`;
|
|
15
|
+
if (data) {
|
|
16
|
+
logMsg += `\n${JSON.stringify(data, null, 2)}`;
|
|
17
|
+
}
|
|
18
|
+
fs.appendFileSync('/tmp/opencode_pollinations_debug.log', logMsg + '\n\n');
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
// ignore logging errors
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function sanitizeTools(tools) {
|
|
25
|
+
if (!Array.isArray(tools))
|
|
26
|
+
return tools;
|
|
27
|
+
const cleanSchema = (schema) => {
|
|
28
|
+
if (!schema || typeof schema !== "object")
|
|
29
|
+
return;
|
|
30
|
+
if (schema.optional !== undefined)
|
|
31
|
+
delete schema.optional;
|
|
32
|
+
if (schema.ref !== undefined)
|
|
33
|
+
delete schema.ref;
|
|
34
|
+
if (schema["$ref"] !== undefined)
|
|
35
|
+
delete schema["$ref"];
|
|
36
|
+
if (schema.properties) {
|
|
37
|
+
for (const key in schema.properties)
|
|
38
|
+
cleanSchema(schema.properties[key]);
|
|
39
|
+
}
|
|
40
|
+
if (schema.items)
|
|
41
|
+
cleanSchema(schema.items);
|
|
42
|
+
};
|
|
43
|
+
return tools.map((tool) => {
|
|
44
|
+
const newTool = { ...tool };
|
|
45
|
+
if (newTool.function && newTool.function.parameters) {
|
|
46
|
+
cleanSchema(newTool.function.parameters);
|
|
47
|
+
}
|
|
48
|
+
return newTool;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function filterTools(tools, maxCount = 120) {
|
|
52
|
+
if (!Array.isArray(tools))
|
|
53
|
+
return [];
|
|
54
|
+
if (tools.length <= maxCount)
|
|
55
|
+
return tools;
|
|
56
|
+
const priorities = [
|
|
57
|
+
"bash", "read", "write", "edit", "webfetch", "glob", "grep",
|
|
58
|
+
"searxng_remote_search", "deepsearch_deep_search", "google_search",
|
|
59
|
+
"task", "todowrite"
|
|
60
|
+
];
|
|
61
|
+
const priorityTools = tools.filter((t) => priorities.includes(t.function.name));
|
|
62
|
+
const otherTools = tools.filter((t) => !priorities.includes(t.function.name));
|
|
63
|
+
const slotsLeft = maxCount - priorityTools.length;
|
|
64
|
+
const othersKept = otherTools.slice(0, Math.max(0, slotsLeft));
|
|
65
|
+
logDebug(`[POLLI-PLUGIN] Filtering tools: ${tools.length} -> ${priorityTools.length + othersKept.length}`);
|
|
66
|
+
return [...priorityTools, ...othersKept];
|
|
67
|
+
}
|
|
68
|
+
// --- Fetch Implementation ---
|
|
69
|
+
export const createPollinationsFetch = (apiKey) => async (input, init) => {
|
|
70
|
+
let url = input.toString();
|
|
71
|
+
const options = init || {};
|
|
72
|
+
let body = null;
|
|
73
|
+
if (options.body && typeof options.body === "string") {
|
|
74
|
+
try {
|
|
75
|
+
body = JSON.parse(options.body);
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
// Not JSON, ignore
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// --- INTERCEPTION & SANITIZATION ---
|
|
82
|
+
if (body) {
|
|
83
|
+
let model = body.model || "";
|
|
84
|
+
// 0. Model Name Normalization
|
|
85
|
+
if (typeof model === "string" && model.startsWith("pollinations/enter/")) {
|
|
86
|
+
body.model = model.replace("pollinations/enter/", "");
|
|
87
|
+
model = body.model;
|
|
88
|
+
}
|
|
89
|
+
// FIX: Remove stream_options (causes 400 on some OpenAI proxies)
|
|
90
|
+
if (body.stream_options) {
|
|
91
|
+
delete body.stream_options;
|
|
92
|
+
}
|
|
93
|
+
// 1. Azure Tool Limit Fix
|
|
94
|
+
if ((model.includes("openai") || model.includes("gpt")) && body.tools) {
|
|
95
|
+
if (body.tools.length > 120) {
|
|
96
|
+
body.tools = filterTools(body.tools, 120);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// 2. Vertex/Gemini Schema Fix
|
|
100
|
+
if (model.includes("gemini") && body.tools) {
|
|
101
|
+
body.tools = sanitizeTools(body.tools);
|
|
102
|
+
}
|
|
103
|
+
// Re-serialize body
|
|
104
|
+
options.body = JSON.stringify(body);
|
|
105
|
+
}
|
|
106
|
+
// Ensure Headers
|
|
107
|
+
const headers = new Headers(options.headers || {});
|
|
108
|
+
headers.set("Authorization", `Bearer ${apiKey}`);
|
|
109
|
+
headers.set("Content-Type", "application/json");
|
|
110
|
+
options.headers = headers;
|
|
111
|
+
logDebug(`Req: ${url}`, body);
|
|
112
|
+
try {
|
|
113
|
+
const response = await global.fetch(url, options);
|
|
114
|
+
// Log response status
|
|
115
|
+
// We clone to read text for debugging errors
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
try {
|
|
118
|
+
const clone = response.clone();
|
|
119
|
+
const text = await clone.text();
|
|
120
|
+
logDebug(`Res (Error): ${response.status}`, text);
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
logDebug(`Res (Error): ${response.status} (Read failed)`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
logDebug(`Res (OK): ${response.status}`);
|
|
128
|
+
}
|
|
129
|
+
return response;
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
logDebug(`Fetch Error: ${e.message}`);
|
|
133
|
+
throw e;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface CommandResult {
|
|
2
|
+
handled: boolean;
|
|
3
|
+
response?: string;
|
|
4
|
+
error?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function handleCommand(command: string): Promise<CommandResult>;
|
|
7
|
+
export declare function createCommandHooks(): {
|
|
8
|
+
'tui.command.execute': (input: any, output: any) => Promise<void>;
|
|
9
|
+
};
|
|
10
|
+
export {};
|