opencode-pollinations-plugin 6.1.0-beta.9 → 6.2.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 +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 -307
- 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 +9 -3
- 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,141 +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
|
-
? uniqueToolNames.map(name => ({
|
|
206
|
-
type: 'function',
|
|
207
|
-
function: {
|
|
208
|
-
name: name,
|
|
209
|
-
description: 'Tool inferred from conversation history (Bedrock compatibility shim)',
|
|
210
|
-
parameters: { type: 'object', properties: {} }
|
|
211
|
-
}
|
|
212
|
-
}))
|
|
213
|
-
: [{
|
|
214
|
-
type: 'function',
|
|
215
|
-
function: {
|
|
216
|
-
name: '_bedrock_shim_tool',
|
|
217
|
-
description: 'Internal shim to satisfy Bedrock toolConfig requirement',
|
|
218
|
-
parameters: { type: 'object', properties: {} }
|
|
219
|
-
}
|
|
220
|
-
}];
|
|
221
|
-
// Inject the tools array (Pollinations format)
|
|
222
|
-
sanitized.tools = shimTools;
|
|
223
|
-
// Also set tool_config for direct Bedrock passthrough (belt & suspenders)
|
|
224
|
-
sanitized.tool_config = {
|
|
225
|
-
tools: shimTools.map(t => ({
|
|
226
|
-
toolSpec: {
|
|
227
|
-
name: t.function.name,
|
|
228
|
-
description: t.function.description,
|
|
229
|
-
inputSchema: { json: t.function.parameters }
|
|
230
|
-
}
|
|
231
|
-
}))
|
|
232
|
-
};
|
|
233
|
-
log(`[Bedrock Shim] Injected ${shimTools.length} shim tool(s) for history compatibility: [${uniqueToolNames.join(', ') || '_bedrock_shim_tool'}]`);
|
|
234
|
-
}
|
|
235
|
-
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);
|
|
236
168
|
}
|
|
237
169
|
const MAX_RETRIES = 3;
|
|
238
170
|
const RETRY_DELAY_MS = 1000;
|
|
171
|
+
const FETCH_TIMEOUT_MS = 600000; // 10 Minutes global timeout
|
|
239
172
|
function sleep(ms) {
|
|
240
173
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
241
174
|
}
|
|
242
175
|
async function fetchWithRetry(url, options, retries = MAX_RETRIES) {
|
|
243
176
|
try {
|
|
244
|
-
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);
|
|
245
181
|
if (response.ok)
|
|
246
182
|
return response;
|
|
247
183
|
if (response.status === 404 || response.status === 401 || response.status === 400) {
|
|
@@ -275,11 +211,6 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
275
211
|
const config = loadConfig();
|
|
276
212
|
// DEBUG: Trace Config State for Hot Reload verification
|
|
277
213
|
log(`[Proxy Request] Config Loaded. Mode: ${config.mode}, HasKey: ${!!config.apiKey}, KeyLength: ${config.apiKey ? config.apiKey.length : 0}`);
|
|
278
|
-
// SPY LOGGING
|
|
279
|
-
try {
|
|
280
|
-
fs.appendFileSync('/tmp/opencode_spy.log', `\n\n=== REQUEST ${new Date().toISOString()} ===\nMODEL: ${body.model}\nBODY:\n${JSON.stringify(body, null, 2)}\n==========================\n`);
|
|
281
|
-
}
|
|
282
|
-
catch (e) { }
|
|
283
214
|
// 0. COMMAND HANDLING
|
|
284
215
|
if (body.messages && body.messages.length > 0) {
|
|
285
216
|
const lastMsg = body.messages[body.messages.length - 1];
|
|
@@ -330,6 +261,31 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
330
261
|
}
|
|
331
262
|
}
|
|
332
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
|
+
}
|
|
333
289
|
// 1. STRICT ROUTING & SAFETY NET LOGIC (V5)
|
|
334
290
|
let actualModel = body.model || "openai";
|
|
335
291
|
let isEnterprise = false;
|
|
@@ -338,16 +294,15 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
338
294
|
// LOAD QUOTA FOR SAFETY CHECKS
|
|
339
295
|
const { getQuotaStatus, formatQuotaForToast } = await import('./quota.js');
|
|
340
296
|
const quota = await getQuotaStatus(false);
|
|
341
|
-
// A. Resolve Base Target
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
actualModel = actualModel.replace(
|
|
297
|
+
// A. Resolve Base Target
|
|
298
|
+
if (actualModel.startsWith('enter/')) {
|
|
299
|
+
isEnterprise = true;
|
|
300
|
+
actualModel = actualModel.replace('enter/', '');
|
|
345
301
|
}
|
|
346
|
-
else if (actualModel.startsWith('free/')
|
|
347
|
-
|
|
302
|
+
else if (actualModel.startsWith('free/')) {
|
|
303
|
+
isEnterprise = false;
|
|
304
|
+
actualModel = actualModel.replace('free/', '');
|
|
348
305
|
}
|
|
349
|
-
// v6.0: Everything is enterprise now (requires API key)
|
|
350
|
-
isEnterprise = true;
|
|
351
306
|
// A.1 PAID MODEL ENFORCEMENT (V5.5 Strategy)
|
|
352
307
|
// Check dynamic list saved by generate-config.ts
|
|
353
308
|
if (isEnterprise) {
|
|
@@ -367,8 +322,13 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
367
322
|
if (quota.walletBalance <= 0.001) { // Floating point safety
|
|
368
323
|
log(`[SafetyNet] Paid Only Model (${actualModel}) requested but Wallet is Empty ($${quota.walletBalance}). BLOCKING.`);
|
|
369
324
|
// Immediate Block or Fallback?
|
|
370
|
-
//
|
|
371
|
-
|
|
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/', '');
|
|
372
332
|
isEnterprise = false;
|
|
373
333
|
isFallbackActive = true;
|
|
374
334
|
fallbackReason = "Paid Only Model requires purchased credits";
|
|
@@ -394,127 +354,121 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
394
354
|
// WE DO NOT RETURN 403. WE ALLOW THE REQUEST.
|
|
395
355
|
// Since config.mode is now 'manual', the next checks (alwaysfree/pro) will be skipped.
|
|
396
356
|
}
|
|
397
|
-
|
|
398
|
-
// - Paid models BLOCKED (not fallback, BLOCK with message)
|
|
399
|
-
// - tier < threshold → fallback economy
|
|
400
|
-
// - tier = 0 → STOP (no wallet access)
|
|
401
|
-
if (config.mode === 'economy') {
|
|
357
|
+
if (config.mode === 'alwaysfree') {
|
|
402
358
|
if (isEnterprise) {
|
|
403
|
-
//
|
|
359
|
+
// Paid Only Check: BLOCK (not fallback) in AlwaysFree mode
|
|
404
360
|
try {
|
|
405
361
|
const homedir = process.env.HOME || '/tmp';
|
|
406
362
|
const standardPaidPath = path.join(homedir, '.pollinations', 'pollinations-paid-models.json');
|
|
407
363
|
if (fs.existsSync(standardPaidPath)) {
|
|
408
364
|
const paidModels = JSON.parse(fs.readFileSync(standardPaidPath, 'utf-8'));
|
|
409
365
|
if (paidModels.includes(actualModel)) {
|
|
410
|
-
log(`[
|
|
411
|
-
emitStatusToast('
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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));
|
|
419
385
|
return;
|
|
420
386
|
}
|
|
421
387
|
}
|
|
422
388
|
}
|
|
423
389
|
catch (e) {
|
|
424
|
-
log(`[
|
|
390
|
+
log(`[Proxy AlwaysFree] Error checking paid models: ${e}`);
|
|
425
391
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
log(`[
|
|
429
|
-
|
|
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;
|
|
430
398
|
isFallbackActive = true;
|
|
431
|
-
fallbackReason =
|
|
399
|
+
fallbackReason = t('proxy.warnings.quota_unreachable_msg');
|
|
432
400
|
}
|
|
433
401
|
else {
|
|
434
402
|
const tierRatio = quota.tierLimit > 0 ? (quota.tierRemaining / quota.tierLimit) : 0;
|
|
435
|
-
// 3. STOP if tier = 0
|
|
436
|
-
if (quota.tierRemaining <= 0.01) {
|
|
437
|
-
log(`[Economy] STOP: Tier exhausted, no wallet access in Economy mode`);
|
|
438
|
-
emitStatusToast('error', `🛑 Quota épuisé! Attendez le reset ou passez en mode Pro.`, 'Mode Economy');
|
|
439
|
-
res.writeHead(429, { 'Content-Type': 'application/json' });
|
|
440
|
-
res.end(JSON.stringify({
|
|
441
|
-
error: {
|
|
442
|
-
message: `🛑 Daily quota exhausted. Wait for reset or switch to Pro mode to use wallet credits.`,
|
|
443
|
-
code: 'ECONOMY_TIER_EXHAUSTED'
|
|
444
|
-
}
|
|
445
|
-
}));
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
// 4. Fallback if tier < threshold
|
|
449
403
|
if (tierRatio <= (config.thresholds.tier / 100)) {
|
|
450
|
-
log(`[
|
|
451
|
-
|
|
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;
|
|
452
408
|
isFallbackActive = true;
|
|
453
|
-
fallbackReason =
|
|
409
|
+
fallbackReason = t('proxy.warnings.tier_limit_msg', { threshold: config.thresholds.tier });
|
|
454
410
|
}
|
|
455
411
|
}
|
|
456
412
|
}
|
|
457
413
|
}
|
|
458
|
-
// === MODE PRO ===
|
|
459
|
-
// - Paid models ALLOWED
|
|
460
|
-
// - wallet < wallet_stop $ → STOP
|
|
461
|
-
// - wallet < wallet_warn % → fallback pro
|
|
462
|
-
// - tier exhausted → info toast (continue on wallet)
|
|
463
414
|
else if (config.mode === 'pro') {
|
|
464
415
|
if (isEnterprise) {
|
|
465
|
-
// Init session wallet tracking (first request)
|
|
466
|
-
if (!config.session?.wallet_initial && quota.walletBalance > 0) {
|
|
467
|
-
const { initSessionWallet } = await import('./config.js');
|
|
468
|
-
initSessionWallet(quota.walletBalance);
|
|
469
|
-
}
|
|
470
416
|
if (quota.tier === 'error') {
|
|
471
|
-
|
|
472
|
-
|
|
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;
|
|
473
422
|
isFallbackActive = true;
|
|
474
|
-
fallbackReason =
|
|
423
|
+
fallbackReason = t('proxy.warnings.quota_unreachable_msg');
|
|
475
424
|
}
|
|
476
425
|
else {
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
res.writeHead(429, { 'Content-Type': 'application/json' });
|
|
486
|
-
res.end(JSON.stringify({
|
|
487
|
-
error: {
|
|
488
|
-
message: `🛑 Wallet ($${quota.walletBalance.toFixed(2)}) below hard limit ($${walletStop}). Add credits or adjust wallet_stop threshold.`,
|
|
489
|
-
code: 'PRO_WALLET_LIMIT'
|
|
490
|
-
}
|
|
491
|
-
}));
|
|
492
|
-
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 });
|
|
493
434
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
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;
|
|
498
440
|
isFallbackActive = true;
|
|
499
|
-
fallbackReason =
|
|
441
|
+
fallbackReason = t('proxy.warnings.wallet_limit_msg', { threshold: config.thresholds.wallet });
|
|
500
442
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
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 });
|
|
504
450
|
}
|
|
505
451
|
}
|
|
506
452
|
}
|
|
507
453
|
}
|
|
508
|
-
// C. Construct URL & Headers
|
|
509
|
-
if (
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
|
514
471
|
}
|
|
515
|
-
targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
|
|
516
|
-
authHeader = `Bearer ${config.apiKey}`;
|
|
517
|
-
log(`Routing to gen.pollinations.ai: ${actualModel}`);
|
|
518
472
|
// NOTIFY SWITCH
|
|
519
473
|
if (isFallbackActive) {
|
|
520
474
|
emitStatusToast('warning', `⚠️ Safety Net: ${actualModel} (${fallbackReason})`, 'Pollinations Safety');
|
|
@@ -546,17 +500,24 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
546
500
|
// LOGIC BLOCK: MODEL SPECIFIC ADAPTATIONS
|
|
547
501
|
// =========================================================
|
|
548
502
|
if (proxyBody.tools && Array.isArray(proxyBody.tools) && proxyBody.tools.length > 0) {
|
|
549
|
-
// B0. KIMI / MOONSHOT SURGICAL FIX
|
|
550
|
-
// Tools are ENABLED. We rely on penalties and strict stops to fight loops.
|
|
503
|
+
// B0. KIMI / MOONSHOT SURGICAL FIX
|
|
551
504
|
if (actualModel.includes("kimi") || actualModel.includes("moonshot")) {
|
|
552
|
-
log(`[Proxy] Kimi: Tools ENABLED. Applying penalties/stops.`);
|
|
505
|
+
log(`[Proxy] Kimi: Tools ENABLED. Applying penalties/stops/sanitization.`);
|
|
553
506
|
proxyBody.frequency_penalty = 1.1;
|
|
554
507
|
proxyBody.presence_penalty = 0.4;
|
|
555
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
|
+
});
|
|
556
516
|
}
|
|
557
|
-
// A. AZURE/OPENAI FIXES
|
|
558
|
-
if (actualModel.includes("gpt") || actualModel.includes("openai") || actualModel.includes("azure")) {
|
|
559
|
-
|
|
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);
|
|
560
521
|
if (proxyBody.messages) {
|
|
561
522
|
proxyBody.messages.forEach((m) => {
|
|
562
523
|
if (m.tool_calls) {
|
|
@@ -571,6 +532,11 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
571
532
|
});
|
|
572
533
|
}
|
|
573
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
|
+
}
|
|
574
540
|
// B1. NOMNOM SPECIAL (Disable Grounding, KEEP Search Tool)
|
|
575
541
|
if (actualModel === "nomnom") {
|
|
576
542
|
proxyBody.tools_config = { google_search_retrieval: { disable: true } };
|
|
@@ -578,36 +544,12 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
578
544
|
proxyBody.tools = sanitizeToolsForVertex(proxyBody.tools || []);
|
|
579
545
|
log(`[Proxy] Nomnom Fix: Grounding Disabled, Search Tool KEPT.`);
|
|
580
546
|
}
|
|
581
|
-
// B2. GEMINI FREE / FAST (CRASH FIX: STRICT SANITIZATION)
|
|
582
|
-
// Restore Tools but REMOVE conflicting ones (Search)
|
|
583
547
|
// B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
|
|
584
|
-
|
|
585
|
-
// GLOBAL BEDROCK FIX (All Models)
|
|
586
|
-
// Check if history has tools but current request misses tools definition.
|
|
587
|
-
// This happens when OpenCode sends the Tool Result (optimisation),
|
|
588
|
-
// but Bedrock requires toolConfig to validate the history.
|
|
589
|
-
const hasToolHistory = proxyBody.messages?.some((m) => m.role === 'tool' || m.tool_calls);
|
|
590
|
-
if (hasToolHistory && (!proxyBody.tools || proxyBody.tools.length === 0)) {
|
|
591
|
-
// Inject Shim Tool to satisfy Bedrock
|
|
592
|
-
proxyBody.tools = [{
|
|
593
|
-
type: 'function',
|
|
594
|
-
function: {
|
|
595
|
-
name: '_bedrock_compatibility_shim',
|
|
596
|
-
description: 'Internal system tool to satisfy Bedrock strict toolConfig requirement. Do not use.',
|
|
597
|
-
parameters: { type: 'object', properties: {} }
|
|
598
|
-
}
|
|
599
|
-
}];
|
|
600
|
-
log(`[Proxy] Bedrock Fix: Injected shim tool for ${actualModel} (History has tools, Request missing tools)`);
|
|
601
|
-
}
|
|
602
|
-
// B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
|
|
603
|
-
// Fixes "Multiple tools" error (Vertex) and "JSON body validation failed" (v5.3.5 regression)
|
|
604
|
-
// Added ChickyTutor (Claude/Gemini based) to fix "toolConfig must be defined" error.
|
|
605
|
-
else if (actualModel.includes("gemini") || actualModel.includes("chickytutor")) {
|
|
548
|
+
else if (actualModel.includes("gemini")) {
|
|
606
549
|
let hasFunctions = false;
|
|
607
550
|
if (proxyBody.tools && Array.isArray(proxyBody.tools)) {
|
|
608
551
|
hasFunctions = proxyBody.tools.some((t) => t.type === 'function' || t.function);
|
|
609
552
|
}
|
|
610
|
-
// Old Shim logic removed (moved up)
|
|
611
553
|
if (hasFunctions) {
|
|
612
554
|
// 1. Strict cleanup of 'google_search' tool
|
|
613
555
|
proxyBody.tools = proxyBody.tools.filter((t) => {
|
|
@@ -638,14 +580,6 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
638
580
|
log(`[Proxy] Gemini Logic: Tools=${proxyBody.tools ? proxyBody.tools.length : 'REMOVED'}, Stops NOT Injected.`);
|
|
639
581
|
}
|
|
640
582
|
}
|
|
641
|
-
// B5. BEDROCK TOKEN LIMIT FIX
|
|
642
|
-
if (actualModel.includes("chicky") || actualModel.includes("mistral")) {
|
|
643
|
-
// Force max_tokens if not present or too high (Bedrock outputs usually max 4k, context 8k+ but strict check)
|
|
644
|
-
if (!proxyBody.max_tokens || proxyBody.max_tokens > 4096) {
|
|
645
|
-
proxyBody.max_tokens = 4096;
|
|
646
|
-
log(`[Proxy] Enforcing max_tokens=4096 for ${actualModel} (Bedrock Limit)`);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
583
|
// C. GEMINI ID BACKTRACKING & SIGNATURE
|
|
650
584
|
if ((actualModel.includes("gemini") || actualModel === "nomnom") && proxyBody.messages) {
|
|
651
585
|
const lastMsg = proxyBody.messages[proxyBody.messages.length - 1];
|
|
@@ -709,18 +643,10 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
709
643
|
if (authHeader)
|
|
710
644
|
headers['Authorization'] = authHeader;
|
|
711
645
|
// 5. Forward (Global Fetch with Retry)
|
|
712
|
-
// BEDROCK COMPATIBILITY: Apply transformation BEFORE first request
|
|
713
|
-
const isBedrockModel = actualModel.includes('nova') ||
|
|
714
|
-
actualModel.includes('amazon') ||
|
|
715
|
-
actualModel.includes('chickytutor');
|
|
716
|
-
const finalProxyBody = isBedrockModel ? sanitizeForBedrock(proxyBody) : proxyBody;
|
|
717
|
-
if (isBedrockModel) {
|
|
718
|
-
log(`[Proxy] Applied Bedrock shim for ${actualModel} (Initial Request)`);
|
|
719
|
-
}
|
|
720
646
|
const fetchRes = await fetchWithRetry(targetUrl, {
|
|
721
647
|
method: 'POST',
|
|
722
648
|
headers: headers,
|
|
723
|
-
body: JSON.stringify(
|
|
649
|
+
body: JSON.stringify(proxyBody)
|
|
724
650
|
});
|
|
725
651
|
res.statusCode = fetchRes.status;
|
|
726
652
|
fetchRes.headers.forEach((val, key) => {
|
|
@@ -739,14 +665,20 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
739
665
|
if ((isEnterpriseFallback || isGeminiToolsFallback) && config.mode !== 'manual') {
|
|
740
666
|
log(`[SafetyNet] Upstream Rejection (${fetchRes.status}). Triggering Transparent Fallback.`);
|
|
741
667
|
if (isEnterpriseFallback) {
|
|
742
|
-
// 1a. Enterprise -> Fallback
|
|
743
|
-
actualModel = config.
|
|
744
|
-
? (config.fallbacks.pro || 'qwen-coder')
|
|
745
|
-
: (config.fallbacks.economy || 'nova-fast');
|
|
668
|
+
// 1a. Enterprise -> Free Fallback
|
|
669
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
746
670
|
isEnterprise = false;
|
|
747
671
|
isFallbackActive = true;
|
|
748
|
-
if (fetchRes.status === 402)
|
|
672
|
+
if (fetchRes.status === 402) {
|
|
749
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
|
+
}
|
|
750
682
|
else if (fetchRes.status === 429)
|
|
751
683
|
fallbackReason = "Rate Limit (Upstream 429)";
|
|
752
684
|
else if (fetchRes.status === 401)
|
|
@@ -764,33 +696,16 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
764
696
|
// 2. Notify
|
|
765
697
|
emitStatusToast('warning', `⚠️ Safety Net: ${actualModel} (${fallbackReason})`, 'Pollinations Safety');
|
|
766
698
|
emitLogToast('warning', `Recovering from ${fetchRes.status} -> Switching to ${actualModel}`, 'Safety Net');
|
|
767
|
-
// 3. Re-Prepare Request
|
|
768
|
-
targetUrl = 'https://
|
|
699
|
+
// 3. Re-Prepare Request
|
|
700
|
+
targetUrl = 'https://text.pollinations.ai/openai/chat/completions';
|
|
769
701
|
const retryHeaders = { ...headers };
|
|
770
|
-
//
|
|
702
|
+
delete retryHeaders['Authorization']; // Free = No Auth
|
|
771
703
|
const retryBody = { ...proxyBody, model: actualModel };
|
|
772
|
-
// LIMIT MAX_TOKENS for lightweight fallback models (e.g. nova-fast limits)
|
|
773
|
-
if (actualModel.includes('nova') || actualModel.includes('fast')) {
|
|
774
|
-
// Force max_tokens down if missing or too high
|
|
775
|
-
if (!retryBody.max_tokens || retryBody.max_tokens > 4000) {
|
|
776
|
-
retryBody.max_tokens = 4000;
|
|
777
|
-
log(`[SafetyNet] Adjusted max_tokens to 4000 for ${actualModel}`);
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
// BEDROCK COMPATIBILITY SHIM (BUG 1 - Proper Transform)
|
|
781
|
-
const isBedrockModel = actualModel.includes('nova') ||
|
|
782
|
-
actualModel.includes('amazon') ||
|
|
783
|
-
actualModel.includes('chickytutor');
|
|
784
|
-
let finalRetryBody = retryBody;
|
|
785
|
-
if (isBedrockModel) {
|
|
786
|
-
finalRetryBody = sanitizeForBedrock(retryBody);
|
|
787
|
-
log(`[SafetyNet] Applied Bedrock shim for ${actualModel}`);
|
|
788
|
-
}
|
|
789
704
|
// 4. Retry Fetch
|
|
790
705
|
const retryRes = await fetchWithRetry(targetUrl, {
|
|
791
706
|
method: 'POST',
|
|
792
707
|
headers: retryHeaders,
|
|
793
|
-
body: JSON.stringify(
|
|
708
|
+
body: JSON.stringify(retryBody)
|
|
794
709
|
});
|
|
795
710
|
if (retryRes.ok) {
|
|
796
711
|
res.statusCode = retryRes.status;
|