opencode-pollinations-plugin 6.4.9 → 6.5.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 +67 -54
- package/README.es.md +79 -66
- package/README.fr.md +70 -57
- package/README.it.md +78 -65
- package/README.md +33 -29
- package/README.zh.md +77 -64
- package/dist/locales/de.json +82 -47
- package/dist/locales/en.json +84 -49
- package/dist/locales/es.json +82 -47
- package/dist/locales/fr.json +81 -46
- package/dist/locales/it.json +82 -47
- package/dist/locales/zh.json +82 -47
- package/dist/server/commands.js +118 -178
- package/dist/server/config.d.ts +22 -6
- package/dist/server/config.js +45 -5
- package/dist/server/connect-response.js +7 -7
- package/dist/server/generate-config.js +2 -2
- package/dist/server/models/cache.d.ts +23 -10
- package/dist/server/models/cache.js +46 -24
- package/dist/server/models/fetcher.js +2 -0
- package/dist/server/models/types.d.ts +2 -0
- package/dist/server/models/worker.js +4 -4
- package/dist/server/proxy.d.ts +6 -0
- package/dist/server/proxy.js +318 -218
- package/dist/server/quota.d.ts +23 -32
- package/dist/server/quota.js +44 -184
- package/dist/server/scripts/pollinations_pricing.js +6 -3
- package/dist/server/status.js +1 -2
- package/dist/server/toast.js +1 -1
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/pollinations/artifact-core.d.ts +53 -0
- package/dist/tools/pollinations/artifact-core.js +159 -0
- package/dist/tools/pollinations/beta_discovery.js +2 -1
- package/dist/tools/pollinations/cost-guard.d.ts +2 -2
- package/dist/tools/pollinations/error-parser.d.ts +38 -0
- package/dist/tools/pollinations/error-parser.js +112 -0
- package/dist/tools/pollinations/gen_3d.d.ts +17 -0
- package/dist/tools/pollinations/gen_3d.js +207 -0
- package/dist/tools/pollinations/gen_image.js +29 -10
- package/dist/tools/pollinations/gen_music.js +3 -2
- package/dist/tools/pollinations/gen_video.js +13 -2
- package/dist/tools/pollinations/polli_config.js +15 -21
- package/dist/tools/pollinations/polli_gen_confirm.js +2 -0
- package/dist/tools/pollinations/shared.d.ts +1 -1
- package/dist/tools/pollinations/shared.js +53 -142
- package/dist/tools/pollinations/timeout-policy.d.ts +80 -0
- package/dist/tools/pollinations/timeout-policy.js +124 -0
- package/dist/tools/pollinations/tool-capability-registry.d.ts +51 -0
- package/dist/tools/pollinations/tool-capability-registry.js +215 -0
- package/dist/tools/pollinations/transcribe_audio.js +5 -24
- package/package.json +64 -62
- package/dist/server/tier-info.d.ts +0 -36
- package/dist/server/tier-info.js +0 -107
|
@@ -36,7 +36,8 @@ async function fetchOpenApiSchema() {
|
|
|
36
36
|
if (cachedSchema)
|
|
37
37
|
return cachedSchema;
|
|
38
38
|
try {
|
|
39
|
-
|
|
39
|
+
// v6.5: bound the OpenAPI fetch (was unbounded → hang risk).
|
|
40
|
+
const response = await fetch(OPENAPI_URL, { signal: AbortSignal.timeout(10000) });
|
|
40
41
|
if (!response.ok)
|
|
41
42
|
throw new Error(`HTTP ${response.status}`);
|
|
42
43
|
cachedSchema = await response.json();
|
|
@@ -24,7 +24,7 @@ export interface PendingRequest {
|
|
|
24
24
|
export declare function savePendingRequest(req: PendingRequest): void;
|
|
25
25
|
export declare function getPendingRequest(id: string): PendingRequest | null;
|
|
26
26
|
export declare function removePendingRequest(id: string): void;
|
|
27
|
-
export declare function isTokenBased(category: 'image' | 'video' | 'audio' | 'text', modelName: string): boolean;
|
|
27
|
+
export declare function isTokenBased(category: 'image' | 'video' | 'audio' | 'text' | '3d', modelName: string): boolean;
|
|
28
28
|
/**
|
|
29
29
|
* Check if a generation should proceed based on cost control settings.
|
|
30
30
|
*
|
|
@@ -35,4 +35,4 @@ export declare function isTokenBased(category: 'image' | 'video' | 'audio' | 'te
|
|
|
35
35
|
* @param category - The model category ('image' | 'video' | 'audio')
|
|
36
36
|
* @returns CostCheckResult
|
|
37
37
|
*/
|
|
38
|
-
export declare function checkCostControl(toolName: string, args: any, modelName: string, estimatedCost: number, category?: 'image' | 'video' | 'audio'): CostCheckResult;
|
|
38
|
+
export declare function checkCostControl(toolName: string, args: any, modelName: string, estimatedCost: number, category?: 'image' | 'video' | 'audio' | '3d'): CostCheckResult;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured error parsing (v6.5) — single parser for Pollinations error
|
|
3
|
+
* envelopes instead of `includes('402')` scattered across tools.
|
|
4
|
+
*
|
|
5
|
+
* Live envelope (Phase 2 T11):
|
|
6
|
+
* { "success": false, "code": "BAD_REQUEST", "timestamp": "...",
|
|
7
|
+
* "details": { "name": "UpstreamError", "upstreamStatus": 400,
|
|
8
|
+
* "upstreamHost": "...", "upstreamBody": "..." } }
|
|
9
|
+
*
|
|
10
|
+
* Safety: `upstreamHost` / `upstreamBody` reveal the real backend and are
|
|
11
|
+
* NEVER exposed to the user — kept only in the sanitized debug fields.
|
|
12
|
+
*/
|
|
13
|
+
export type PolliErrorKind = 'payment' | 'auth' | 'rate_limit' | 'bad_request' | 'not_found' | 'upstream' | 'timeout' | 'network' | 'unknown';
|
|
14
|
+
export interface ParsedPolliError {
|
|
15
|
+
kind: PolliErrorKind;
|
|
16
|
+
code?: string;
|
|
17
|
+
status?: number;
|
|
18
|
+
message: string;
|
|
19
|
+
/** Sanitized debug info (backend host/body redacted). */
|
|
20
|
+
debug?: {
|
|
21
|
+
upstreamStatus?: number;
|
|
22
|
+
upstreamHost?: string;
|
|
23
|
+
upstreamBodyTruncated?: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Map an HTTP status code to an error kind. */
|
|
27
|
+
export declare function kindForStatus(status: number): PolliErrorKind;
|
|
28
|
+
/**
|
|
29
|
+
* Parse an error body string (or object) into a structured error.
|
|
30
|
+
* Handles the Pollinations envelope, OpenAI-style {error:{message,code}},
|
|
31
|
+
* and plain text. upstreamHost/upstreamBody are never exposed in `message`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parsePolliError(body: string | Record<string, any> | null | undefined, status?: number): ParsedPolliError;
|
|
34
|
+
/**
|
|
35
|
+
* Parse a thrown error (Error object or string) into a structured error.
|
|
36
|
+
* Detects the envelope inside messages like "HTTP 402: {...json...}".
|
|
37
|
+
*/
|
|
38
|
+
export declare function parsePolliErrorFromThrow(error: unknown): ParsedPolliError;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured error parsing (v6.5) — single parser for Pollinations error
|
|
3
|
+
* envelopes instead of `includes('402')` scattered across tools.
|
|
4
|
+
*
|
|
5
|
+
* Live envelope (Phase 2 T11):
|
|
6
|
+
* { "success": false, "code": "BAD_REQUEST", "timestamp": "...",
|
|
7
|
+
* "details": { "name": "UpstreamError", "upstreamStatus": 400,
|
|
8
|
+
* "upstreamHost": "...", "upstreamBody": "..." } }
|
|
9
|
+
*
|
|
10
|
+
* Safety: `upstreamHost` / `upstreamBody` reveal the real backend and are
|
|
11
|
+
* NEVER exposed to the user — kept only in the sanitized debug fields.
|
|
12
|
+
*/
|
|
13
|
+
/** Map an HTTP status code to an error kind. */
|
|
14
|
+
export function kindForStatus(status) {
|
|
15
|
+
if (status === 402)
|
|
16
|
+
return 'payment';
|
|
17
|
+
if (status === 401 || status === 403)
|
|
18
|
+
return 'auth';
|
|
19
|
+
if (status === 429)
|
|
20
|
+
return 'rate_limit';
|
|
21
|
+
if (status === 400 || status === 422)
|
|
22
|
+
return 'bad_request';
|
|
23
|
+
if (status === 404)
|
|
24
|
+
return 'not_found';
|
|
25
|
+
if (status >= 500)
|
|
26
|
+
return 'upstream';
|
|
27
|
+
return 'unknown';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Parse an error body string (or object) into a structured error.
|
|
31
|
+
* Handles the Pollinations envelope, OpenAI-style {error:{message,code}},
|
|
32
|
+
* and plain text. upstreamHost/upstreamBody are never exposed in `message`.
|
|
33
|
+
*/
|
|
34
|
+
export function parsePolliError(body, status) {
|
|
35
|
+
const kind = status !== undefined && status !== 0 ? kindForStatus(status) : 'unknown';
|
|
36
|
+
if (body === null || body === undefined || body === '') {
|
|
37
|
+
return { kind, status, message: `HTTP ${status ?? 'error'}` };
|
|
38
|
+
}
|
|
39
|
+
let parsed = null;
|
|
40
|
+
if (typeof body === 'string') {
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(body);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
parsed = null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
parsed = body;
|
|
50
|
+
}
|
|
51
|
+
if (parsed && typeof parsed === 'object') {
|
|
52
|
+
// Pollinations envelope
|
|
53
|
+
if (parsed.success === false && (parsed.code || parsed.details)) {
|
|
54
|
+
const details = parsed.details || {};
|
|
55
|
+
const cleanMessage = typeof parsed.message === 'string'
|
|
56
|
+
? parsed.message
|
|
57
|
+
: (parsed.code || 'Upstream error');
|
|
58
|
+
return {
|
|
59
|
+
kind,
|
|
60
|
+
code: parsed.code,
|
|
61
|
+
status,
|
|
62
|
+
message: `${cleanMessage}${details.upstreamStatus !== undefined ? ` (upstream ${details.upstreamStatus})` : ''}`,
|
|
63
|
+
debug: {
|
|
64
|
+
upstreamStatus: details.upstreamStatus,
|
|
65
|
+
upstreamHost: typeof details.upstreamHost === 'string' ? details.upstreamHost : undefined,
|
|
66
|
+
upstreamBodyTruncated: typeof details.upstreamBody === 'string' ? details.upstreamBody.slice(0, 200) : undefined,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// OpenAI-style
|
|
71
|
+
if (parsed.error && typeof parsed.error === 'object') {
|
|
72
|
+
return {
|
|
73
|
+
kind,
|
|
74
|
+
code: parsed.error.code,
|
|
75
|
+
status,
|
|
76
|
+
message: parsed.error.message || `HTTP ${status ?? 'error'}`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (typeof parsed.message === 'string') {
|
|
80
|
+
return { kind, status, message: parsed.message };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Plain text
|
|
84
|
+
const text = typeof body === 'string' ? body.slice(0, 300) : JSON.stringify(body).slice(0, 300);
|
|
85
|
+
return { kind, status, message: text };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Parse a thrown error (Error object or string) into a structured error.
|
|
89
|
+
* Detects the envelope inside messages like "HTTP 402: {...json...}".
|
|
90
|
+
*/
|
|
91
|
+
export function parsePolliErrorFromThrow(error) {
|
|
92
|
+
if (error instanceof Error) {
|
|
93
|
+
const msg = error.message || '';
|
|
94
|
+
// Timeout markers
|
|
95
|
+
if (/timeout/i.test(msg) && !/HTTP \d/.test(msg)) {
|
|
96
|
+
return { kind: 'timeout', message: msg };
|
|
97
|
+
}
|
|
98
|
+
// "HTTP 402: {envelope}" pattern from shared.ts httpsGet
|
|
99
|
+
const httpMatch = msg.match(/HTTP (\d{3})/);
|
|
100
|
+
if (httpMatch) {
|
|
101
|
+
const status = parseInt(httpMatch[1], 10);
|
|
102
|
+
const body = msg.slice(msg.indexOf(':', msg.indexOf(httpMatch[0])) + 1).trim();
|
|
103
|
+
const parsed = parsePolliError(body || null, status);
|
|
104
|
+
return parsed;
|
|
105
|
+
}
|
|
106
|
+
if (/Network Error|fetch failed|ECONN|ETIMEDOUT/i.test(msg)) {
|
|
107
|
+
return { kind: 'network', message: msg };
|
|
108
|
+
}
|
|
109
|
+
return { kind: 'unknown', message: msg };
|
|
110
|
+
}
|
|
111
|
+
return { kind: 'unknown', message: String(error) };
|
|
112
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* polli_gen_3d Tool — Pollinations 3D Generation (v6.5)
|
|
3
|
+
*
|
|
4
|
+
* Models (live-validated Phase 2):
|
|
5
|
+
* - trellis-2 — default, ~0.24 pollen (low), GLB, LONG_BLOCKING
|
|
6
|
+
* - hyper3d-rodin — paid_only, ~0.10 pollen, GLB, LONG_BLOCKING
|
|
7
|
+
*
|
|
8
|
+
* Endpoint: GET /3d/{prompt}?model=...&resolution=...&image=<url>&seed=...
|
|
9
|
+
* Artifact: GLB (glTF-binary) — validated via magic bytes, never written
|
|
10
|
+
* with a wrong extension.
|
|
11
|
+
*
|
|
12
|
+
* Retry policy: after a client timeout the generation may STILL be running
|
|
13
|
+
* and billed upstream. We never auto-resubmit; we return recovery metadata
|
|
14
|
+
* (same request = cache hit, not rebilled).
|
|
15
|
+
*/
|
|
16
|
+
import { type ToolDefinition } from '@opencode-ai/plugin/tool';
|
|
17
|
+
export declare const polliGen3dTool: ToolDefinition;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* polli_gen_3d Tool — Pollinations 3D Generation (v6.5)
|
|
3
|
+
*
|
|
4
|
+
* Models (live-validated Phase 2):
|
|
5
|
+
* - trellis-2 — default, ~0.24 pollen (low), GLB, LONG_BLOCKING
|
|
6
|
+
* - hyper3d-rodin — paid_only, ~0.10 pollen, GLB, LONG_BLOCKING
|
|
7
|
+
*
|
|
8
|
+
* Endpoint: GET /3d/{prompt}?model=...&resolution=...&image=<url>&seed=...
|
|
9
|
+
* Artifact: GLB (glTF-binary) — validated via magic bytes, never written
|
|
10
|
+
* with a wrong extension.
|
|
11
|
+
*
|
|
12
|
+
* Retry policy: after a client timeout the generation may STILL be running
|
|
13
|
+
* and billed upstream. We never auto-resubmit; we return recovery metadata
|
|
14
|
+
* (same request = cache hit, not rebilled).
|
|
15
|
+
*/
|
|
16
|
+
import { tool } from '@opencode-ai/plugin/tool';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
import { getApiKey, hasApiKey, httpsGet, ensureDir, generateFilename, getDefaultOutputDir, formatCost, formatFileSize, extractCostFromHeaders, isCostEstimatorEnabled, fetchEnterBalance, sanitizeFilename, validateHttpUrl, } from './shared.js';
|
|
19
|
+
import { ModelRegistry } from '../../server/models/index.js';
|
|
20
|
+
import { checkCostControl } from './cost-guard.js';
|
|
21
|
+
import { detectArtifactType, persistArtifact } from './artifact-core.js';
|
|
22
|
+
import { resolveCapabilityTimeout } from './tool-capability-registry.js';
|
|
23
|
+
import { validateTimeoutSeconds } from './timeout-policy.js';
|
|
24
|
+
import { loadConfig } from '../../server/config.js';
|
|
25
|
+
import { emitStatusToast } from '../../server/toast.js';
|
|
26
|
+
import { t } from '../../locales/index.js';
|
|
27
|
+
// ─── Constants ─────────────────────────────────────────────────────────────
|
|
28
|
+
const DEFAULT_MODEL = 'trellis-2';
|
|
29
|
+
const DEFAULT_RESOLUTION = 'low';
|
|
30
|
+
const VALID_RESOLUTIONS = ['low', 'medium', 'high'];
|
|
31
|
+
// Live-observed flat costs (Phase 2 / 2.2). Registry pricing wins when present.
|
|
32
|
+
const THREE_D_COST_FALLBACK = {
|
|
33
|
+
'trellis-2': 0.24,
|
|
34
|
+
'hyper3d-rodin': 0.10,
|
|
35
|
+
};
|
|
36
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
37
|
+
function estimate3DCost(model) {
|
|
38
|
+
const m = ModelRegistry.getByNameOrAlias('3d', model);
|
|
39
|
+
if (m?.pricing?.completionImageTokens !== undefined)
|
|
40
|
+
return m.pricing.completionImageTokens;
|
|
41
|
+
if (m?.averageCost !== undefined)
|
|
42
|
+
return m.averageCost;
|
|
43
|
+
return THREE_D_COST_FALLBACK[model] ?? 0.24;
|
|
44
|
+
}
|
|
45
|
+
// ─── Tool Definition ──────────────────────────────────────────────────────
|
|
46
|
+
export const polliGen3dTool = tool({
|
|
47
|
+
description: t('tools.gen3d.desc'),
|
|
48
|
+
args: {
|
|
49
|
+
prompt: tool.schema.string().optional().describe(t('tools.gen3d.arg_prompt')),
|
|
50
|
+
image: tool.schema.string().optional().describe(t('tools.gen3d.arg_image')),
|
|
51
|
+
model: tool.schema.string().optional().describe(t('tools.gen3d.arg_model', { model: DEFAULT_MODEL })),
|
|
52
|
+
resolution: tool.schema.enum(VALID_RESOLUTIONS).optional().describe(t('tools.gen3d.arg_resolution', { res: DEFAULT_RESOLUTION })),
|
|
53
|
+
seed: tool.schema.number().optional().describe(t('tools.gen3d.arg_seed')),
|
|
54
|
+
save_to: tool.schema.string().optional().describe(t('tools.gen3d.arg_save_to')),
|
|
55
|
+
filename: tool.schema.string().optional().describe(t('tools.gen3d.arg_filename')),
|
|
56
|
+
timeout_seconds: tool.schema.number().optional().describe(t('tools.gen3d.arg_timeout')),
|
|
57
|
+
},
|
|
58
|
+
async execute(args, context) {
|
|
59
|
+
const apiKey = getApiKey();
|
|
60
|
+
if (!hasApiKey()) {
|
|
61
|
+
return t('tools.gen3d.req_key');
|
|
62
|
+
}
|
|
63
|
+
const model = args.model || DEFAULT_MODEL;
|
|
64
|
+
const resolution = args.resolution || DEFAULT_RESOLUTION;
|
|
65
|
+
const prompt = args.prompt || (args.image ? '3d model from reference image' : 'a simple 3d object');
|
|
66
|
+
// Known model check (beta passthrough for unknown ids)
|
|
67
|
+
const knownModels = ModelRegistry.list('3d');
|
|
68
|
+
const knownModel = knownModels.some(m => m.name === model || m.aliases.includes(model));
|
|
69
|
+
if (!knownModel) {
|
|
70
|
+
emitStatusToast('warning', t('tools.gen3d.unknown_model', { model }), '🧊 polli_gen_3d');
|
|
71
|
+
}
|
|
72
|
+
// Reference image (1 ref max — trellis uses the image only)
|
|
73
|
+
if (args.image && !validateHttpUrl(args.image)) {
|
|
74
|
+
return t('tools.gen3d.invalid_image_url');
|
|
75
|
+
}
|
|
76
|
+
// Per-call timeout validation (>= 10s, <= 3600s)
|
|
77
|
+
const timeoutCheck = validateTimeoutSeconds(args.timeout_seconds);
|
|
78
|
+
if (!timeoutCheck.ok) {
|
|
79
|
+
return t('tools.gen3d.invalid_timeout', { reason: timeoutCheck.reason || '' });
|
|
80
|
+
}
|
|
81
|
+
// Seed: resolved up-front (recovery metadata — same seed = cache hit).
|
|
82
|
+
const seed = args.seed !== undefined ? args.seed : Math.floor(Math.random() * 1000000);
|
|
83
|
+
// Estimate cost + Cost Guard
|
|
84
|
+
const estimatedCost = estimate3DCost(model);
|
|
85
|
+
const costCheck = checkCostControl('polli_gen_3d', args, model, estimatedCost, '3d');
|
|
86
|
+
if (!costCheck.allowed) {
|
|
87
|
+
return costCheck.message || t('tools.gen3d.blocked');
|
|
88
|
+
}
|
|
89
|
+
const config = loadConfig();
|
|
90
|
+
const argsStr = config.gui?.logs === 'verbose' ? `\nParameters: ${JSON.stringify(args)}` : '';
|
|
91
|
+
emitStatusToast('info', t('tools.gen3d.generating', { model, resolution }) + argsStr, '🧊 polli_gen_3d');
|
|
92
|
+
context.metadata({ title: `🧊 3D: ${model} (${resolution})` });
|
|
93
|
+
// Effective timeout via hierarchy: per-call > model override > capability > global
|
|
94
|
+
const timeoutSeconds = resolveCapabilityTimeout('gen_3d', model, args.timeout_seconds, config.timeouts ?? null);
|
|
95
|
+
const timeoutMs = timeoutSeconds * 1000;
|
|
96
|
+
try {
|
|
97
|
+
// Build canonical GET /3d/{prompt}
|
|
98
|
+
const params = new URLSearchParams({
|
|
99
|
+
model,
|
|
100
|
+
resolution,
|
|
101
|
+
seed: String(seed),
|
|
102
|
+
nologo: 'true',
|
|
103
|
+
private: 'true',
|
|
104
|
+
});
|
|
105
|
+
if (args.image)
|
|
106
|
+
params.set('image', args.image);
|
|
107
|
+
const promptEncoded = encodeURIComponent(prompt);
|
|
108
|
+
const url = `https://gen.pollinations.ai/3d/${promptEncoded}?${params}`;
|
|
109
|
+
const headers = {};
|
|
110
|
+
if (apiKey)
|
|
111
|
+
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
112
|
+
const balBefore = await fetchEnterBalance();
|
|
113
|
+
const result = await httpsGet(url, headers, timeoutMs);
|
|
114
|
+
const glbData = result.data;
|
|
115
|
+
const responseHeaders = result.headers;
|
|
116
|
+
// Artifact validation: GLB magic bytes ('glTF'). Never write a
|
|
117
|
+
// fake format with an arbitrary extension.
|
|
118
|
+
const detected = detectArtifactType(glbData);
|
|
119
|
+
if (!detected || detected.format !== 'glb') {
|
|
120
|
+
return t('tools.gen3d.err_format', {
|
|
121
|
+
detected: detected ? detected.format : 'unknown',
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// Persist with the detected extension
|
|
125
|
+
let outputDir = getDefaultOutputDir('3d');
|
|
126
|
+
let filename = args.filename ? sanitizeFilename(args.filename) : undefined;
|
|
127
|
+
if (args.save_to) {
|
|
128
|
+
if (args.save_to.match(/\.glb$/i)) {
|
|
129
|
+
outputDir = path.dirname(args.save_to);
|
|
130
|
+
filename = path.basename(args.save_to);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
outputDir = args.save_to;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
ensureDir(outputDir);
|
|
137
|
+
if (!filename)
|
|
138
|
+
filename = generateFilename('3d', model, 'glb');
|
|
139
|
+
const persisted = persistArtifact(glbData, {
|
|
140
|
+
outputDir,
|
|
141
|
+
filename,
|
|
142
|
+
preferredExt: 'glb',
|
|
143
|
+
detectExt: true,
|
|
144
|
+
});
|
|
145
|
+
// Real cost via balance delta (ledger sync delay) + headers fallback
|
|
146
|
+
let realCost;
|
|
147
|
+
if (balBefore !== null) {
|
|
148
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
149
|
+
const balAfter = await fetchEnterBalance();
|
|
150
|
+
if (balAfter !== null) {
|
|
151
|
+
realCost = Math.round((balBefore - balAfter) * 10000) / 10000;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const costTracking = extractCostFromHeaders(responseHeaders);
|
|
155
|
+
const lines = [];
|
|
156
|
+
lines.push(t('tools.gen3d.res_title'));
|
|
157
|
+
lines.push(`━━━━━━━━━━━━━━━━━━`);
|
|
158
|
+
lines.push(t('tools.gen3d.res_prompt', { prompt: prompt.substring(0, 100) + (prompt.length > 100 ? '...' : '') }));
|
|
159
|
+
lines.push(t('tools.gen3d.res_model', { model }));
|
|
160
|
+
lines.push(t('tools.gen3d.res_resolution', { resolution }));
|
|
161
|
+
lines.push(t('tools.gen3d.res_seed', { seed }));
|
|
162
|
+
if (args.image) {
|
|
163
|
+
lines.push(t('tools.gen3d.res_image', { src: args.image.substring(0, 60) + '...' }));
|
|
164
|
+
}
|
|
165
|
+
lines.push(t('tools.gen3d.res_file', { path: persisted.filePath }));
|
|
166
|
+
lines.push(t('tools.gen3d.res_size', { size: formatFileSize(persisted.size) }));
|
|
167
|
+
lines.push(t('tools.gen3d.res_format', { ext: persisted.ext.toUpperCase() }));
|
|
168
|
+
if (isCostEstimatorEnabled()) {
|
|
169
|
+
lines.push(t('tools.gen3d.res_cost_est', { cost: formatCost(estimatedCost) }));
|
|
170
|
+
if (realCost !== undefined) {
|
|
171
|
+
lines.push(t('tools.gen3d.res_cost_real', { cost: formatCost(realCost) }));
|
|
172
|
+
}
|
|
173
|
+
else if (costTracking.costUsd !== undefined) {
|
|
174
|
+
lines.push(t('tools.gen3d.res_cost_real', { cost: formatCost(costTracking.costUsd) }));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (responseHeaders['x-model-used']) {
|
|
178
|
+
lines.push(t('tools.gen3d.res_model_used', { model: responseHeaders['x-model-used'] }));
|
|
179
|
+
}
|
|
180
|
+
if (responseHeaders['x-request-id']) {
|
|
181
|
+
lines.push(t('tools.gen3d.res_request_id', { id: responseHeaders['x-request-id'] }));
|
|
182
|
+
}
|
|
183
|
+
emitStatusToast('success', t('tools.gen3d.success', { model }), '🧊 gen_3d', { filePath: persisted.filePath });
|
|
184
|
+
return lines.join('\n');
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
emitStatusToast('error', t('tools.gen3d.err_toast', { error: String(err.message || err).substring(0, 60) }), '🧊 gen_3d');
|
|
188
|
+
const isTimeout = /timeout/i.test(String(err.message || err));
|
|
189
|
+
if (isTimeout) {
|
|
190
|
+
// NO AUTOMATIC RESUBMIT: the generation may still be running
|
|
191
|
+
// and billed upstream. Offer cache recovery with the same seed.
|
|
192
|
+
return t('tools.gen3d.err_timeout', {
|
|
193
|
+
model,
|
|
194
|
+
seed,
|
|
195
|
+
seconds: timeoutSeconds,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (err.message?.includes('402') || err.message?.includes('Payment')) {
|
|
199
|
+
return t('tools.gen3d.err_pollen');
|
|
200
|
+
}
|
|
201
|
+
if (err.message?.includes('401') || err.message?.includes('403')) {
|
|
202
|
+
return t('tools.gen3d.err_auth');
|
|
203
|
+
}
|
|
204
|
+
return t('tools.gen3d.err_gen', { error: String(err.message || err) });
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
});
|
|
@@ -13,6 +13,10 @@ import * as path from 'path';
|
|
|
13
13
|
import { getApiKey, hasApiKey, httpsGet, ensureDir, generateFilename, getDefaultOutputDir, formatCost, formatFileSize, estimateImageCost, extractCostFromHeaders, isCostEstimatorEnabled, supportsI2I, getPaidImageModels, fetchEnterBalance, sanitizeFilename, validateHttpUrl, } from './shared.js';
|
|
14
14
|
import { loadConfig } from '../../server/config.js';
|
|
15
15
|
import { checkCostControl, isTokenBased } from './cost-guard.js';
|
|
16
|
+
import { validateTimeoutSeconds } from './timeout-policy.js';
|
|
17
|
+
import { resolveCapabilityTimeout } from './tool-capability-registry.js';
|
|
18
|
+
import { persistArtifact } from './artifact-core.js';
|
|
19
|
+
import { parsePolliErrorFromThrow } from './error-parser.js';
|
|
16
20
|
import { emitStatusToast } from '../../server/toast.js';
|
|
17
21
|
import { t } from '../../locales/index.js';
|
|
18
22
|
// ─── Constants ─────────────────────────────────────────────────────────────
|
|
@@ -31,6 +35,7 @@ export const polliGenImageTool = tool({
|
|
|
31
35
|
transparent: tool.schema.boolean().optional().describe(t('tools.image.arg_trans')),
|
|
32
36
|
save_to: tool.schema.string().optional().describe(t('tools.image.arg_save_to')),
|
|
33
37
|
filename: tool.schema.string().optional().describe(t('tools.image.arg_filename')),
|
|
38
|
+
timeout_seconds: tool.schema.number().optional().describe(t('tools.image.arg_timeout')),
|
|
34
39
|
},
|
|
35
40
|
async execute(args, context) {
|
|
36
41
|
const apiKey = getApiKey();
|
|
@@ -64,6 +69,11 @@ export const polliGenImageTool = tool({
|
|
|
64
69
|
return t('tools.image.no_i2i', { model, models });
|
|
65
70
|
}
|
|
66
71
|
}
|
|
72
|
+
// Per-call timeout validation (v6.5: >= 10s, <= 3600s)
|
|
73
|
+
const timeoutCheck = validateTimeoutSeconds(args.timeout_seconds);
|
|
74
|
+
if (!timeoutCheck.ok) {
|
|
75
|
+
return t('tools.image.invalid_timeout', { reason: timeoutCheck.reason || '' });
|
|
76
|
+
}
|
|
67
77
|
// Estimate cost
|
|
68
78
|
const estimatedCost = estimateImageCost(model);
|
|
69
79
|
// Cost Guard check V2
|
|
@@ -113,14 +123,16 @@ export const polliGenImageTool = tool({
|
|
|
113
123
|
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
114
124
|
// 1. Fetch balance avant génération
|
|
115
125
|
const balBefore = await fetchEnterBalance();
|
|
116
|
-
const
|
|
126
|
+
const timeoutSeconds = resolveCapabilityTimeout('gen_image', model, args.timeout_seconds, config.timeouts ?? null);
|
|
127
|
+
const result = await httpsGet(url, headers, timeoutSeconds * 1000);
|
|
117
128
|
imageData = result.data;
|
|
118
129
|
responseHeaders = result.headers;
|
|
119
130
|
// Update used model from response if available
|
|
120
131
|
if (responseHeaders['x-model-used']) {
|
|
121
132
|
usedModel = responseHeaders['x-model-used'];
|
|
122
133
|
}
|
|
123
|
-
// Save the image
|
|
134
|
+
// Save the image — extension follows REAL magic bytes (a b64
|
|
135
|
+
// response can be JPEG even when the caller assumed PNG).
|
|
124
136
|
let outputDir = getDefaultOutputDir('images');
|
|
125
137
|
let filename = args.filename ? sanitizeFilename(args.filename) : undefined;
|
|
126
138
|
if (args.save_to) {
|
|
@@ -133,9 +145,15 @@ export const polliGenImageTool = tool({
|
|
|
133
145
|
}
|
|
134
146
|
}
|
|
135
147
|
ensureDir(outputDir);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
148
|
+
if (!filename)
|
|
149
|
+
filename = generateFilename('image', usedModel, 'png');
|
|
150
|
+
const persisted = persistArtifact(imageData, {
|
|
151
|
+
outputDir,
|
|
152
|
+
filename,
|
|
153
|
+
preferredExt: 'png',
|
|
154
|
+
detectExt: true,
|
|
155
|
+
});
|
|
156
|
+
const filePath = persisted.filePath;
|
|
139
157
|
// 2. Fetch balance après génération (delay for API sync)
|
|
140
158
|
let balAfter = null;
|
|
141
159
|
let realCost;
|
|
@@ -196,16 +214,17 @@ export const polliGenImageTool = tool({
|
|
|
196
214
|
}
|
|
197
215
|
catch (err) {
|
|
198
216
|
emitStatusToast('error', t('tools.image.error', { error: err.message?.substring(0, 60) }), '🎨 gen_image');
|
|
199
|
-
|
|
217
|
+
const parsed = parsePolliErrorFromThrow(err);
|
|
218
|
+
if (parsed.kind === 'payment') {
|
|
200
219
|
return t('tools.image.insufficient_funds', { model });
|
|
201
220
|
}
|
|
202
|
-
if (
|
|
221
|
+
if (parsed.kind === 'auth') {
|
|
203
222
|
return t('tools.image.invalid_key');
|
|
204
223
|
}
|
|
205
|
-
if (
|
|
206
|
-
return t('tools.image.invalid_params', { error:
|
|
224
|
+
if (parsed.kind === 'bad_request') {
|
|
225
|
+
return t('tools.image.invalid_params', { error: parsed.message });
|
|
207
226
|
}
|
|
208
|
-
return t('tools.image.gen_error_msg', { error:
|
|
227
|
+
return t('tools.image.gen_error_msg', { error: parsed.message });
|
|
209
228
|
}
|
|
210
229
|
},
|
|
211
230
|
});
|
|
@@ -79,8 +79,9 @@ export const polliGenMusicTool = tool({
|
|
|
79
79
|
const headers = {
|
|
80
80
|
'Authorization': `Bearer ${apiKey}`,
|
|
81
81
|
};
|
|
82
|
-
// Music generation takes time
|
|
83
|
-
const
|
|
82
|
+
// Music generation takes time (~1.2x duration). v6.5: bound generously.
|
|
83
|
+
const musicTimeoutMs = Math.min(Math.ceil((duration + 60) * 1000), 3600000);
|
|
84
|
+
const result = await httpsGet(url, headers, musicTimeoutMs);
|
|
84
85
|
const audioData = result.data;
|
|
85
86
|
const responseHeaders = result.headers;
|
|
86
87
|
// Save audio
|
|
@@ -18,6 +18,8 @@ import * as path from 'path';
|
|
|
18
18
|
import { getApiKey, httpsGet, ensureDir, generateFilename, getDefaultOutputDir, formatCost, formatFileSize, estimateVideoCost, extractCostFromHeaders, isCostEstimatorEnabled, supportsI2V, requiresI2V, getDurationRange, getVideoModels, fetchEnterBalance, sanitizeFilename, validateHttpUrl, } from './shared.js';
|
|
19
19
|
import { loadConfig } from '../../server/config.js';
|
|
20
20
|
import { checkCostControl, isTokenBased } from './cost-guard.js';
|
|
21
|
+
import { validateTimeoutSeconds } from './timeout-policy.js';
|
|
22
|
+
import { resolveCapabilityTimeout } from './tool-capability-registry.js';
|
|
21
23
|
import { emitStatusToast } from '../../server/toast.js';
|
|
22
24
|
import { t } from '../../locales/index.js';
|
|
23
25
|
// ─── Constants ─────────────────────────────────────────────────────────────
|
|
@@ -34,6 +36,7 @@ export const polliGenVideoTool = tool({
|
|
|
34
36
|
aspect_ratio: tool.schema.enum(['16:9', '9:16', '1:1', '4:3']).optional().describe(t('tools.polli_gen_video.arg_aspect')),
|
|
35
37
|
reference_image: tool.schema.string().optional().describe(t('tools.polli_gen_video.arg_ref')),
|
|
36
38
|
seed: tool.schema.number().optional().describe(t('tools.polli_gen_video.arg_seed')),
|
|
39
|
+
timeout_seconds: tool.schema.number().optional().describe(t('tools.polli_gen_video.arg_timeout')),
|
|
37
40
|
save_to: tool.schema.string().optional().describe(t('tools.polli_gen_video.arg_save_to')),
|
|
38
41
|
filename: tool.schema.string().optional().describe(t('tools.polli_gen_video.arg_filename')),
|
|
39
42
|
},
|
|
@@ -82,6 +85,11 @@ export const polliGenVideoTool = tool({
|
|
|
82
85
|
.join(', ');
|
|
83
86
|
return t('tools.polli_gen_video.no_i2v', { model, models });
|
|
84
87
|
}
|
|
88
|
+
// Per-call timeout validation (v6.5: >= 10s, <= 3600s, no auto resubmit)
|
|
89
|
+
const timeoutCheck = validateTimeoutSeconds(args.timeout_seconds);
|
|
90
|
+
if (!timeoutCheck.ok) {
|
|
91
|
+
return t('tools.polli_gen_video.invalid_timeout', { reason: timeoutCheck.reason || '' });
|
|
92
|
+
}
|
|
85
93
|
// Estimate cost
|
|
86
94
|
const estimatedCost = estimateVideoCost(model, duration);
|
|
87
95
|
// Cost Guard check V2
|
|
@@ -125,14 +133,17 @@ export const polliGenVideoTool = tool({
|
|
|
125
133
|
params.set('seed', String(args.seed));
|
|
126
134
|
}
|
|
127
135
|
const promptEncoded = encodeURIComponent(args.prompt);
|
|
128
|
-
|
|
136
|
+
// v6.5: /video/{prompt} is the canonical route (SDK/CLI). /image/{prompt}
|
|
137
|
+
// remains compatible upstream but the plugin follows the semantic route.
|
|
138
|
+
const url = `https://gen.pollinations.ai/video/${promptEncoded}?${params}`;
|
|
129
139
|
const headers = {
|
|
130
140
|
'Authorization': `Bearer ${apiKey}`,
|
|
131
141
|
};
|
|
132
142
|
// 1. Fetch balance avant génération
|
|
133
143
|
const balBefore = await fetchEnterBalance();
|
|
134
144
|
// Video generation takes time (30-70 seconds depending on model)
|
|
135
|
-
const
|
|
145
|
+
const timeoutSeconds = resolveCapabilityTimeout('gen_video', model, args.timeout_seconds, config.timeouts ?? null);
|
|
146
|
+
const result = await httpsGet(url, headers, timeoutSeconds * 1000);
|
|
136
147
|
const videoData = result.data;
|
|
137
148
|
const responseHeaders = result.headers;
|
|
138
149
|
// Save video
|
|
@@ -9,16 +9,17 @@ You must strictly understand the 3 INDEPENDENT categories of settings before exp
|
|
|
9
9
|
=== 1. CHAT MODELS & FALLBACKS (Applies ONLY to conversational chat models) ===
|
|
10
10
|
- mode: Dictates fallback rules for the chat.
|
|
11
11
|
* 'manual': No automatic rules.
|
|
12
|
-
* '
|
|
13
|
-
* '
|
|
14
|
-
|
|
15
|
-
-
|
|
12
|
+
* 'quest' (QUEST_PREFERRED): Quest pollen first. Server may fall back to Paid if Quest is insufficient. Falls back to the Free Universe when BOTH Quest and Paid look exhausted.
|
|
13
|
+
* 'quest_only' (QUEST_ELIGIBLE_ONLY): Blocks paid_only models locally, only sends when the client considers the call Quest-eligible. BEST-EFFORT — a Paid (pack) debit can still occur server-side (race/real cost). No paid re-route.
|
|
14
|
+
* 'paid' (PAID_ALLOWED): Paid allowed, paid_only allowed per Cost Guard. Falls back to Free Universe when the wallet drops below thresholdsWallet.
|
|
15
|
+
- thresholdsQuest: absolute Quest pollen floor (e.g. 0.05) that triggers chat fallback in 'quest_only' mode.
|
|
16
|
+
- thresholdsWallet: absolute Paid pollen floor (e.g. 0.5) that triggers chat fallback in 'paid' mode.
|
|
16
17
|
*Note: 'enter.agent' or 'free.agent' are fallback conversational models for logic reasoning, THEY DO NOT GENERATE IMAGES OR VIDEOS!*
|
|
17
18
|
|
|
18
19
|
=== 2. TOOLS PROTECTION (Applies ONLY to independent 'polli_' tools like image, video, search) ===
|
|
19
|
-
- enablePaidTools:
|
|
20
|
+
- enablePaidTools: When false, tools that would use 'Paid' pollen are BLOCKED LOCALLY — models flagged paid_only are rejected before sending. IMPORTANT: this is a LOCAL client-side guard, NOT a server guarantee — a Paid (pack) debit can still occur in a race or on real-cost overage (the server picks the billing bucket at debit time).
|
|
20
21
|
- costConfirmationRequired: Safety lock for tools. If true, the user MUST manually confirm BEFORE executing ANY tool whose cost estimate exceeds the 'costThreshold'.
|
|
21
|
-
- costThreshold:
|
|
22
|
+
- costThreshold: 🌼 limit (cost of the tool execution) that triggers the confirmation lock.
|
|
22
23
|
- costEstimator: Shows live cost estimates IN TOOL OUTPUTS (false = Silent Mode).
|
|
23
24
|
|
|
24
25
|
=== 3. UI & NOTIFICATIONS (General display) ===
|
|
@@ -28,17 +29,15 @@ Use 'action=update' to change these. NEVER confuse Chat Mode with Tools Protecti
|
|
|
28
29
|
args: {
|
|
29
30
|
action: tool.schema.enum(['view', 'update'])
|
|
30
31
|
.describe('Action to perform: "view" to see current configuration, "update" to modify it.'),
|
|
31
|
-
mode: tool.schema.enum(['manual', '
|
|
32
|
+
mode: tool.schema.enum(['manual', 'quest', 'quest_only', 'paid']).optional().describe('CHAT ONLY: Dictates automatic fallback rules (manual/quest/quest_only/paid).'),
|
|
32
33
|
costEstimator: tool.schema.boolean().optional().describe('Set to true to show cost estimates auto. Set to false for "Manual Mode" (hide estimates).'),
|
|
33
34
|
statusBar: tool.schema.boolean().optional().describe('Enable/disable status bar visibility (true/false)'),
|
|
34
35
|
costConfirmationRequired: tool.schema.boolean().optional().describe('Safety Lock: Set to true to ask user confirmation before spending money. Set to false to spend automatically.'),
|
|
35
|
-
enablePaidTools: tool.schema.boolean().optional().describe('Allow execution of paid or premium models using
|
|
36
|
-
costThreshold: tool.schema.number().optional().describe('Cost threshold in
|
|
37
|
-
|
|
38
|
-
thresholdsWallet: tool.schema.number().optional().describe('
|
|
36
|
+
enablePaidTools: tool.schema.boolean().optional().describe('Allow execution of paid or premium models using Paid pollen (true/false)'),
|
|
37
|
+
costThreshold: tool.schema.number().optional().describe('Cost threshold in 🌼 above which confirmation is required'),
|
|
38
|
+
thresholdsQuest: tool.schema.number().optional().describe('Absolute Quest pollen floor (e.g. 0.05) for quest_only fallback.'),
|
|
39
|
+
thresholdsWallet: tool.schema.number().optional().describe('Absolute Paid pollen floor (e.g. 0.5) for paid mode fallback.'),
|
|
39
40
|
lang: tool.schema.enum(['en', 'fr', 'es', 'de', 'it', 'zh']).optional().describe('Plugin language for commands and toasts (en, fr, es, de, it, zh).'),
|
|
40
|
-
refillOverride: tool.schema.number().optional().describe('Manual Quest Pollen hourly refill override (0.01, 0.15, 0.4, 0.8, or 10). Set to 0 for auto-deduction.'),
|
|
41
|
-
questStashInFreeMode: tool.schema.boolean().optional().describe('Count accumulated quest stash as free pollen in alwaysfree Safety Net (default: true).')
|
|
42
41
|
},
|
|
43
42
|
async execute(args, context) {
|
|
44
43
|
if (args.action === 'view') {
|
|
@@ -65,17 +64,12 @@ Use 'action=update' to change these. NEVER confuse Chat Mode with Tools Protecti
|
|
|
65
64
|
updates.enablePaidTools = args.enablePaidTools;
|
|
66
65
|
if (args.costThreshold !== undefined)
|
|
67
66
|
updates.costThreshold = args.costThreshold;
|
|
68
|
-
if (args.refillOverride !== undefined) {
|
|
69
|
-
updates.refillOverride = args.refillOverride === 0 ? undefined : args.refillOverride;
|
|
70
|
-
}
|
|
71
|
-
if (args.questStashInFreeMode !== undefined)
|
|
72
|
-
updates.questStashInFreeMode = args.questStashInFreeMode;
|
|
73
67
|
if (args.lang !== undefined)
|
|
74
68
|
updates.lang = args.lang;
|
|
75
|
-
if (args.
|
|
69
|
+
if (args.thresholdsQuest !== undefined || args.thresholdsWallet !== undefined) {
|
|
76
70
|
updates.thresholds = { ...currentConfig.thresholds };
|
|
77
|
-
if (args.
|
|
78
|
-
updates.thresholds.
|
|
71
|
+
if (args.thresholdsQuest !== undefined)
|
|
72
|
+
updates.thresholds.quest = args.thresholdsQuest;
|
|
79
73
|
if (args.thresholdsWallet !== undefined)
|
|
80
74
|
updates.thresholds.wallet = args.thresholdsWallet;
|
|
81
75
|
}
|