opencode-pollinations-plugin 6.1.0-beta.8 → 6.2.0
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 +130 -0
- package/README.es.md +130 -0
- package/README.fr.md +130 -0
- package/README.it.md +130 -0
- package/README.md +87 -73
- package/dist/index.js +52 -161
- package/dist/locales/de.json +374 -0
- package/dist/locales/en.json +373 -0
- package/dist/locales/es.json +374 -0
- package/dist/locales/fr.json +373 -0
- package/dist/locales/index.d.ts +1 -0
- package/dist/locales/index.js +37 -0
- package/dist/locales/it.json +374 -0
- package/dist/server/commands.d.ts +6 -0
- package/dist/server/commands.js +394 -125
- package/dist/server/config.d.ts +34 -23
- package/dist/server/config.js +200 -108
- package/dist/server/connect-response.d.ts +2 -0
- package/dist/server/connect-response.js +59 -0
- package/dist/server/generate-config.d.ts +3 -30
- package/dist/server/generate-config.js +164 -106
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +124 -149
- package/dist/server/logger.d.ts +8 -0
- package/dist/server/logger.js +38 -0
- package/dist/server/models/cache.d.ts +35 -0
- package/dist/server/models/cache.js +160 -0
- package/dist/server/models/fetcher.d.ts +18 -0
- package/dist/server/models/fetcher.js +194 -0
- package/dist/server/models/index.d.ts +6 -0
- package/dist/server/models/index.js +5 -0
- package/dist/server/models/manual.d.ts +15 -0
- package/dist/server/models/manual.js +92 -0
- package/dist/server/models/types.d.ts +55 -0
- package/dist/server/models/types.js +7 -0
- package/dist/server/models/worker.d.ts +22 -0
- package/dist/server/models/worker.js +174 -0
- package/dist/server/pollinations-api.d.ts +11 -0
- package/dist/server/pollinations-api.js +21 -8
- package/dist/server/proxy.js +222 -293
- package/dist/server/quota.d.ts +2 -0
- package/dist/server/quota.js +89 -86
- package/dist/server/scripts/pollinations_pricing.d.ts +8 -0
- package/dist/server/scripts/pollinations_pricing.js +246 -0
- package/dist/server/scripts/test_cost_endpoints.d.ts +1 -0
- package/dist/server/scripts/test_cost_endpoints.js +61 -0
- package/dist/server/scripts/test_dynamic_pricing.d.ts +1 -0
- package/dist/server/scripts/test_dynamic_pricing.js +39 -0
- package/dist/server/scripts/test_freetier_audit.d.ts +11 -0
- package/dist/server/scripts/test_freetier_audit.js +215 -0
- package/dist/server/scripts/test_parallel_cost.d.ts +1 -0
- package/dist/server/scripts/test_parallel_cost.js +104 -0
- package/dist/server/toast.d.ts +7 -1
- package/dist/server/toast.js +43 -10
- package/dist/tools/design/gen_diagram.d.ts +2 -0
- package/dist/tools/design/gen_diagram.js +94 -0
- package/dist/tools/design/gen_palette.d.ts +2 -0
- package/dist/tools/design/gen_palette.js +182 -0
- package/dist/tools/design/gen_qrcode.d.ts +2 -0
- package/dist/tools/design/gen_qrcode.js +50 -0
- package/dist/tools/ffmpeg.d.ts +24 -0
- package/dist/tools/ffmpeg.js +54 -0
- package/dist/tools/index.d.ts +25 -0
- package/dist/tools/index.js +86 -0
- package/dist/tools/pollinations/beta_discovery.d.ts +9 -0
- package/dist/tools/pollinations/beta_discovery.js +201 -0
- package/dist/tools/pollinations/cost-guard.d.ts +38 -0
- package/dist/tools/pollinations/cost-guard.js +136 -0
- package/dist/tools/pollinations/deepsearch.d.ts +7 -0
- package/dist/tools/pollinations/deepsearch.js +80 -0
- package/dist/tools/pollinations/gen_audio.d.ts +18 -0
- package/dist/tools/pollinations/gen_audio.js +220 -0
- package/dist/tools/pollinations/gen_image.d.ts +11 -0
- package/dist/tools/pollinations/gen_image.js +211 -0
- package/dist/tools/pollinations/gen_music.d.ts +14 -0
- package/dist/tools/pollinations/gen_music.js +157 -0
- package/dist/tools/pollinations/gen_video.d.ts +16 -0
- package/dist/tools/pollinations/gen_video.js +249 -0
- package/dist/tools/pollinations/polli_config.d.ts +2 -0
- package/dist/tools/pollinations/polli_config.js +95 -0
- package/dist/tools/pollinations/polli_gen_confirm.d.ts +2 -0
- package/dist/tools/pollinations/polli_gen_confirm.js +48 -0
- package/dist/tools/pollinations/polli_status.d.ts +2 -0
- package/dist/tools/pollinations/polli_status.js +31 -0
- package/dist/tools/pollinations/polli_web_search.d.ts +15 -0
- package/dist/tools/pollinations/polli_web_search.js +126 -0
- package/dist/tools/pollinations/search_crawl_scrape.d.ts +7 -0
- package/dist/tools/pollinations/search_crawl_scrape.js +85 -0
- package/dist/tools/pollinations/shared.d.ts +181 -0
- package/dist/tools/pollinations/shared.js +758 -0
- package/dist/tools/pollinations/test_estimators.d.ts +1 -0
- package/dist/tools/pollinations/test_estimators.js +22 -0
- package/dist/tools/pollinations/transcribe_audio.d.ts +13 -0
- package/dist/tools/pollinations/transcribe_audio.js +171 -0
- package/dist/tools/power/extract_audio.d.ts +2 -0
- package/dist/tools/power/extract_audio.js +179 -0
- package/dist/tools/power/extract_frames.d.ts +2 -0
- package/dist/tools/power/extract_frames.js +237 -0
- package/dist/tools/power/file_to_url.d.ts +2 -0
- package/dist/tools/power/file_to_url.js +217 -0
- package/dist/tools/power/remove_background.d.ts +2 -0
- package/dist/tools/power/remove_background.js +404 -0
- package/dist/tools/power/rmbg_keys.d.ts +2 -0
- package/dist/tools/power/rmbg_keys.js +79 -0
- package/dist/tools/shared.d.ts +30 -0
- package/dist/tools/shared.js +80 -0
- package/package.json +10 -4
- package/dist/server/models-seed.d.ts +0 -18
- package/dist/server/models-seed.js +0 -55
package/dist/server/proxy.js
CHANGED
|
@@ -3,26 +3,22 @@ import * as path from 'path';
|
|
|
3
3
|
import { loadConfig, saveConfig } from './config.js';
|
|
4
4
|
import { handleCommand } from './commands.js';
|
|
5
5
|
import { emitStatusToast, emitLogToast } from './toast.js';
|
|
6
|
+
import { buildConnectResponse } from './connect-response.js';
|
|
7
|
+
import { log } from './logger.js';
|
|
8
|
+
import { getConfigDir } from './config.js';
|
|
9
|
+
import { t } from '../locales/index.js';
|
|
6
10
|
// --- PERSISTENCE: SIGNATURE MAP (Multi-Round Support) ---
|
|
7
|
-
const SIG_FILE = path.join(
|
|
11
|
+
const SIG_FILE = path.join(getConfigDir(), 'pollinations-signature.json');
|
|
8
12
|
let signatureMap = {};
|
|
9
13
|
let lastSignature = null; // V1 Fallback Global
|
|
10
|
-
function log(msg) {
|
|
11
|
-
try {
|
|
12
|
-
const ts = new Date().toISOString();
|
|
13
|
-
if (!fs.existsSync('/tmp/opencode_pollinations_debug.log')) {
|
|
14
|
-
fs.writeFileSync('/tmp/opencode_pollinations_debug.log', '');
|
|
15
|
-
}
|
|
16
|
-
fs.appendFileSync('/tmp/opencode_pollinations_debug.log', `[Proxy] ${ts} ${msg}\n`);
|
|
17
|
-
}
|
|
18
|
-
catch (e) { }
|
|
19
|
-
}
|
|
20
14
|
try {
|
|
21
15
|
if (fs.existsSync(SIG_FILE)) {
|
|
22
16
|
signatureMap = JSON.parse(fs.readFileSync(SIG_FILE, 'utf-8'));
|
|
23
17
|
}
|
|
24
18
|
}
|
|
25
|
-
catch (e) {
|
|
19
|
+
catch (e) {
|
|
20
|
+
log(`[Proxy Signature] Error loading: ${e}`);
|
|
21
|
+
}
|
|
26
22
|
function saveSignatureMap() {
|
|
27
23
|
try {
|
|
28
24
|
if (!fs.existsSync(path.dirname(SIG_FILE)))
|
|
@@ -78,6 +74,23 @@ function dereferenceSchema(schema, rootDefs) {
|
|
|
78
74
|
schema.description = (schema.description || "") + " [Ref Failed]";
|
|
79
75
|
}
|
|
80
76
|
}
|
|
77
|
+
// VERTEX FIX: 'const' not supported -> convert to 'enum'
|
|
78
|
+
if (schema.const !== undefined) {
|
|
79
|
+
schema.enum = [schema.const];
|
|
80
|
+
delete schema.const;
|
|
81
|
+
}
|
|
82
|
+
// VERTEX FIX: 'anyOf' must be exclusive (no other siblings)
|
|
83
|
+
if (schema.anyOf || schema.oneOf) {
|
|
84
|
+
// Vertex demands strict exclusivity.
|
|
85
|
+
// We keep 'definitions'/'$defs' if present at root (though unlikely here)
|
|
86
|
+
// But for a property node, we must strip EVERYTHING else.
|
|
87
|
+
const keys = Object.keys(schema);
|
|
88
|
+
keys.forEach(k => {
|
|
89
|
+
if (k !== 'anyOf' && k !== 'oneOf' && k !== 'definitions' && k !== '$defs') {
|
|
90
|
+
delete schema[k];
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
81
94
|
if (schema.properties) {
|
|
82
95
|
for (const key in schema.properties) {
|
|
83
96
|
schema.properties[key] = dereferenceSchema(schema.properties[key], rootDefs);
|
|
@@ -86,6 +99,15 @@ function dereferenceSchema(schema, rootDefs) {
|
|
|
86
99
|
if (schema.items) {
|
|
87
100
|
schema.items = dereferenceSchema(schema.items, rootDefs);
|
|
88
101
|
}
|
|
102
|
+
if (schema.anyOf) {
|
|
103
|
+
schema.anyOf = schema.anyOf.map((s) => dereferenceSchema(s, rootDefs));
|
|
104
|
+
}
|
|
105
|
+
if (schema.oneOf) {
|
|
106
|
+
schema.oneOf = schema.oneOf.map((s) => dereferenceSchema(s, rootDefs));
|
|
107
|
+
}
|
|
108
|
+
if (schema.allOf) {
|
|
109
|
+
schema.allOf = schema.allOf.map((s) => dereferenceSchema(s, rootDefs));
|
|
110
|
+
}
|
|
89
111
|
if (schema.optional !== undefined)
|
|
90
112
|
delete schema.optional;
|
|
91
113
|
if (schema.title)
|
|
@@ -107,127 +129,55 @@ function sanitizeToolsForVertex(tools) {
|
|
|
107
129
|
return tool;
|
|
108
130
|
});
|
|
109
131
|
}
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
// --- BEDROCK COMPATIBILITY SHIM ---
|
|
116
|
-
// Bedrock requires:
|
|
117
|
-
// 1. toolUseId matching [a-zA-Z0-9_-]+ (no dots, spaces, special chars)
|
|
118
|
-
// 2. toolConfig present when toolUse/toolResult in messages
|
|
119
|
-
function sanitizeToolUseId(id) {
|
|
120
|
-
// Replace any char not in [a-zA-Z0-9_-] with underscore
|
|
121
|
-
return id.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
122
|
-
}
|
|
123
|
-
function sanitizeForBedrock(body) {
|
|
124
|
-
if (!body)
|
|
125
|
-
return body;
|
|
126
|
-
const sanitized = JSON.parse(JSON.stringify(body)); // Deep clone
|
|
127
|
-
// 1. Sanitize toolUseId in all messages AND detect tool history
|
|
128
|
-
let hasToolHistory = false;
|
|
129
|
-
const toolNamesFromHistory = [];
|
|
130
|
-
if (sanitized.messages && Array.isArray(sanitized.messages)) {
|
|
131
|
-
for (const msg of sanitized.messages) {
|
|
132
|
-
if (msg.content && Array.isArray(msg.content)) {
|
|
133
|
-
for (const block of msg.content) {
|
|
134
|
-
// Handle toolUse blocks
|
|
135
|
-
if (block.toolUse && block.toolUse.toolUseId) {
|
|
136
|
-
block.toolUse.toolUseId = sanitizeToolUseId(block.toolUse.toolUseId);
|
|
137
|
-
hasToolHistory = true;
|
|
138
|
-
if (block.toolUse.name)
|
|
139
|
-
toolNamesFromHistory.push(block.toolUse.name);
|
|
140
|
-
}
|
|
141
|
-
// Handle toolResult blocks
|
|
142
|
-
if (block.toolResult && block.toolResult.toolUseId) {
|
|
143
|
-
block.toolResult.toolUseId = sanitizeToolUseId(block.toolResult.toolUseId);
|
|
144
|
-
hasToolHistory = true;
|
|
145
|
-
}
|
|
146
|
-
// Handle OpenAI-style tool_use in content array
|
|
147
|
-
if (block.type === 'tool_use' && block.id) {
|
|
148
|
-
block.id = sanitizeToolUseId(block.id);
|
|
149
|
-
hasToolHistory = true;
|
|
150
|
-
if (block.name)
|
|
151
|
-
toolNamesFromHistory.push(block.name);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
// Handle assistant tool_calls array (OpenAI format)
|
|
156
|
-
if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
|
|
157
|
-
hasToolHistory = true;
|
|
158
|
-
for (const tc of msg.tool_calls) {
|
|
159
|
-
if (tc.id)
|
|
160
|
-
tc.id = sanitizeToolUseId(tc.id);
|
|
161
|
-
if (tc.function?.name)
|
|
162
|
-
toolNamesFromHistory.push(tc.function.name);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
// Handle tool role message (OpenAI format)
|
|
166
|
-
if (msg.role === 'tool') {
|
|
167
|
-
hasToolHistory = true;
|
|
168
|
-
if (msg.tool_call_id) {
|
|
169
|
-
msg.tool_call_id = sanitizeToolUseId(msg.tool_call_id);
|
|
170
|
-
}
|
|
132
|
+
function sanitizeToolsForBedrock(tools) {
|
|
133
|
+
return tools.map(tool => {
|
|
134
|
+
if (tool.function) {
|
|
135
|
+
if (!tool.function.description || tool.function.description.length === 0) {
|
|
136
|
+
tool.function.description = " "; // Force non-empty string
|
|
171
137
|
}
|
|
172
138
|
}
|
|
139
|
+
return tool;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function sanitizeSchemaForKimi(schema) {
|
|
143
|
+
if (!schema || typeof schema !== 'object')
|
|
144
|
+
return schema;
|
|
145
|
+
// Kimi Fixes
|
|
146
|
+
if (schema.title)
|
|
147
|
+
delete schema.title;
|
|
148
|
+
// Fix empty objects "{}" which Kimi hates.
|
|
149
|
+
// If it's an empty object without type, assume string or object?
|
|
150
|
+
// Often happens with "additionalProperties: {}"
|
|
151
|
+
if (Object.keys(schema).length === 0) {
|
|
152
|
+
schema.type = "string"; // Fallback to safe type
|
|
153
|
+
schema.description = "Any value";
|
|
173
154
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
// Build toolConfig from tools array (Bedrock Converse format)
|
|
178
|
-
sanitized.tool_config = {
|
|
179
|
-
tools: sanitized.tools.map((t) => {
|
|
180
|
-
if (t.function) {
|
|
181
|
-
// OpenAI format -> Bedrock format
|
|
182
|
-
return {
|
|
183
|
-
toolSpec: {
|
|
184
|
-
name: t.function.name,
|
|
185
|
-
description: t.function.description || '',
|
|
186
|
-
inputSchema: {
|
|
187
|
-
json: t.function.parameters || { type: 'object', properties: {} }
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
return t; // Already in Bedrock format
|
|
193
|
-
})
|
|
194
|
-
};
|
|
155
|
+
if (schema.properties) {
|
|
156
|
+
for (const key in schema.properties) {
|
|
157
|
+
schema.properties[key] = sanitizeSchemaForKimi(schema.properties[key]);
|
|
195
158
|
}
|
|
196
159
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
toolSpec: {
|
|
206
|
-
name: name,
|
|
207
|
-
description: 'Tool inferred from conversation history (Bedrock compatibility shim)',
|
|
208
|
-
inputSchema: { json: { type: 'object', properties: {} } }
|
|
209
|
-
}
|
|
210
|
-
}))
|
|
211
|
-
: [{
|
|
212
|
-
toolSpec: {
|
|
213
|
-
name: '_bedrock_shim_tool',
|
|
214
|
-
description: 'Internal shim to satisfy Bedrock toolConfig requirement',
|
|
215
|
-
inputSchema: { json: { type: 'object', properties: {} } }
|
|
216
|
-
}
|
|
217
|
-
}]
|
|
218
|
-
};
|
|
219
|
-
log(`[Bedrock Shim] Injected toolConfig with ${uniqueToolNames.length || 1} shim tool(s) for history compatibility`);
|
|
220
|
-
}
|
|
221
|
-
return sanitized;
|
|
160
|
+
if (schema.items)
|
|
161
|
+
sanitizeSchemaForKimi(schema.items);
|
|
162
|
+
return schema;
|
|
163
|
+
}
|
|
164
|
+
function truncateTools(tools, limit = 120) {
|
|
165
|
+
if (!tools || tools.length <= limit)
|
|
166
|
+
return tools;
|
|
167
|
+
return tools.slice(0, limit);
|
|
222
168
|
}
|
|
223
169
|
const MAX_RETRIES = 3;
|
|
224
170
|
const RETRY_DELAY_MS = 1000;
|
|
171
|
+
const FETCH_TIMEOUT_MS = 600000; // 10 Minutes global timeout
|
|
225
172
|
function sleep(ms) {
|
|
226
173
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
227
174
|
}
|
|
228
175
|
async function fetchWithRetry(url, options, retries = MAX_RETRIES) {
|
|
229
176
|
try {
|
|
230
|
-
const
|
|
177
|
+
const controller = new AbortController();
|
|
178
|
+
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
179
|
+
const response = await fetch(url, { ...options, signal: controller.signal });
|
|
180
|
+
clearTimeout(timeoutId);
|
|
231
181
|
if (response.ok)
|
|
232
182
|
return response;
|
|
233
183
|
if (response.status === 404 || response.status === 401 || response.status === 400) {
|
|
@@ -261,11 +211,6 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
261
211
|
const config = loadConfig();
|
|
262
212
|
// DEBUG: Trace Config State for Hot Reload verification
|
|
263
213
|
log(`[Proxy Request] Config Loaded. Mode: ${config.mode}, HasKey: ${!!config.apiKey}, KeyLength: ${config.apiKey ? config.apiKey.length : 0}`);
|
|
264
|
-
// SPY LOGGING
|
|
265
|
-
try {
|
|
266
|
-
fs.appendFileSync('/tmp/opencode_spy.log', `\n\n=== REQUEST ${new Date().toISOString()} ===\nMODEL: ${body.model}\nBODY:\n${JSON.stringify(body, null, 2)}\n==========================\n`);
|
|
267
|
-
}
|
|
268
|
-
catch (e) { }
|
|
269
214
|
// 0. COMMAND HANDLING
|
|
270
215
|
if (body.messages && body.messages.length > 0) {
|
|
271
216
|
const lastMsg = body.messages[body.messages.length - 1];
|
|
@@ -316,6 +261,31 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
316
261
|
}
|
|
317
262
|
}
|
|
318
263
|
log(`Incoming Model (OpenCode ID): ${body.model}`);
|
|
264
|
+
// 0. SPECIAL: pollinations/connect (Guide & Status)
|
|
265
|
+
const CONNECT_MODEL_IDS = ['pollinations/connect', 'free/pollinations/connect', 'enter/pollinations/connect', 'connect-pollinations'];
|
|
266
|
+
if (CONNECT_MODEL_IDS.includes(body.model)) {
|
|
267
|
+
const guideContent = await buildConnectResponse(config);
|
|
268
|
+
res.writeHead(200, {
|
|
269
|
+
'Content-Type': 'text/event-stream',
|
|
270
|
+
'Cache-Control': 'no-cache',
|
|
271
|
+
'Connection': 'keep-alive'
|
|
272
|
+
});
|
|
273
|
+
const chunk = JSON.stringify({
|
|
274
|
+
id: 'connect-' + Date.now(),
|
|
275
|
+
object: 'chat.completion.chunk',
|
|
276
|
+
created: Math.floor(Date.now() / 1000),
|
|
277
|
+
model: 'pollinations/connect',
|
|
278
|
+
choices: [{
|
|
279
|
+
index: 0,
|
|
280
|
+
delta: { role: 'assistant', content: guideContent },
|
|
281
|
+
finish_reason: 'stop' // Instant finish
|
|
282
|
+
}]
|
|
283
|
+
});
|
|
284
|
+
res.write(`data: ${chunk}\n\n`);
|
|
285
|
+
res.write(`data: [DONE]\n\n`);
|
|
286
|
+
res.end();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
319
289
|
// 1. STRICT ROUTING & SAFETY NET LOGIC (V5)
|
|
320
290
|
let actualModel = body.model || "openai";
|
|
321
291
|
let isEnterprise = false;
|
|
@@ -324,16 +294,15 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
324
294
|
// LOAD QUOTA FOR SAFETY CHECKS
|
|
325
295
|
const { getQuotaStatus, formatQuotaForToast } = await import('./quota.js');
|
|
326
296
|
const quota = await getQuotaStatus(false);
|
|
327
|
-
// A. Resolve Base Target
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
actualModel = actualModel.replace(
|
|
297
|
+
// A. Resolve Base Target
|
|
298
|
+
if (actualModel.startsWith('enter/')) {
|
|
299
|
+
isEnterprise = true;
|
|
300
|
+
actualModel = actualModel.replace('enter/', '');
|
|
331
301
|
}
|
|
332
|
-
else if (actualModel.startsWith('free/')
|
|
333
|
-
|
|
302
|
+
else if (actualModel.startsWith('free/')) {
|
|
303
|
+
isEnterprise = false;
|
|
304
|
+
actualModel = actualModel.replace('free/', '');
|
|
334
305
|
}
|
|
335
|
-
// v6.0: Everything is enterprise now (requires API key)
|
|
336
|
-
isEnterprise = true;
|
|
337
306
|
// A.1 PAID MODEL ENFORCEMENT (V5.5 Strategy)
|
|
338
307
|
// Check dynamic list saved by generate-config.ts
|
|
339
308
|
if (isEnterprise) {
|
|
@@ -353,8 +322,13 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
353
322
|
if (quota.walletBalance <= 0.001) { // Floating point safety
|
|
354
323
|
log(`[SafetyNet] Paid Only Model (${actualModel}) requested but Wallet is Empty ($${quota.walletBalance}). BLOCKING.`);
|
|
355
324
|
// Immediate Block or Fallback?
|
|
356
|
-
//
|
|
357
|
-
|
|
325
|
+
// Text says: "💎 Paid Only models require purchased pollen only"
|
|
326
|
+
// Blocking is safer/clearer than falling back to a free model which might not be what the user expects for a "Pro" feature?
|
|
327
|
+
// Actually, Fallback to Free is usually better for UX if configured, BUT for specific "Paid Only" requests, the user explicitly chose a powerful model.
|
|
328
|
+
// Falling back to Mistral might be confusing if they asked for Gemini-Large.
|
|
329
|
+
// BUT we are failing gracefully.
|
|
330
|
+
// Let's Fallback to Free Default and Warn.
|
|
331
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
358
332
|
isEnterprise = false;
|
|
359
333
|
isFallbackActive = true;
|
|
360
334
|
fallbackReason = "Paid Only Model requires purchased credits";
|
|
@@ -380,127 +354,121 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
380
354
|
// WE DO NOT RETURN 403. WE ALLOW THE REQUEST.
|
|
381
355
|
// Since config.mode is now 'manual', the next checks (alwaysfree/pro) will be skipped.
|
|
382
356
|
}
|
|
383
|
-
|
|
384
|
-
// - Paid models BLOCKED (not fallback, BLOCK with message)
|
|
385
|
-
// - tier < threshold → fallback economy
|
|
386
|
-
// - tier = 0 → STOP (no wallet access)
|
|
387
|
-
if (config.mode === 'economy') {
|
|
357
|
+
if (config.mode === 'alwaysfree') {
|
|
388
358
|
if (isEnterprise) {
|
|
389
|
-
//
|
|
359
|
+
// Paid Only Check: BLOCK (not fallback) in AlwaysFree mode
|
|
390
360
|
try {
|
|
391
361
|
const homedir = process.env.HOME || '/tmp';
|
|
392
362
|
const standardPaidPath = path.join(homedir, '.pollinations', 'pollinations-paid-models.json');
|
|
393
363
|
if (fs.existsSync(standardPaidPath)) {
|
|
394
364
|
const paidModels = JSON.parse(fs.readFileSync(standardPaidPath, 'utf-8'));
|
|
395
365
|
if (paidModels.includes(actualModel)) {
|
|
396
|
-
log(`[
|
|
397
|
-
emitStatusToast('
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
366
|
+
log(`[AlwaysFree] BLOCKED: Paid Only Model (${actualModel}).`);
|
|
367
|
+
emitStatusToast('warning', t('proxy.warnings.paid_blocked_alwaysfree_title', { model: actualModel }), 'AlwaysFree Mode');
|
|
368
|
+
const blockMsg = {
|
|
369
|
+
id: `chatcmpl-block-${Date.now()}`,
|
|
370
|
+
object: 'chat.completion',
|
|
371
|
+
created: Math.floor(Date.now() / 1000),
|
|
372
|
+
model: actualModel,
|
|
373
|
+
choices: [{
|
|
374
|
+
index: 0,
|
|
375
|
+
message: {
|
|
376
|
+
role: 'assistant',
|
|
377
|
+
content: t('proxy.warnings.paid_blocked_alwaysfree_msg', { model: actualModel })
|
|
378
|
+
},
|
|
379
|
+
finish_reason: 'stop'
|
|
380
|
+
}],
|
|
381
|
+
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
382
|
+
};
|
|
383
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
384
|
+
res.end(JSON.stringify(blockMsg));
|
|
405
385
|
return;
|
|
406
386
|
}
|
|
407
387
|
}
|
|
408
388
|
}
|
|
409
389
|
catch (e) {
|
|
410
|
-
log(`[
|
|
390
|
+
log(`[Proxy AlwaysFree] Error checking paid models: ${e}`);
|
|
411
391
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
log(`[
|
|
415
|
-
|
|
392
|
+
if (!isFallbackActive && quota.tier === 'error') {
|
|
393
|
+
// Network error or unknown error (but NOT auth_limited, handled above)
|
|
394
|
+
log(`[SafetyNet] AlwaysFree Mode: Quota Check Failed. Switching to Free Fallback.`);
|
|
395
|
+
emitStatusToast('warning', t('proxy.warnings.quota_unreachable_title'), 'AlwaysFree Mode');
|
|
396
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
397
|
+
isEnterprise = false;
|
|
416
398
|
isFallbackActive = true;
|
|
417
|
-
fallbackReason =
|
|
399
|
+
fallbackReason = t('proxy.warnings.quota_unreachable_msg');
|
|
418
400
|
}
|
|
419
401
|
else {
|
|
420
402
|
const tierRatio = quota.tierLimit > 0 ? (quota.tierRemaining / quota.tierLimit) : 0;
|
|
421
|
-
// 3. STOP if tier = 0
|
|
422
|
-
if (quota.tierRemaining <= 0.01) {
|
|
423
|
-
log(`[Economy] STOP: Tier exhausted, no wallet access in Economy mode`);
|
|
424
|
-
emitStatusToast('error', `🛑 Quota épuisé! Attendez le reset ou passez en mode Pro.`, 'Mode Economy');
|
|
425
|
-
res.writeHead(429, { 'Content-Type': 'application/json' });
|
|
426
|
-
res.end(JSON.stringify({
|
|
427
|
-
error: {
|
|
428
|
-
message: `🛑 Daily quota exhausted. Wait for reset or switch to Pro mode to use wallet credits.`,
|
|
429
|
-
code: 'ECONOMY_TIER_EXHAUSTED'
|
|
430
|
-
}
|
|
431
|
-
}));
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
// 4. Fallback if tier < threshold
|
|
435
403
|
if (tierRatio <= (config.thresholds.tier / 100)) {
|
|
436
|
-
log(`[
|
|
437
|
-
|
|
404
|
+
log(`[SafetyNet] AlwaysFree Mode: Tier (${(tierRatio * 100).toFixed(1)}%) <= Threshold (${config.thresholds.tier}%). Switching.`);
|
|
405
|
+
emitStatusToast('warning', t('proxy.warnings.tier_limit_title', { threshold: config.thresholds.tier }), 'AlwaysFree Mode');
|
|
406
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
407
|
+
isEnterprise = false;
|
|
438
408
|
isFallbackActive = true;
|
|
439
|
-
fallbackReason =
|
|
409
|
+
fallbackReason = t('proxy.warnings.tier_limit_msg', { threshold: config.thresholds.tier });
|
|
440
410
|
}
|
|
441
411
|
}
|
|
442
412
|
}
|
|
443
413
|
}
|
|
444
|
-
// === MODE PRO ===
|
|
445
|
-
// - Paid models ALLOWED
|
|
446
|
-
// - wallet < wallet_stop $ → STOP
|
|
447
|
-
// - wallet < wallet_warn % → fallback pro
|
|
448
|
-
// - tier exhausted → info toast (continue on wallet)
|
|
449
414
|
else if (config.mode === 'pro') {
|
|
450
415
|
if (isEnterprise) {
|
|
451
|
-
// Init session wallet tracking (first request)
|
|
452
|
-
if (!config.session?.wallet_initial && quota.walletBalance > 0) {
|
|
453
|
-
const { initSessionWallet } = await import('./config.js');
|
|
454
|
-
initSessionWallet(quota.walletBalance);
|
|
455
|
-
}
|
|
456
416
|
if (quota.tier === 'error') {
|
|
457
|
-
|
|
458
|
-
|
|
417
|
+
// Network error or unknown
|
|
418
|
+
log(`[SafetyNet] Pro Mode: Quota Unreachable. Switching to Free Fallback.`);
|
|
419
|
+
emitStatusToast('warning', t('proxy.warnings.quota_unreachable_title'), 'Pro Mode');
|
|
420
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
421
|
+
isEnterprise = false;
|
|
459
422
|
isFallbackActive = true;
|
|
460
|
-
fallbackReason =
|
|
423
|
+
fallbackReason = t('proxy.warnings.quota_unreachable_msg');
|
|
461
424
|
}
|
|
462
425
|
else {
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
res.writeHead(429, { 'Content-Type': 'application/json' });
|
|
472
|
-
res.end(JSON.stringify({
|
|
473
|
-
error: {
|
|
474
|
-
message: `🛑 Wallet ($${quota.walletBalance.toFixed(2)}) below hard limit ($${walletStop}). Add credits or adjust wallet_stop threshold.`,
|
|
475
|
-
code: 'PRO_WALLET_LIMIT'
|
|
476
|
-
}
|
|
477
|
-
}));
|
|
478
|
-
return;
|
|
426
|
+
const tierRatio = quota.tierLimit > 0 ? (quota.tierRemaining / quota.tierLimit) : 0;
|
|
427
|
+
if (quota.walletBalance < config.thresholds.wallet && tierRatio <= (config.thresholds.tier / 100)) {
|
|
428
|
+
log(`[SafetyNet] Pro Mode: Wallet < $${config.thresholds.wallet} AND Tier < ${config.thresholds.tier}%. Switching.`);
|
|
429
|
+
emitStatusToast('warning', t('proxy.warnings.wallet_tier_critical_title', { wallet: config.thresholds.wallet, tier: config.thresholds.tier }), 'Pro Mode');
|
|
430
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
431
|
+
isEnterprise = false;
|
|
432
|
+
isFallbackActive = true;
|
|
433
|
+
fallbackReason = t('proxy.warnings.wallet_tier_critical_msg', { wallet: config.thresholds.wallet, tier: config.thresholds.tier });
|
|
479
434
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
actualModel = config.fallbacks.
|
|
435
|
+
else if (quota.walletBalance < config.thresholds.wallet) {
|
|
436
|
+
log(`[SafetyNet] Pro Mode: Wallet < $${config.thresholds.wallet}. Switching.`);
|
|
437
|
+
emitStatusToast('warning', t('proxy.warnings.wallet_limit_title', { wallet: config.thresholds.wallet }), 'Pro Mode');
|
|
438
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
439
|
+
isEnterprise = false;
|
|
484
440
|
isFallbackActive = true;
|
|
485
|
-
fallbackReason =
|
|
441
|
+
fallbackReason = t('proxy.warnings.wallet_limit_msg', { threshold: config.thresholds.wallet });
|
|
486
442
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
emitStatusToast('
|
|
443
|
+
else if (tierRatio <= (config.thresholds.tier / 100)) {
|
|
444
|
+
log(`[SafetyNet] Pro Mode: Tier < ${config.thresholds.tier}%. Switching.`);
|
|
445
|
+
emitStatusToast('warning', t('proxy.warnings.tier_limit_title', { threshold: config.thresholds.tier }), 'Pro Mode');
|
|
446
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
447
|
+
isEnterprise = false;
|
|
448
|
+
isFallbackActive = true;
|
|
449
|
+
fallbackReason = t('proxy.warnings.tier_limit_msg', { threshold: config.thresholds.tier });
|
|
490
450
|
}
|
|
491
451
|
}
|
|
492
452
|
}
|
|
493
453
|
}
|
|
494
|
-
// C. Construct URL & Headers
|
|
495
|
-
if (
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
454
|
+
// C. Construct URL & Headers
|
|
455
|
+
if (isEnterprise) {
|
|
456
|
+
if (!config.apiKey) {
|
|
457
|
+
emitLogToast('error', "Missing API Key for Enterprise Model", 'Proxy Error');
|
|
458
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
459
|
+
res.end(JSON.stringify({ error: { message: "API Key required for Enterprise models." } }));
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
|
|
463
|
+
authHeader = `Bearer ${config.apiKey}`;
|
|
464
|
+
log(`Routing to ENTERPRISE: ${actualModel}`);
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
targetUrl = 'https://text.pollinations.ai/openai/chat/completions';
|
|
468
|
+
authHeader = undefined;
|
|
469
|
+
log(`Routing to FREE: ${actualModel} ${isFallbackActive ? '(FALLBACK)' : ''}`);
|
|
470
|
+
// emitLogToast('info', `Routing to: FREE UNIVERSE (${actualModel})`, 'Pollinations Routing'); // Too noisy
|
|
500
471
|
}
|
|
501
|
-
targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
|
|
502
|
-
authHeader = `Bearer ${config.apiKey}`;
|
|
503
|
-
log(`Routing to gen.pollinations.ai: ${actualModel}`);
|
|
504
472
|
// NOTIFY SWITCH
|
|
505
473
|
if (isFallbackActive) {
|
|
506
474
|
emitStatusToast('warning', `⚠️ Safety Net: ${actualModel} (${fallbackReason})`, 'Pollinations Safety');
|
|
@@ -532,17 +500,24 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
532
500
|
// LOGIC BLOCK: MODEL SPECIFIC ADAPTATIONS
|
|
533
501
|
// =========================================================
|
|
534
502
|
if (proxyBody.tools && Array.isArray(proxyBody.tools) && proxyBody.tools.length > 0) {
|
|
535
|
-
// B0. KIMI / MOONSHOT SURGICAL FIX
|
|
536
|
-
// Tools are ENABLED. We rely on penalties and strict stops to fight loops.
|
|
503
|
+
// B0. KIMI / MOONSHOT SURGICAL FIX
|
|
537
504
|
if (actualModel.includes("kimi") || actualModel.includes("moonshot")) {
|
|
538
|
-
log(`[Proxy] Kimi: Tools ENABLED. Applying penalties/stops.`);
|
|
505
|
+
log(`[Proxy] Kimi: Tools ENABLED. Applying penalties/stops/sanitization.`);
|
|
539
506
|
proxyBody.frequency_penalty = 1.1;
|
|
540
507
|
proxyBody.presence_penalty = 0.4;
|
|
541
508
|
proxyBody.stop = ["<|endoftext|>", "User:", "\nUser", "User :"];
|
|
509
|
+
// KIMI FIX: Remove 'title' from schema
|
|
510
|
+
proxyBody.tools = proxyBody.tools.map((t) => {
|
|
511
|
+
if (t.function && t.function.parameters) {
|
|
512
|
+
t.function.parameters = sanitizeSchemaForKimi(t.function.parameters);
|
|
513
|
+
}
|
|
514
|
+
return t;
|
|
515
|
+
});
|
|
542
516
|
}
|
|
543
|
-
// A. AZURE/OPENAI FIXES
|
|
544
|
-
if (actualModel.includes("gpt") || actualModel.includes("openai") || actualModel.includes("azure")) {
|
|
545
|
-
|
|
517
|
+
// A. AZURE/OPENAI FIXES + MIDJOURNEY + GROK
|
|
518
|
+
if (actualModel.includes("gpt") || actualModel.includes("openai") || actualModel.includes("azure") || actualModel.includes("midijourney") || actualModel.includes("grok")) {
|
|
519
|
+
const limit = (actualModel.includes("midijourney") || actualModel.includes("grok")) ? 128 : 120;
|
|
520
|
+
proxyBody.tools = truncateTools(proxyBody.tools, limit);
|
|
546
521
|
if (proxyBody.messages) {
|
|
547
522
|
proxyBody.messages.forEach((m) => {
|
|
548
523
|
if (m.tool_calls) {
|
|
@@ -557,6 +532,11 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
557
532
|
});
|
|
558
533
|
}
|
|
559
534
|
}
|
|
535
|
+
// BEDROCK FIX (Claude / Nova / ChickyTutor)
|
|
536
|
+
if (actualModel.includes("claude") || actualModel.includes("nova") || actualModel.includes("bedrock") || actualModel.includes("chickytutor")) {
|
|
537
|
+
log(`[Proxy] Bedrock: Sanitizing tools description.`);
|
|
538
|
+
proxyBody.tools = sanitizeToolsForBedrock(proxyBody.tools);
|
|
539
|
+
}
|
|
560
540
|
// B1. NOMNOM SPECIAL (Disable Grounding, KEEP Search Tool)
|
|
561
541
|
if (actualModel === "nomnom") {
|
|
562
542
|
proxyBody.tools_config = { google_search_retrieval: { disable: true } };
|
|
@@ -564,36 +544,12 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
564
544
|
proxyBody.tools = sanitizeToolsForVertex(proxyBody.tools || []);
|
|
565
545
|
log(`[Proxy] Nomnom Fix: Grounding Disabled, Search Tool KEPT.`);
|
|
566
546
|
}
|
|
567
|
-
// B2. GEMINI FREE / FAST (CRASH FIX: STRICT SANITIZATION)
|
|
568
|
-
// Restore Tools but REMOVE conflicting ones (Search)
|
|
569
547
|
// B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
|
|
570
|
-
|
|
571
|
-
// GLOBAL BEDROCK FIX (All Models)
|
|
572
|
-
// Check if history has tools but current request misses tools definition.
|
|
573
|
-
// This happens when OpenCode sends the Tool Result (optimisation),
|
|
574
|
-
// but Bedrock requires toolConfig to validate the history.
|
|
575
|
-
const hasToolHistory = proxyBody.messages?.some((m) => m.role === 'tool' || m.tool_calls);
|
|
576
|
-
if (hasToolHistory && (!proxyBody.tools || proxyBody.tools.length === 0)) {
|
|
577
|
-
// Inject Shim Tool to satisfy Bedrock
|
|
578
|
-
proxyBody.tools = [{
|
|
579
|
-
type: 'function',
|
|
580
|
-
function: {
|
|
581
|
-
name: '_bedrock_compatibility_shim',
|
|
582
|
-
description: 'Internal system tool to satisfy Bedrock strict toolConfig requirement. Do not use.',
|
|
583
|
-
parameters: { type: 'object', properties: {} }
|
|
584
|
-
}
|
|
585
|
-
}];
|
|
586
|
-
log(`[Proxy] Bedrock Fix: Injected shim tool for ${actualModel} (History has tools, Request missing tools)`);
|
|
587
|
-
}
|
|
588
|
-
// B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
|
|
589
|
-
// Fixes "Multiple tools" error (Vertex) and "JSON body validation failed" (v5.3.5 regression)
|
|
590
|
-
// Added ChickyTutor (Claude/Gemini based) to fix "toolConfig must be defined" error.
|
|
591
|
-
else if (actualModel.includes("gemini") || actualModel.includes("chickytutor")) {
|
|
548
|
+
else if (actualModel.includes("gemini")) {
|
|
592
549
|
let hasFunctions = false;
|
|
593
550
|
if (proxyBody.tools && Array.isArray(proxyBody.tools)) {
|
|
594
551
|
hasFunctions = proxyBody.tools.some((t) => t.type === 'function' || t.function);
|
|
595
552
|
}
|
|
596
|
-
// Old Shim logic removed (moved up)
|
|
597
553
|
if (hasFunctions) {
|
|
598
554
|
// 1. Strict cleanup of 'google_search' tool
|
|
599
555
|
proxyBody.tools = proxyBody.tools.filter((t) => {
|
|
@@ -624,14 +580,6 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
624
580
|
log(`[Proxy] Gemini Logic: Tools=${proxyBody.tools ? proxyBody.tools.length : 'REMOVED'}, Stops NOT Injected.`);
|
|
625
581
|
}
|
|
626
582
|
}
|
|
627
|
-
// B5. BEDROCK TOKEN LIMIT FIX
|
|
628
|
-
if (actualModel.includes("chicky") || actualModel.includes("mistral")) {
|
|
629
|
-
// Force max_tokens if not present or too high (Bedrock outputs usually max 4k, context 8k+ but strict check)
|
|
630
|
-
if (!proxyBody.max_tokens || proxyBody.max_tokens > 4096) {
|
|
631
|
-
proxyBody.max_tokens = 4096;
|
|
632
|
-
log(`[Proxy] Enforcing max_tokens=4096 for ${actualModel} (Bedrock Limit)`);
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
583
|
// C. GEMINI ID BACKTRACKING & SIGNATURE
|
|
636
584
|
if ((actualModel.includes("gemini") || actualModel === "nomnom") && proxyBody.messages) {
|
|
637
585
|
const lastMsg = proxyBody.messages[proxyBody.messages.length - 1];
|
|
@@ -695,18 +643,10 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
695
643
|
if (authHeader)
|
|
696
644
|
headers['Authorization'] = authHeader;
|
|
697
645
|
// 5. Forward (Global Fetch with Retry)
|
|
698
|
-
// BEDROCK COMPATIBILITY: Apply transformation BEFORE first request
|
|
699
|
-
const isBedrockModel = actualModel.includes('nova') ||
|
|
700
|
-
actualModel.includes('amazon') ||
|
|
701
|
-
actualModel.includes('chickytutor');
|
|
702
|
-
const finalProxyBody = isBedrockModel ? sanitizeForBedrock(proxyBody) : proxyBody;
|
|
703
|
-
if (isBedrockModel) {
|
|
704
|
-
log(`[Proxy] Applied Bedrock shim for ${actualModel} (Initial Request)`);
|
|
705
|
-
}
|
|
706
646
|
const fetchRes = await fetchWithRetry(targetUrl, {
|
|
707
647
|
method: 'POST',
|
|
708
648
|
headers: headers,
|
|
709
|
-
body: JSON.stringify(
|
|
649
|
+
body: JSON.stringify(proxyBody)
|
|
710
650
|
});
|
|
711
651
|
res.statusCode = fetchRes.status;
|
|
712
652
|
fetchRes.headers.forEach((val, key) => {
|
|
@@ -725,14 +665,20 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
725
665
|
if ((isEnterpriseFallback || isGeminiToolsFallback) && config.mode !== 'manual') {
|
|
726
666
|
log(`[SafetyNet] Upstream Rejection (${fetchRes.status}). Triggering Transparent Fallback.`);
|
|
727
667
|
if (isEnterpriseFallback) {
|
|
728
|
-
// 1a. Enterprise -> Fallback
|
|
729
|
-
actualModel = config.
|
|
730
|
-
? (config.fallbacks.pro || 'qwen-coder')
|
|
731
|
-
: (config.fallbacks.economy || 'nova-fast');
|
|
668
|
+
// 1a. Enterprise -> Free Fallback
|
|
669
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
732
670
|
isEnterprise = false;
|
|
733
671
|
isFallbackActive = true;
|
|
734
|
-
if (fetchRes.status === 402)
|
|
672
|
+
if (fetchRes.status === 402) {
|
|
735
673
|
fallbackReason = "Insufficient Funds (Upstream 402)";
|
|
674
|
+
// Force refresh quota cache so next pre-flight check is accurate
|
|
675
|
+
try {
|
|
676
|
+
await getQuotaStatus(true);
|
|
677
|
+
}
|
|
678
|
+
catch (e) {
|
|
679
|
+
log(`[Proxy Quota] Silent refresh error: ${e}`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
736
682
|
else if (fetchRes.status === 429)
|
|
737
683
|
fallbackReason = "Rate Limit (Upstream 429)";
|
|
738
684
|
else if (fetchRes.status === 401)
|
|
@@ -750,33 +696,16 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
750
696
|
// 2. Notify
|
|
751
697
|
emitStatusToast('warning', `⚠️ Safety Net: ${actualModel} (${fallbackReason})`, 'Pollinations Safety');
|
|
752
698
|
emitLogToast('warning', `Recovering from ${fetchRes.status} -> Switching to ${actualModel}`, 'Safety Net');
|
|
753
|
-
// 3. Re-Prepare Request
|
|
754
|
-
targetUrl = 'https://
|
|
699
|
+
// 3. Re-Prepare Request
|
|
700
|
+
targetUrl = 'https://text.pollinations.ai/openai/chat/completions';
|
|
755
701
|
const retryHeaders = { ...headers };
|
|
756
|
-
//
|
|
702
|
+
delete retryHeaders['Authorization']; // Free = No Auth
|
|
757
703
|
const retryBody = { ...proxyBody, model: actualModel };
|
|
758
|
-
// LIMIT MAX_TOKENS for lightweight fallback models (e.g. nova-fast limits)
|
|
759
|
-
if (actualModel.includes('nova') || actualModel.includes('fast')) {
|
|
760
|
-
// Force max_tokens down if missing or too high
|
|
761
|
-
if (!retryBody.max_tokens || retryBody.max_tokens > 4000) {
|
|
762
|
-
retryBody.max_tokens = 4000;
|
|
763
|
-
log(`[SafetyNet] Adjusted max_tokens to 4000 for ${actualModel}`);
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
// BEDROCK COMPATIBILITY SHIM (BUG 1 - Proper Transform)
|
|
767
|
-
const isBedrockModel = actualModel.includes('nova') ||
|
|
768
|
-
actualModel.includes('amazon') ||
|
|
769
|
-
actualModel.includes('chickytutor');
|
|
770
|
-
let finalRetryBody = retryBody;
|
|
771
|
-
if (isBedrockModel) {
|
|
772
|
-
finalRetryBody = sanitizeForBedrock(retryBody);
|
|
773
|
-
log(`[SafetyNet] Applied Bedrock shim for ${actualModel}`);
|
|
774
|
-
}
|
|
775
704
|
// 4. Retry Fetch
|
|
776
705
|
const retryRes = await fetchWithRetry(targetUrl, {
|
|
777
706
|
method: 'POST',
|
|
778
707
|
headers: retryHeaders,
|
|
779
|
-
body: JSON.stringify(
|
|
708
|
+
body: JSON.stringify(retryBody)
|
|
780
709
|
});
|
|
781
710
|
if (retryRes.ok) {
|
|
782
711
|
res.statusCode = retryRes.status;
|