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
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
// Centraliser tous les logs dans un seul dossier temporaire
|
|
5
|
+
const LOG_DIR = path.join(os.tmpdir(), 'pollinations-plugin');
|
|
6
|
+
const LOG_FILE = path.join(LOG_DIR, 'plugin.log');
|
|
7
|
+
const API_LOG_FILE = path.join(LOG_DIR, 'api-debug.log');
|
|
8
|
+
const TOAST_LOG_FILE = path.join(LOG_DIR, 'toasts.log');
|
|
9
|
+
// Initialisation unique
|
|
10
|
+
function ensureLogDir() {
|
|
11
|
+
try {
|
|
12
|
+
if (!fs.existsSync(LOG_DIR)) {
|
|
13
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
catch { /* Silent fail — logging should never crash the app */ }
|
|
17
|
+
}
|
|
18
|
+
ensureLogDir();
|
|
19
|
+
export function log(msg, file = LOG_FILE) {
|
|
20
|
+
try {
|
|
21
|
+
ensureLogDir(); // Ensure dir exists (in case it was deleted)
|
|
22
|
+
// Censure du header Authorization (MOD-01)
|
|
23
|
+
const safeMsg = msg.replace(/(Authorization:\s*Bearer\s+)[a-zA-Z0-9.\-_]+/gi, '$1[CENSORED]');
|
|
24
|
+
fs.appendFileSync(file, `[${new Date().toISOString()}] ${safeMsg}\n`);
|
|
25
|
+
}
|
|
26
|
+
catch { }
|
|
27
|
+
}
|
|
28
|
+
export function logApi(msg) {
|
|
29
|
+
log(msg, API_LOG_FILE);
|
|
30
|
+
}
|
|
31
|
+
export function logToast(msg) {
|
|
32
|
+
log(msg, TOAST_LOG_FILE);
|
|
33
|
+
}
|
|
34
|
+
export const LOG_FILES = {
|
|
35
|
+
main: LOG_FILE,
|
|
36
|
+
api: API_LOG_FILE,
|
|
37
|
+
toast: TOAST_LOG_FILE,
|
|
38
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModelRegistry — Singleton cache for Pollinations models
|
|
3
|
+
*
|
|
4
|
+
* Central access point for all model metadata. Backed by the fetcher
|
|
5
|
+
* with a configurable TTL. Falls back to static data if fetch fails.
|
|
6
|
+
*/
|
|
7
|
+
import type { PollinationsModel, ModelCategory, ModelRegistryInterface } from './types.js';
|
|
8
|
+
declare class ModelRegistryImpl implements ModelRegistryInterface {
|
|
9
|
+
private models;
|
|
10
|
+
private lastRefresh;
|
|
11
|
+
private ttl;
|
|
12
|
+
private ready;
|
|
13
|
+
private refreshing;
|
|
14
|
+
constructor();
|
|
15
|
+
/** Get a single model by category and name */
|
|
16
|
+
get(category: ModelCategory, name: string): PollinationsModel | undefined;
|
|
17
|
+
/** Also search by alias */
|
|
18
|
+
getByNameOrAlias(category: ModelCategory, name: string): PollinationsModel | undefined;
|
|
19
|
+
/** List all models in a category */
|
|
20
|
+
list(category: ModelCategory): PollinationsModel[];
|
|
21
|
+
/** Check if registry has been populated */
|
|
22
|
+
isReady(): boolean;
|
|
23
|
+
/** Check if cache is stale */
|
|
24
|
+
isStale(): boolean;
|
|
25
|
+
/** Force refresh from API */
|
|
26
|
+
refresh(apiKey?: string): Promise<void>;
|
|
27
|
+
/** Get all models across all categories */
|
|
28
|
+
all(): PollinationsModel[];
|
|
29
|
+
/** Auto-refresh if stale (non-blocking) */
|
|
30
|
+
ensureFresh(): void;
|
|
31
|
+
/** Get count per category (for logging) */
|
|
32
|
+
stats(): Record<ModelCategory, number>;
|
|
33
|
+
}
|
|
34
|
+
export declare const ModelRegistry: ModelRegistryImpl;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModelRegistry — Singleton cache for Pollinations models
|
|
3
|
+
*
|
|
4
|
+
* Central access point for all model metadata. Backed by the fetcher
|
|
5
|
+
* with a configurable TTL. Falls back to static data if fetch fails.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { log } from '../logger.js';
|
|
10
|
+
import { loadConfig, getConfigDir } from '../config.js';
|
|
11
|
+
import { fetchAllModels } from './fetcher.js';
|
|
12
|
+
// ─── Static Fallback Data ────────────────────────────────────────────────
|
|
13
|
+
// Minimal fallback used ONLY when API is unreachable at startup.
|
|
14
|
+
// Keeps the plugin functional offline.
|
|
15
|
+
const STATIC_FALLBACK = [
|
|
16
|
+
// Image — most common
|
|
17
|
+
{ name: 'flux', description: 'Flux Schnell', category: 'image', aliases: [], pricing: { currency: 'pollen', completionImageTokens: 0.0002 }, paid_only: false, supportsI2X: false, outputType: 'image', input_modalities: ['text'], output_modalities: ['image'], costHeader: 'x-usage-completion-image-tokens' },
|
|
18
|
+
{ name: 'zimage', description: 'Z-Image Turbo', category: 'image', aliases: [], pricing: { currency: 'pollen', completionImageTokens: 0.0002 }, paid_only: false, supportsI2X: false, outputType: 'image', input_modalities: ['text'], output_modalities: ['image'], costHeader: 'x-usage-completion-image-tokens' },
|
|
19
|
+
{ name: 'klein', description: 'FLUX.2 Klein 4B', category: 'image', aliases: [], pricing: { currency: 'pollen', completionImageTokens: 0.008 }, paid_only: false, supportsI2X: true, outputType: 'image', input_modalities: ['text', 'image'], output_modalities: ['image'], costHeader: 'x-usage-completion-image-tokens' },
|
|
20
|
+
{ name: 'kontext', description: 'FLUX.1 Kontext', category: 'image', aliases: [], pricing: { currency: 'pollen', completionImageTokens: 0.04 }, paid_only: true, supportsI2X: true, outputType: 'image', input_modalities: ['text', 'image'], output_modalities: ['image'], costHeader: 'x-usage-completion-image-tokens' },
|
|
21
|
+
// Video — essential
|
|
22
|
+
{ name: 'grok-video', description: 'Grok Video', category: 'video', aliases: [], pricing: { currency: 'pollen', completionVideoSeconds: 0.0025 }, paid_only: false, supportsI2X: true, outputType: 'video', input_modalities: ['text', 'image'], output_modalities: ['video'], durationRange: [1, 15], aspectRatios: ['16:9', '9:16', '1:1', '4:3'], costHeader: 'x-usage-completion-video-seconds', genTimeEstimate: '~10s' },
|
|
23
|
+
{ name: 'veo', description: 'Veo 3.1 Fast', category: 'video', aliases: [], pricing: { currency: 'pollen', completionVideoSeconds: 0.15 }, paid_only: true, supportsI2X: true, outputType: 'video', input_modalities: ['text', 'image'], output_modalities: ['video'], durationRange: [4, 8], aspectRatios: ['16:9', '9:16', '1:1'], costHeader: 'x-usage-completion-video-seconds', genTimeEstimate: '~45-68s' },
|
|
24
|
+
// Audio — essential
|
|
25
|
+
{ name: 'elevenlabs', description: 'ElevenLabs v3 TTS', category: 'audio', aliases: [], pricing: { currency: 'pollen', completionAudioTokens: 0.00018 }, paid_only: false, supportsI2X: false, outputType: 'audio', input_modalities: ['text'], output_modalities: ['audio'] },
|
|
26
|
+
{ name: 'whisper', description: 'Whisper v3 STT', category: 'audio', aliases: [], pricing: { currency: 'pollen', promptAudioSeconds: 0.0000445 }, paid_only: false, supportsI2X: false, outputType: 'audio', input_modalities: ['audio'], output_modalities: ['text'] },
|
|
27
|
+
];
|
|
28
|
+
const DEFAULT_TTL = 60 * 60 * 1000; // 1 hour
|
|
29
|
+
function getCacheFilePath() {
|
|
30
|
+
const dir = getConfigDir();
|
|
31
|
+
if (!fs.existsSync(dir)) {
|
|
32
|
+
try {
|
|
33
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
catch (e) { }
|
|
36
|
+
}
|
|
37
|
+
return path.join(dir, 'pollinations_models_cache.json');
|
|
38
|
+
}
|
|
39
|
+
function loadCacheFromDisk() {
|
|
40
|
+
try {
|
|
41
|
+
const filePath = getCacheFilePath();
|
|
42
|
+
if (fs.existsSync(filePath)) {
|
|
43
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
44
|
+
return JSON.parse(content);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
log(`[ModelRegistry] Failed to load cache from disk: ${e}`);
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
function saveCacheToDisk(models, timestamp) {
|
|
53
|
+
try {
|
|
54
|
+
const filePath = getCacheFilePath();
|
|
55
|
+
const data = { timestamp, models };
|
|
56
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
log(`[ModelRegistry] Failed to save cache to disk: ${e}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// ─── Registry Implementation ─────────────────────────────────────────────
|
|
63
|
+
class ModelRegistryImpl {
|
|
64
|
+
models = [];
|
|
65
|
+
lastRefresh = 0;
|
|
66
|
+
ttl = DEFAULT_TTL;
|
|
67
|
+
ready = false;
|
|
68
|
+
refreshing = false;
|
|
69
|
+
constructor() {
|
|
70
|
+
const diskCache = loadCacheFromDisk();
|
|
71
|
+
if (diskCache && (Date.now() - diskCache.timestamp) < this.ttl) {
|
|
72
|
+
this.models = diskCache.models;
|
|
73
|
+
this.lastRefresh = diskCache.timestamp;
|
|
74
|
+
this.ready = true;
|
|
75
|
+
log(`[ModelRegistry] Loaded ${this.models.length} models from disk cache.`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Get a single model by category and name */
|
|
79
|
+
get(category, name) {
|
|
80
|
+
return this.models.find(m => m.category === category && m.name === name);
|
|
81
|
+
}
|
|
82
|
+
/** Also search by alias */
|
|
83
|
+
getByNameOrAlias(category, name) {
|
|
84
|
+
return this.models.find(m => m.category === category && (m.name === name || m.aliases.includes(name)));
|
|
85
|
+
}
|
|
86
|
+
/** List all models in a category */
|
|
87
|
+
list(category) {
|
|
88
|
+
return this.models.filter(m => m.category === category);
|
|
89
|
+
}
|
|
90
|
+
/** Check if registry has been populated */
|
|
91
|
+
isReady() {
|
|
92
|
+
return this.ready;
|
|
93
|
+
}
|
|
94
|
+
/** Check if cache is stale */
|
|
95
|
+
isStale() {
|
|
96
|
+
return Date.now() - this.lastRefresh > this.ttl;
|
|
97
|
+
}
|
|
98
|
+
/** Force refresh from API */
|
|
99
|
+
async refresh(apiKey) {
|
|
100
|
+
if (this.refreshing)
|
|
101
|
+
return; // Prevent concurrent refreshes
|
|
102
|
+
this.refreshing = true;
|
|
103
|
+
try {
|
|
104
|
+
const key = apiKey || loadConfig().apiKey;
|
|
105
|
+
const fetched = await fetchAllModels(key);
|
|
106
|
+
if (fetched.length > 0) {
|
|
107
|
+
this.models = fetched;
|
|
108
|
+
this.lastRefresh = Date.now();
|
|
109
|
+
this.ready = true;
|
|
110
|
+
saveCacheToDisk(this.models, this.lastRefresh);
|
|
111
|
+
log(`[ModelRegistry] Refreshed: ${this.models.length} models cached to disk.`);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
// API returned empty — keep existing data or use fallback
|
|
115
|
+
if (!this.ready) {
|
|
116
|
+
this.models = [...STATIC_FALLBACK];
|
|
117
|
+
this.ready = true;
|
|
118
|
+
log(`[ModelRegistry] API empty. Using static fallback (${STATIC_FALLBACK.length} models).`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
log(`[ModelRegistry] API returned empty, keeping existing ${this.models.length} models.`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
if (!this.ready) {
|
|
127
|
+
this.models = [...STATIC_FALLBACK];
|
|
128
|
+
this.ready = true;
|
|
129
|
+
log(`[ModelRegistry] Fetch failed, using static fallback: ${e}`);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
log(`[ModelRegistry] Refresh failed, keeping cache: ${e}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
this.refreshing = false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Get all models across all categories */
|
|
140
|
+
all() {
|
|
141
|
+
return [...this.models];
|
|
142
|
+
}
|
|
143
|
+
/** Auto-refresh if stale (non-blocking) */
|
|
144
|
+
ensureFresh() {
|
|
145
|
+
if (this.isStale()) {
|
|
146
|
+
this.refresh().catch(() => { }); // Fire-and-forget
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** Get count per category (for logging) */
|
|
150
|
+
stats() {
|
|
151
|
+
return {
|
|
152
|
+
image: this.list('image').length,
|
|
153
|
+
video: this.list('video').length,
|
|
154
|
+
audio: this.list('audio').length,
|
|
155
|
+
text: this.list('text').length,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// ─── Singleton Export ────────────────────────────────────────────────────
|
|
160
|
+
export const ModelRegistry = new ModelRegistryImpl();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model Fetcher — Dynamic model discovery from Pollinations API
|
|
3
|
+
*
|
|
4
|
+
* Fetches /image/models and /audio/models from gen.pollinations.ai,
|
|
5
|
+
* categorizes them by output_modalities, and applies local patches
|
|
6
|
+
* for data the API doesn't provide (video duration, aspect ratios, etc.).
|
|
7
|
+
*/
|
|
8
|
+
import type { PollinationsModel } from './types.js';
|
|
9
|
+
/**
|
|
10
|
+
* Fetch all models from the Pollinations API.
|
|
11
|
+
*
|
|
12
|
+
* - /image/models returns both image AND video models (sorted by output_modalities)
|
|
13
|
+
* - /audio/models returns audio models (TTS, STT, music)
|
|
14
|
+
*
|
|
15
|
+
* @param apiKey - Bearer token for authenticated endpoints
|
|
16
|
+
* @returns Array of unified PollinationsModel objects
|
|
17
|
+
*/
|
|
18
|
+
export declare function fetchAllModels(apiKey?: string): Promise<PollinationsModel[]>;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model Fetcher — Dynamic model discovery from Pollinations API
|
|
3
|
+
*
|
|
4
|
+
* Fetches /image/models and /audio/models from gen.pollinations.ai,
|
|
5
|
+
* categorizes them by output_modalities, and applies local patches
|
|
6
|
+
* for data the API doesn't provide (video duration, aspect ratios, etc.).
|
|
7
|
+
*/
|
|
8
|
+
import * as https from 'https';
|
|
9
|
+
import { log } from '../logger.js';
|
|
10
|
+
// ─── Constants ───────────────────────────────────────────────────────────
|
|
11
|
+
const API_BASE = 'gen.pollinations.ai';
|
|
12
|
+
import { getManualPatch } from './manual.js';
|
|
13
|
+
// ─── HTTP Helper ─────────────────────────────────────────────────────────
|
|
14
|
+
function fetchJson(url, headers = {}) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const req = https.get(url, { headers }, (res) => {
|
|
17
|
+
let data = '';
|
|
18
|
+
res.on('data', (chunk) => data += chunk);
|
|
19
|
+
res.on('end', () => {
|
|
20
|
+
try {
|
|
21
|
+
resolve(JSON.parse(data));
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
log(`[ModelFetcher] JSON parse error for ${url}: ${e}`);
|
|
25
|
+
resolve([]);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
req.on('error', (e) => {
|
|
30
|
+
log(`[ModelFetcher] Network error for ${url}: ${e.message}`);
|
|
31
|
+
reject(e);
|
|
32
|
+
});
|
|
33
|
+
req.setTimeout(8000, () => {
|
|
34
|
+
req.destroy();
|
|
35
|
+
reject(new Error('Timeout'));
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
// ─── Category Detection ──────────────────────────────────────────────────
|
|
40
|
+
function detectCategory(raw) {
|
|
41
|
+
const outputs = raw.output_modalities || [];
|
|
42
|
+
if (outputs.includes('video'))
|
|
43
|
+
return 'video';
|
|
44
|
+
if (outputs.includes('image'))
|
|
45
|
+
return 'image';
|
|
46
|
+
if (outputs.includes('audio'))
|
|
47
|
+
return 'audio';
|
|
48
|
+
// Correctif : Forcer les modèles Speech-to-Text en audio même si leur sortie API est 'text'
|
|
49
|
+
const nameStr = (raw.name || raw.id || '').toLowerCase();
|
|
50
|
+
if (nameStr.includes('whisper') || nameStr.includes('scribe'))
|
|
51
|
+
return 'audio';
|
|
52
|
+
return 'text';
|
|
53
|
+
}
|
|
54
|
+
function detectOutputType(raw) {
|
|
55
|
+
return detectCategory(raw);
|
|
56
|
+
}
|
|
57
|
+
// ─── Model Mapping ──────────────────────────────────────────────────────
|
|
58
|
+
function mapRawToModel(raw, fallbackCategory, averageCost) {
|
|
59
|
+
const category = detectCategory(raw);
|
|
60
|
+
const inputMods = raw.input_modalities || ['text'];
|
|
61
|
+
const outputMods = raw.output_modalities || ['text'];
|
|
62
|
+
const pricing = {
|
|
63
|
+
currency: raw.pricing?.currency || 'pollen',
|
|
64
|
+
...(raw.pricing || {}),
|
|
65
|
+
};
|
|
66
|
+
const model = {
|
|
67
|
+
name: raw.name || raw.id || 'unknown',
|
|
68
|
+
description: raw.description || raw.name || '',
|
|
69
|
+
category,
|
|
70
|
+
aliases: raw.aliases || [],
|
|
71
|
+
pricing,
|
|
72
|
+
paid_only: raw.paid_only === true,
|
|
73
|
+
supportsI2X: inputMods.includes('image'),
|
|
74
|
+
outputType: detectOutputType(raw),
|
|
75
|
+
input_modalities: inputMods,
|
|
76
|
+
output_modalities: outputMods,
|
|
77
|
+
voices: raw.voices,
|
|
78
|
+
tools: raw.tools,
|
|
79
|
+
reasoning: raw.reasoning,
|
|
80
|
+
is_specialized: raw.is_specialized,
|
|
81
|
+
context_window: raw.context_window,
|
|
82
|
+
averageCost: averageCost !== undefined && !isNaN(averageCost) ? averageCost : undefined,
|
|
83
|
+
};
|
|
84
|
+
// Apply local patches from manual.ts
|
|
85
|
+
const patch = getManualPatch(category, model.name);
|
|
86
|
+
if (patch) {
|
|
87
|
+
Object.assign(model, patch);
|
|
88
|
+
}
|
|
89
|
+
return model;
|
|
90
|
+
}
|
|
91
|
+
// ─── Main Fetch Function ─────────────────────────────────────────────────
|
|
92
|
+
/**
|
|
93
|
+
* Fetch all models from the Pollinations API.
|
|
94
|
+
*
|
|
95
|
+
* - /image/models returns both image AND video models (sorted by output_modalities)
|
|
96
|
+
* - /audio/models returns audio models (TTS, STT, music)
|
|
97
|
+
*
|
|
98
|
+
* @param apiKey - Bearer token for authenticated endpoints
|
|
99
|
+
* @returns Array of unified PollinationsModel objects
|
|
100
|
+
*/
|
|
101
|
+
export async function fetchAllModels(apiKey) {
|
|
102
|
+
const headers = {};
|
|
103
|
+
if (apiKey && apiKey.length > 5 && apiKey !== 'dummy') {
|
|
104
|
+
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
105
|
+
}
|
|
106
|
+
const results = [];
|
|
107
|
+
const seen = new Set();
|
|
108
|
+
const endpoints = [
|
|
109
|
+
{ url: `https://${API_BASE}/image/models`, fallbackCategory: 'image' },
|
|
110
|
+
{ url: `https://${API_BASE}/audio/models`, fallbackCategory: 'audio' },
|
|
111
|
+
{ url: `https://${API_BASE}/text/models`, fallbackCategory: 'text' },
|
|
112
|
+
];
|
|
113
|
+
const statsPromise = fetchJson('https://enter.pollinations.ai/api/model-stats', headers).catch(() => ({ data: [] }));
|
|
114
|
+
const openapiPromise = fetchJson('https://enter.pollinations.ai/api/docs/open-api/generate-schema', headers).catch(() => ({}));
|
|
115
|
+
const fetches = endpoints.map(async ({ url, fallbackCategory }) => {
|
|
116
|
+
try {
|
|
117
|
+
const raw = await fetchJson(url, headers);
|
|
118
|
+
return { url, fallbackCategory, raw };
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
log(`[ModelFetcher] Failed to fetch ${url}: ${e}`);
|
|
122
|
+
return { url, fallbackCategory, raw: [] };
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
const resultsRaw = await Promise.all([...fetches, statsPromise, openapiPromise]);
|
|
126
|
+
const openapiRaw = resultsRaw.pop();
|
|
127
|
+
const statsRaw = resultsRaw.pop();
|
|
128
|
+
const statsList = Array.isArray(statsRaw?.data) ? statsRaw.data : [];
|
|
129
|
+
const statsMap = new Map();
|
|
130
|
+
for (const s of statsList) {
|
|
131
|
+
if (s.model && s.avg_cost_usd !== undefined) {
|
|
132
|
+
statsMap.set(s.model, parseFloat(s.avg_cost_usd));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const res of resultsRaw) {
|
|
136
|
+
const list = Array.isArray(res.raw) ? res.raw : (res.raw.data || []);
|
|
137
|
+
for (const item of list) {
|
|
138
|
+
const modelId = item.name || item.id;
|
|
139
|
+
const avgCost = statsMap.get(modelId);
|
|
140
|
+
const model = mapRawToModel(item, res.fallbackCategory, avgCost);
|
|
141
|
+
const uniqueId = model.name;
|
|
142
|
+
if (!seen.has(uniqueId)) {
|
|
143
|
+
seen.add(uniqueId);
|
|
144
|
+
results.push(model);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
log(`[ModelFetcher] Parsed ${list.length} models from ${res.url}`);
|
|
148
|
+
}
|
|
149
|
+
log(`[ModelFetcher] Total: ${results.length} models (${results.filter(m => m.category === 'image').length} image, ${results.filter(m => m.category === 'video').length} video, ${results.filter(m => m.category === 'audio').length} audio, ${results.filter(m => m.category === 'text').length} text)`);
|
|
150
|
+
// --- ENRICH MODELS WITH OPENAPI CONSTRAINTS ---
|
|
151
|
+
try {
|
|
152
|
+
const videoParams = openapiRaw?.paths?.['/video/{prompt}']?.get?.parameters || [];
|
|
153
|
+
const durationParam = videoParams.find((p) => p.name === 'duration');
|
|
154
|
+
const durationDesc = durationParam?.description || '';
|
|
155
|
+
const audioParams = openapiRaw?.paths?.['/audio/{text}']?.get?.parameters || [];
|
|
156
|
+
const musicParam = audioParams.find((p) => p.name === 'duration');
|
|
157
|
+
const musicDesc = musicParam?.description || '';
|
|
158
|
+
for (const model of results) {
|
|
159
|
+
if (model.category === 'video' && durationDesc) {
|
|
160
|
+
const regex = new RegExp(`${model.name}:\\s*([^.]+)s`, 'i');
|
|
161
|
+
const match = durationDesc.match(regex);
|
|
162
|
+
if (match) {
|
|
163
|
+
const constraint = match[1].toLowerCase();
|
|
164
|
+
if (constraint.includes('or')) {
|
|
165
|
+
const numbers = constraint.match(/\d+/g)?.map(Number) || [];
|
|
166
|
+
if (numbers.length > 0)
|
|
167
|
+
model.durationRange = [Math.min(...numbers), Math.max(...numbers)];
|
|
168
|
+
}
|
|
169
|
+
else if (constraint.match(/(\d+)\s*(?:-|to)\s*(\d+)/)) {
|
|
170
|
+
const m = constraint.match(/(\d+)\s*(?:-|to)\s*(\d+)/);
|
|
171
|
+
if (m)
|
|
172
|
+
model.durationRange = [Number(m[1]), Number(m[2])];
|
|
173
|
+
}
|
|
174
|
+
else if (constraint.match(/(?:up to\s*~?|max\s*)\s*(\d+)/)) {
|
|
175
|
+
const m = constraint.match(/(?:up to\s*~?|max\s*)\s*(\d+)/);
|
|
176
|
+
if (m)
|
|
177
|
+
model.durationRange = [1, Number(m[1])];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (model.name === 'elevenmusic' && musicDesc) {
|
|
182
|
+
const boundsMatch = musicDesc.match(/(\d+)\s*-\s*(\d+)/);
|
|
183
|
+
if (boundsMatch) {
|
|
184
|
+
model.durationRange = [Number(boundsMatch[1]), Number(boundsMatch[2])];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
log(`[ModelFetcher] Failed to parse OpenAPI constraints: ${e}`);
|
|
191
|
+
}
|
|
192
|
+
// ----------------------------------------------
|
|
193
|
+
return results;
|
|
194
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manual Model Registry (Overrides)
|
|
3
|
+
*
|
|
4
|
+
* Defines local patches for models that the dynamic API does not provide.
|
|
5
|
+
* These patches are deeply merged into the dynamic registry at runtime.
|
|
6
|
+
*/
|
|
7
|
+
import type { PollinationsModel } from './types.js';
|
|
8
|
+
export interface ManualOverride {
|
|
9
|
+
name: string;
|
|
10
|
+
category: PollinationsModel['category'];
|
|
11
|
+
patch: Partial<PollinationsModel>;
|
|
12
|
+
}
|
|
13
|
+
export declare const MANUAL_OVERRIDES: ManualOverride[];
|
|
14
|
+
export declare function getManualOverrides(category: PollinationsModel['category']): ManualOverride[];
|
|
15
|
+
export declare function getManualPatch(category: PollinationsModel['category'], name: string): Partial<PollinationsModel> | undefined;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manual Model Registry (Overrides)
|
|
3
|
+
*
|
|
4
|
+
* Defines local patches for models that the dynamic API does not provide.
|
|
5
|
+
* These patches are deeply merged into the dynamic registry at runtime.
|
|
6
|
+
*/
|
|
7
|
+
export const MANUAL_OVERRIDES = [
|
|
8
|
+
// Video Models
|
|
9
|
+
{
|
|
10
|
+
name: 'grok-video',
|
|
11
|
+
category: 'video',
|
|
12
|
+
patch: {
|
|
13
|
+
durationRange: [1, 15],
|
|
14
|
+
aspectRatios: ['16:9', '9:16', '1:1', '4:3'],
|
|
15
|
+
costHeader: 'x-usage-completion-video-seconds',
|
|
16
|
+
genTimeEstimate: '~10s',
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'ltx-2',
|
|
21
|
+
category: 'video',
|
|
22
|
+
patch: {
|
|
23
|
+
durationRange: [5, 20],
|
|
24
|
+
aspectRatios: ['16:9'],
|
|
25
|
+
costHeader: 'x-usage-completion-video-seconds',
|
|
26
|
+
genTimeEstimate: '~35s',
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'wan',
|
|
31
|
+
category: 'video',
|
|
32
|
+
patch: {
|
|
33
|
+
durationRange: [5, 10], // API validates max 10
|
|
34
|
+
aspectRatios: ['16:9', '9:16', '1:1', '4:3'],
|
|
35
|
+
costHeader: 'x-usage-completion-video-seconds',
|
|
36
|
+
genTimeEstimate: '~30-60s',
|
|
37
|
+
input_modalities: ['image', 'text'],
|
|
38
|
+
description: 'Powerful video model. 💡 Text-to-Video Hack: To use in T2V mode, you MUST provide a dummy blank image URL in the `image` parameter (e.g., https://dummyimage.com/1280x720/000/000.jpg) matching your desired aspect ratio.'
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'veo',
|
|
43
|
+
category: 'video',
|
|
44
|
+
patch: {
|
|
45
|
+
durationRange: [4, 8],
|
|
46
|
+
aspectRatios: ['16:9', '9:16', '1:1'],
|
|
47
|
+
costHeader: 'x-usage-completion-video-seconds',
|
|
48
|
+
genTimeEstimate: '~45-68s',
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'seedance',
|
|
53
|
+
category: 'video',
|
|
54
|
+
patch: {
|
|
55
|
+
durationRange: [4, 12],
|
|
56
|
+
aspectRatios: ['16:9', '9:16', '1:1'],
|
|
57
|
+
costHeader: 'x-usage-completion-video-tokens',
|
|
58
|
+
genTimeEstimate: '~30s',
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'seedance-pro',
|
|
63
|
+
category: 'video',
|
|
64
|
+
patch: {
|
|
65
|
+
durationRange: [4, 12],
|
|
66
|
+
aspectRatios: ['16:9', '9:16', '1:1'],
|
|
67
|
+
costHeader: 'x-usage-completion-video-tokens',
|
|
68
|
+
genTimeEstimate: '~30s',
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
// Image Models
|
|
72
|
+
{
|
|
73
|
+
name: 'kontext',
|
|
74
|
+
category: 'image',
|
|
75
|
+
patch: {
|
|
76
|
+
costHeader: 'x-usage-completion-image-tokens',
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'klein',
|
|
81
|
+
category: 'image',
|
|
82
|
+
patch: {
|
|
83
|
+
costHeader: 'x-usage-completion-image-tokens',
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
];
|
|
87
|
+
export function getManualOverrides(category) {
|
|
88
|
+
return MANUAL_OVERRIDES.filter(o => o.category === category);
|
|
89
|
+
}
|
|
90
|
+
export function getManualPatch(category, name) {
|
|
91
|
+
return MANUAL_OVERRIDES.find(o => o.category === category && o.name === name)?.patch;
|
|
92
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Model Types for ModelRegistry
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for all Pollinations model metadata.
|
|
5
|
+
* Replaces the fragmented hardcoded constants in shared.ts.
|
|
6
|
+
*/
|
|
7
|
+
export type ModelCategory = 'image' | 'video' | 'audio' | 'text';
|
|
8
|
+
export interface ModelPricing {
|
|
9
|
+
currency: string;
|
|
10
|
+
completionImageTokens?: number;
|
|
11
|
+
completionVideoSeconds?: number;
|
|
12
|
+
completionVideoTokens?: number;
|
|
13
|
+
completionAudioTokens?: number;
|
|
14
|
+
completionAudioSeconds?: number;
|
|
15
|
+
promptAudioTokens?: number;
|
|
16
|
+
promptAudioSeconds?: number;
|
|
17
|
+
promptTextTokens?: number;
|
|
18
|
+
promptCachedTokens?: number;
|
|
19
|
+
promptImageTokens?: number;
|
|
20
|
+
completionTextTokens?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface PollinationsModel {
|
|
23
|
+
name: string;
|
|
24
|
+
description: string;
|
|
25
|
+
category: ModelCategory;
|
|
26
|
+
aliases: string[];
|
|
27
|
+
pricing: ModelPricing;
|
|
28
|
+
paid_only: boolean;
|
|
29
|
+
supportsI2X: boolean;
|
|
30
|
+
outputType: ModelCategory;
|
|
31
|
+
voices?: string[];
|
|
32
|
+
tools?: boolean;
|
|
33
|
+
reasoning?: boolean;
|
|
34
|
+
is_specialized?: boolean;
|
|
35
|
+
context_window?: number;
|
|
36
|
+
input_modalities: string[];
|
|
37
|
+
output_modalities: string[];
|
|
38
|
+
durationRange?: [number, number];
|
|
39
|
+
aspectRatios?: string[];
|
|
40
|
+
costHeader?: string;
|
|
41
|
+
genTimeEstimate?: string;
|
|
42
|
+
averageCost?: number;
|
|
43
|
+
}
|
|
44
|
+
export interface ModelRegistryInterface {
|
|
45
|
+
/** Get a single model by category and name */
|
|
46
|
+
get(category: ModelCategory, name: string): PollinationsModel | undefined;
|
|
47
|
+
/** List all models in a category */
|
|
48
|
+
list(category: ModelCategory): PollinationsModel[];
|
|
49
|
+
/** Check if registry has been populated */
|
|
50
|
+
isReady(): boolean;
|
|
51
|
+
/** Force refresh from API */
|
|
52
|
+
refresh(apiKey?: string): Promise<void>;
|
|
53
|
+
/** Get all models across all categories */
|
|
54
|
+
all(): PollinationsModel[];
|
|
55
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ToolRegistryWorker
|
|
3
|
+
* Responsabilité : Patcher dynamiquement la propriété "description" des Tools statiques
|
|
4
|
+
* (ex: polliGenImageTool, polliGenVideoTool) pour y injecter le catalogue temps réel
|
|
5
|
+
* rapatrié par le ModelRegistry.
|
|
6
|
+
*/
|
|
7
|
+
export declare class ToolRegistryWorker {
|
|
8
|
+
private static isRunning;
|
|
9
|
+
private static interval;
|
|
10
|
+
private static isPatching;
|
|
11
|
+
private static CHECK_INTERVAL;
|
|
12
|
+
/**
|
|
13
|
+
* Démarre le Worker en tâche de fond.
|
|
14
|
+
*/
|
|
15
|
+
static start(): void;
|
|
16
|
+
static stop(): void;
|
|
17
|
+
/**
|
|
18
|
+
* Lit le registre en mémoire et injecte les listes Markdown formatées
|
|
19
|
+
* dans les propriétés ToolDefinition.description
|
|
20
|
+
*/
|
|
21
|
+
private static patchTools;
|
|
22
|
+
}
|