opencode-pollinations-plugin 6.4.10 → 6.5.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.
Files changed (54) hide show
  1. package/README.de.md +67 -55
  2. package/README.es.md +79 -67
  3. package/README.fr.md +70 -58
  4. package/README.it.md +78 -66
  5. package/README.md +33 -29
  6. package/README.zh.md +77 -65
  7. package/dist/locales/de.json +82 -50
  8. package/dist/locales/en.json +84 -52
  9. package/dist/locales/es.json +82 -50
  10. package/dist/locales/fr.json +81 -49
  11. package/dist/locales/it.json +82 -50
  12. package/dist/locales/zh.json +82 -50
  13. package/dist/server/commands.js +112 -110
  14. package/dist/server/config.d.ts +24 -6
  15. package/dist/server/config.js +45 -4
  16. package/dist/server/connect-response.js +7 -7
  17. package/dist/server/generate-config.d.ts +7 -0
  18. package/dist/server/generate-config.js +22 -16
  19. package/dist/server/models/cache.d.ts +23 -10
  20. package/dist/server/models/cache.js +46 -24
  21. package/dist/server/models/fetcher.js +9 -0
  22. package/dist/server/models/types.d.ts +8 -0
  23. package/dist/server/models/worker.js +32 -8
  24. package/dist/server/proxy.d.ts +6 -0
  25. package/dist/server/proxy.js +297 -210
  26. package/dist/server/quota.d.ts +23 -32
  27. package/dist/server/quota.js +45 -185
  28. package/dist/server/status.js +1 -2
  29. package/dist/server/toast.js +1 -1
  30. package/dist/tools/index.d.ts +2 -1
  31. package/dist/tools/index.js +8 -3
  32. package/dist/tools/pollinations/artifact-core.d.ts +53 -0
  33. package/dist/tools/pollinations/artifact-core.js +159 -0
  34. package/dist/tools/pollinations/beta_discovery.js +2 -1
  35. package/dist/tools/pollinations/cost-guard.d.ts +2 -2
  36. package/dist/tools/pollinations/error-parser.d.ts +38 -0
  37. package/dist/tools/pollinations/error-parser.js +112 -0
  38. package/dist/tools/pollinations/gen_3d.d.ts +17 -0
  39. package/dist/tools/pollinations/gen_3d.js +207 -0
  40. package/dist/tools/pollinations/gen_image.js +29 -10
  41. package/dist/tools/pollinations/gen_music.js +3 -2
  42. package/dist/tools/pollinations/gen_video.js +13 -2
  43. package/dist/tools/pollinations/polli_config.js +18 -21
  44. package/dist/tools/pollinations/polli_gen_confirm.js +2 -0
  45. package/dist/tools/pollinations/shared.d.ts +1 -1
  46. package/dist/tools/pollinations/shared.js +53 -142
  47. package/dist/tools/pollinations/timeout-policy.d.ts +80 -0
  48. package/dist/tools/pollinations/timeout-policy.js +124 -0
  49. package/dist/tools/pollinations/tool-capability-registry.d.ts +51 -0
  50. package/dist/tools/pollinations/tool-capability-registry.js +215 -0
  51. package/dist/tools/pollinations/transcribe_audio.js +5 -24
  52. package/package.json +6 -4
  53. package/dist/server/tier-info.d.ts +0 -36
  54. package/dist/server/tier-info.js +0 -107
@@ -6,6 +6,7 @@ import { polliGenImageTool } from './gen_image.js';
6
6
  import { polliGenVideoTool } from './gen_video.js';
7
7
  import { polliGenAudioTool } from './gen_audio.js';
8
8
  import { polliGenMusicTool } from './gen_music.js';
9
+ import { polliGen3dTool } from './gen_3d.js';
9
10
  import { polliWebSearchTool } from './polli_web_search.js';
10
11
  import { formatCost } from './shared.js';
11
12
  export const polliGenConfirmTool = tool({
@@ -30,6 +31,7 @@ export const polliGenConfirmTool = tool({
30
31
  'polli_gen_video': polliGenVideoTool,
31
32
  'polli_gen_audio': polliGenAudioTool,
32
33
  'polli_gen_music': polliGenMusicTool,
34
+ 'polli_gen_3d': polliGen3dTool,
33
35
  'polli_web_search': polliWebSearchTool,
34
36
  };
35
37
  const targetTool = toolRegistry[pendingReq.toolName];
@@ -105,7 +105,7 @@ export declare function getMusicModel(): Record<string, {
105
105
  params: string[];
106
106
  duration: [number, number];
107
107
  }>;
108
- export declare function httpsGet(url: string, headers?: Record<string, string>): Promise<{
108
+ export declare function httpsGet(url: string, headers?: Record<string, string>, timeoutMs?: number): Promise<{
109
109
  data: Buffer;
110
110
  headers: Record<string, string>;
111
111
  }>;
@@ -50,7 +50,8 @@ export function getPaidImageModels() {
50
50
  }
51
51
  return result;
52
52
  }
53
- return _STATIC_PAID_IMAGE_MODELS;
53
+ // v6.5: offline fallback is served by the Model Registry STATIC_FALLBACK.
54
+ return {};
54
55
  }
55
56
  /**
56
57
  * Dynamic Video Models accessor.
@@ -65,9 +66,11 @@ export function getVideoModels() {
65
66
  result[m.name] = {
66
67
  desc: m.description,
67
68
  cost: costStr,
68
- t2v: !_STATIC_I2V_ONLY.has(m.name), // wan is I2V only
69
+ // wan is I2V-only upstream; manual.ts patches [image,text] +
70
+ // a T2V dummy-image hack, so t2v stays enabled for it.
71
+ t2v: !(m.supportsI2X && m.input_modalities.length === 1 && m.input_modalities[0] === 'image'),
69
72
  i2v: m.supportsI2X,
70
- audio: !_STATIC_NO_AUDIO.has(m.name),
73
+ audio: !VIDEO_NO_AUDIO_MODELS.has(m.name),
71
74
  duration: m.durationRange || [1, 10],
72
75
  aspectRatios: m.aspectRatios || ['16:9'],
73
76
  costHeader: m.costHeader || 'x-usage-completion-video-seconds',
@@ -76,7 +79,7 @@ export function getVideoModels() {
76
79
  }
77
80
  return result;
78
81
  }
79
- return _STATIC_VIDEO_MODELS;
82
+ return {};
80
83
  }
81
84
  /**
82
85
  * Dynamic Audio Models accessor.
@@ -99,7 +102,7 @@ export function getAudioModels() {
99
102
  }
100
103
  return result;
101
104
  }
102
- return _STATIC_AUDIO_MODELS;
105
+ return {};
103
106
  }
104
107
  /**
105
108
  * Text Model accessor
@@ -144,9 +147,15 @@ export function getMusicModel() {
144
147
  // getPaidImageModels(), getVideoModels(), getAudioModels(), getMusicModel()
145
148
  // For direct model lookup: use ModelRegistry.getByNameOrAlias()
146
149
  // ─── Private Static Fallback Data ─────────────────────────────────────────
147
- // Used ONLY when ModelRegistry is not ready (startup race, offline).
148
- const _STATIC_I2V_ONLY = new Set(); // Models that are I2V only (no T2V)
149
- const _STATIC_NO_AUDIO = new Set(['seedance', 'seedance-pro']); // Video models without audio
150
+ // v6.5: the three big model tables (_STATIC_PAID_IMAGE_MODELS,
151
+ // _STATIC_VIDEO_MODELS, _STATIC_AUDIO_MODELS) and the empty
152
+ // _STATIC_I2V_ONLY set were removed the Model Registry serves the
153
+ // offline fallback (STATIC_FALLBACK in models/cache.ts) and the live
154
+ // catalog provides paid_only/modalities/voices dynamically.
155
+ // What remains are transport rules the catalogue does NOT expose.
156
+ // Video models without audio output (mirrors manual.ts; the catalogue's
157
+ // video_capabilities.audio_output is not yet mapped to PollinationsModel).
158
+ const VIDEO_NO_AUDIO_MODELS = new Set(['seedance', 'seedance-pro']);
150
159
  const _STATIC_AUDIO_ENDPOINTS = {
151
160
  'openai-audio': '/v1/chat/completions',
152
161
  'elevenlabs': '/audio/{text}',
@@ -154,101 +163,6 @@ const _STATIC_AUDIO_ENDPOINTS = {
154
163
  'scribe': '/v1/audio/transcriptions',
155
164
  'elevenmusic': '/audio/{text}',
156
165
  };
157
- const _STATIC_PAID_IMAGE_MODELS = {
158
- 'flux': { desc: 'Flux Schnell', cost: '0.0002 🌻', t2i: true, i2i: false, params: ['width', 'height'] },
159
- 'sana': { desc: 'Sana (Efficient)', cost: '0.0002 🌻', t2i: true, i2i: false, params: ['width', 'height'] },
160
- 'zimage': { desc: 'Z-Image Turbo (6B Flux 2x)', cost: '0.0002 🌻', t2i: true, i2i: false, params: ['width', 'height'] },
161
- 'imagen-4': { desc: 'Imagen 4 (alpha)', cost: '0.0025 🌻', t2i: true, i2i: false, params: ['width', 'height'] },
162
- 'klein': { desc: 'FLUX.2 Klein 4B', cost: '0.008 🌻', t2i: true, i2i: true, params: ['width', 'height', 'image'] },
163
- 'klein-large': { desc: 'FLUX.2 Klein 9B', cost: '0.012 🌻', t2i: true, i2i: true, params: ['width', 'height', 'image'] },
164
- 'gptimage': { desc: 'GPT Image 1 Mini (OpenAI)', cost: 'tokens', t2i: true, i2i: false, params: ['width', 'height', 'quality', 'transparent'] },
165
- 'gptimage-large': { desc: 'GPT Image 1.5 (Advanced)', cost: 'tokens', t2i: true, i2i: false, params: ['width', 'height', 'quality', 'transparent'] },
166
- 'kontext': { desc: 'FLUX.1 Kontext', cost: '0.04 🌻 💎', t2i: true, i2i: true, params: ['width', 'height', 'image'], notes: 'In-Context Editing' },
167
- 'seedream': { desc: 'Seedream 4.0 (ByteDance ARK)', cost: '0.03 🌻', t2i: true, i2i: true, params: ['width', 'height', 'image'] },
168
- 'seedream-pro': { desc: 'Seedream 4.5 Pro (ARK 4K)', cost: '0.04 🌻 💎', t2i: true, i2i: true, params: ['width', 'height', 'image'], notes: '4K, Multi-Image' },
169
- 'nanobanana': { desc: 'NanoBanana (Gemini 2.5 Flash)', cost: 'tokens', t2i: true, i2i: true, params: ['width', 'height', 'image'] },
170
- 'nanobanana-pro': { desc: 'NanoBanana Pro (Gemini 3 Pro)', cost: 'tokens', t2i: true, i2i: true, params: ['width', 'height', 'image'], notes: 'Thinking Model' },
171
- };
172
- const _STATIC_VIDEO_MODELS = {
173
- 'grok-video': {
174
- desc: 'Grok Video (alpha)',
175
- cost: '0.0025/sec',
176
- t2v: true, i2v: false, audio: true,
177
- duration: [1, 15],
178
- aspectRatios: ['16:9', '9:16', '1:1', '4:3'],
179
- costHeader: 'x-usage-completion-video-seconds',
180
- genTime: '~10s'
181
- },
182
- 'ltx-2': {
183
- desc: 'LTX-2 (Lightricks)',
184
- cost: '0.01/sec',
185
- t2v: true, i2v: false, audio: true,
186
- duration: [5, 20],
187
- aspectRatios: ['16:9'],
188
- costHeader: 'x-usage-completion-video-seconds',
189
- genTime: '~35s'
190
- },
191
- 'wan': {
192
- desc: 'Wan 2.6 (Alibaba)',
193
- cost: '0.025/sec',
194
- t2v: false, i2v: true, audio: true,
195
- duration: [5, 15],
196
- aspectRatios: ['16:9', '9:16', '1:1', '4:3'],
197
- costHeader: 'x-usage-completion-video-seconds',
198
- genTime: '~30s'
199
- },
200
- 'veo': {
201
- desc: 'Veo 3.1 Fast (Google)',
202
- cost: '0.15/sec 💎',
203
- t2v: true, i2v: true, audio: true,
204
- duration: [4, 8],
205
- aspectRatios: ['16:9', '9:16', '1:1'],
206
- costHeader: 'x-usage-completion-video-seconds',
207
- genTime: '~45-68s',
208
- },
209
- 'seedance': {
210
- desc: 'Seedance Lite (BytePlus)',
211
- cost: 'tokens',
212
- t2v: true, i2v: true, audio: false,
213
- duration: [4, 12],
214
- aspectRatios: ['16:9', '9:16', '1:1'],
215
- costHeader: 'x-usage-completion-video-tokens',
216
- genTime: '~30s'
217
- },
218
- 'seedance-pro': {
219
- desc: 'Seedance Pro-Fast (BytePlus)',
220
- cost: 'tokens',
221
- t2v: true, i2v: true, audio: false,
222
- duration: [4, 12],
223
- aspectRatios: ['16:9', '9:16', '1:1'],
224
- costHeader: 'x-usage-completion-video-tokens',
225
- genTime: '~30s'
226
- },
227
- };
228
- const _STATIC_AUDIO_MODELS = {
229
- 'openai-audio': {
230
- desc: 'GPT-4o Audio Preview',
231
- type: 'both',
232
- endpoint: '/v1/chat/completions',
233
- params: ['voice', 'format'],
234
- voices: ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'],
235
- notes: 'DEFAULT - least expensive'
236
- },
237
- 'elevenlabs': {
238
- desc: 'ElevenLabs v3',
239
- type: 'tts',
240
- endpoint: '/audio/{text}',
241
- params: ['voice', 'response_format'],
242
- voices: ['rachel', 'domi', 'bella', 'elli', 'charlotte', 'dorothy', 'sarah', 'emily', 'lily', 'matilda', 'adam', 'antoni', 'arnold', 'josh', 'sam', 'daniel', 'charlie', 'james', 'fin', 'callum', 'liam', 'george', 'brian', 'bill', 'ash', 'ballad', 'coral', 'sage', 'verse'],
243
- },
244
- 'whisper': {
245
- desc: 'OpenAI Whisper v3',
246
- type: 'stt',
247
- endpoint: '/v1/audio/transcriptions',
248
- params: ['file'],
249
- notes: 'POST ONLY (multipart)'
250
- },
251
- };
252
166
  const _STATIC_MUSIC_MODEL = {
253
167
  'elevenmusic': {
254
168
  desc: 'ElevenLabs Music',
@@ -292,7 +206,7 @@ function detectAudioType(m) {
292
206
  return 'tts';
293
207
  }
294
208
  // ─── HTTP Helpers ─────────────────────────────────────────────────────────
295
- export function httpsGet(url, headers = {}) {
209
+ export function httpsGet(url, headers = {}, timeoutMs = 300000) {
296
210
  return new Promise((resolve, reject) => {
297
211
  const parsedUrl = new URL(url);
298
212
  const options = {
@@ -305,9 +219,9 @@ export function httpsGet(url, headers = {}) {
305
219
  },
306
220
  };
307
221
  const req = https.request(options, (res) => {
308
- // Handle redirects
222
+ // Handle redirects (propagate the timeout on follow)
309
223
  if ([301, 302, 307].includes(res.statusCode || 0) && res.headers.location) {
310
- httpsGet(res.headers.location, headers).then(resolve).catch(reject);
224
+ httpsGet(res.headers.location, headers, timeoutMs).then(resolve).catch(reject);
311
225
  return;
312
226
  }
313
227
  const chunks = [];
@@ -342,9 +256,11 @@ export function httpsGet(url, headers = {}) {
342
256
  });
343
257
  });
344
258
  req.on('error', reject);
345
- req.setTimeout(300000, () => {
259
+ // v6.5: per-call timeout (clamped to 1h absolute max), no blind replay.
260
+ const clamped = Math.min(Math.max(timeoutMs, 10000), 3600000);
261
+ req.setTimeout(clamped, () => {
346
262
  req.destroy();
347
- reject(new Error('Timeout (300s)'));
263
+ reject(new Error(`Timeout (${Math.round(clamped / 1000)}s)`));
348
264
  });
349
265
  req.end();
350
266
  });
@@ -587,39 +503,36 @@ export function per1pollen(cost) {
587
503
  return `${x.toFixed(1)}`;
588
504
  }
589
505
  export function estimateImageCost(model) {
590
- // Try ModelRegistry first
506
+ // Try ModelRegistry first (averageCost or catalogue pricing)
591
507
  if (ModelRegistry.isReady()) {
592
508
  const m = ModelRegistry.getByNameOrAlias('image', model);
593
- if (m && m.averageCost !== undefined) {
594
- return m.averageCost;
509
+ if (m) {
510
+ if (m.averageCost !== undefined)
511
+ return m.averageCost;
512
+ if (m.pricing.completionImageTokens !== undefined)
513
+ return m.pricing.completionImageTokens;
595
514
  }
596
515
  }
597
- // Fallback to static
598
- const info = _STATIC_PAID_IMAGE_MODELS[model];
599
- if (!info)
600
- return 0.0002;
601
- const costMatch = info.cost.match(/[\d.]+/);
602
- return costMatch ? parseFloat(costMatch[0]) : 0.0002;
516
+ return 0.0002;
603
517
  }
604
518
  export function estimateVideoCost(model, duration) {
605
- // Try ModelRegistry first
519
+ // Try ModelRegistry first (averageCost or catalogue pricing)
606
520
  if (ModelRegistry.isReady()) {
607
521
  const m = ModelRegistry.getByNameOrAlias('video', model);
608
- if (m && m.averageCost !== undefined) {
609
- return m.averageCost;
522
+ if (m) {
523
+ if (m.averageCost !== undefined)
524
+ return m.averageCost;
525
+ if (m.pricing.completionVideoSeconds !== undefined) {
526
+ return duration * m.pricing.completionVideoSeconds;
527
+ }
528
+ if (m.pricing.completionVideoTokens !== undefined) {
529
+ // seedance-style token billing (~21780 tokens/s observed)
530
+ const tokensPerSecond = 21780;
531
+ return (duration * tokensPerSecond) * m.pricing.completionVideoTokens;
532
+ }
610
533
  }
611
534
  }
612
- // Fallback to static
613
- const info = _STATIC_VIDEO_MODELS[model];
614
- if (!info)
615
- return duration * 0.01;
616
- if (info.costHeader === 'x-usage-completion-video-tokens') {
617
- const tokensPerSecond = 21780;
618
- return (duration * tokensPerSecond) * 0.00001;
619
- }
620
- const costMatch = info.cost.match(/[\d.]+/);
621
- const perSecond = costMatch ? parseFloat(costMatch[0]) : 0.01;
622
- return duration * perSecond;
535
+ return duration * 0.01;
623
536
  }
624
537
  export function estimateTtsCost(textLength) {
625
538
  // Try ModelRegistry first
@@ -707,8 +620,7 @@ export function supportsI2I(model) {
707
620
  const m = ModelRegistry.getByNameOrAlias('image', model);
708
621
  return m?.supportsI2X === true;
709
622
  }
710
- const info = _STATIC_PAID_IMAGE_MODELS[model];
711
- return info?.i2i === true;
623
+ return false;
712
624
  }
713
625
  /**
714
626
  * Check if video model supports Image-to-Video
@@ -718,21 +630,22 @@ export function supportsI2V(model) {
718
630
  const m = ModelRegistry.getByNameOrAlias('video', model);
719
631
  return m?.supportsI2X === true;
720
632
  }
721
- const info = _STATIC_VIDEO_MODELS[model];
722
- return info?.i2v === true;
633
+ return false;
723
634
  }
724
635
  /**
725
636
  * Check if video model requires Image-to-Video (no T2V)
726
637
  */
727
638
  export function requiresI2V(model) {
639
+ // v6.5: I2V-only = accepts image input and no text input. manual.ts
640
+ // patches wan with [image,text] + T2V dummy-image hack, so no current
641
+ // model is strictly I2V-only.
728
642
  if (ModelRegistry.isReady()) {
729
643
  const m = ModelRegistry.getByNameOrAlias('video', model);
730
644
  if (m) {
731
- return _STATIC_I2V_ONLY.has(m.name); // Only wan is I2V-only for now
645
+ return m.input_modalities.includes('image') && !m.input_modalities.includes('text');
732
646
  }
733
647
  }
734
- const info = _STATIC_VIDEO_MODELS[model];
735
- return info?.t2v === false && info?.i2v === true;
648
+ return false;
736
649
  }
737
650
  /**
738
651
  * Validate aspect ratio for video model
@@ -742,8 +655,7 @@ export function validateAspectRatio(model, ratio) {
742
655
  const m = ModelRegistry.getByNameOrAlias('video', model);
743
656
  return m?.aspectRatios?.includes(ratio) ?? false;
744
657
  }
745
- const info = _STATIC_VIDEO_MODELS[model];
746
- return info?.aspectRatios.includes(ratio) ?? false;
658
+ return false;
747
659
  }
748
660
  /**
749
661
  * Get valid duration range for video model
@@ -753,6 +665,5 @@ export function getDurationRange(model) {
753
665
  const m = ModelRegistry.getByNameOrAlias('video', model);
754
666
  return m?.durationRange ?? [1, 10];
755
667
  }
756
- const info = _STATIC_VIDEO_MODELS[model];
757
- return info?.duration ?? [1, 10];
668
+ return [1, 10];
758
669
  }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Timeout Policy (v6.5) — single source of truth for operation timeouts.
3
+ *
4
+ * Hierarchy (highest precedence first):
5
+ * per-call (timeout_seconds?)
6
+ * > model/backend override
7
+ * > capability
8
+ * > global default
9
+ *
10
+ * Clamp: minimum 10s, absolute maximum = hierarchy.max (3600s).
11
+ * Values based on the Phase 2/2.2 execution audit (server-side limits:
12
+ * trellis ~5 min, hyper3d ~10 min, wan-pro ~15 min, veo/grok ~3 min).
13
+ */
14
+ export interface TimeoutHierarchy {
15
+ /** Global default for all remote operations (s). */
16
+ default: number;
17
+ /** Default for long-running generations (s). */
18
+ longRunning: number;
19
+ /** Absolute ceiling (s). */
20
+ max: number;
21
+ /** Capability floors (s). */
22
+ capabilities: Record<string, number>;
23
+ /** Model/backend overrides (s). `"name"` exact match, `"prefix-*"` wildcard. */
24
+ overrides: Record<string, number>;
25
+ }
26
+ export declare const DEFAULT_TIMEOUT_HIERARCHY: TimeoutHierarchy;
27
+ export declare const MIN_TIMEOUT_SECONDS = 10;
28
+ export declare const MAX_TIMEOUT_SECONDS = 3600;
29
+ /** Raw user timeout config (config.timeouts) — merged OVER the built-ins. */
30
+ export interface UserTimeoutConfig {
31
+ default?: number;
32
+ longRunning?: number;
33
+ max?: number;
34
+ capabilities?: Record<string, number>;
35
+ overrides?: Record<string, number>;
36
+ }
37
+ export interface ResolveTimeoutOptions {
38
+ perCall?: number;
39
+ model?: string;
40
+ capability?: string;
41
+ longRunning?: boolean;
42
+ hierarchy?: TimeoutHierarchy;
43
+ }
44
+ export interface TimeoutValidation {
45
+ ok: boolean;
46
+ seconds?: number;
47
+ reason?: string;
48
+ }
49
+ /** Validate a per-call timeout_seconds value (>= 10s, <= 3600s). */
50
+ export declare function validateTimeoutSeconds(value: number | undefined): TimeoutValidation;
51
+ /** Exact or wildcard ("prefix-*") override lookup. */
52
+ export declare function lookupTimeoutOverride(overrides: Record<string, number> | undefined, model: string | undefined): number | undefined;
53
+ /**
54
+ * Resolve the effective timeout with the full v6.5 precedence:
55
+ *
56
+ * per-call (timeout_seconds)
57
+ * > USER model override (config.timeouts.overrides[model])
58
+ * > USER capability override (config.timeouts.capabilities[cap])
59
+ * > built-in model default (DEFAULT hierarchy overrides)
60
+ * > built-in capability default (DEFAULT hierarchy capabilities)
61
+ * > global (longRunning | default)
62
+ *
63
+ * Clamped to [10, user.max || builtin.max].
64
+ */
65
+ export declare function resolveEffectiveTimeout(opts: {
66
+ perCall?: number;
67
+ model?: string;
68
+ capabilityKey?: string;
69
+ longRunning?: boolean;
70
+ user?: UserTimeoutConfig | null;
71
+ builtin?: TimeoutHierarchy;
72
+ }): number;
73
+ /**
74
+ * Legacy resolution used by the TCR capability timeout test surface:
75
+ * per-call > model override > capability > (longRunning ? longRunning : default).
76
+ * Result is clamped to [10, hierarchy.max].
77
+ */
78
+ export declare function resolveTimeoutSeconds(opts: ResolveTimeoutOptions): number;
79
+ /** Merge user-configured timeout overrides (from config) onto the defaults. */
80
+ export declare function mergeTimeoutHierarchy(user?: Partial<TimeoutHierarchy>): TimeoutHierarchy;
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Timeout Policy (v6.5) — single source of truth for operation timeouts.
3
+ *
4
+ * Hierarchy (highest precedence first):
5
+ * per-call (timeout_seconds?)
6
+ * > model/backend override
7
+ * > capability
8
+ * > global default
9
+ *
10
+ * Clamp: minimum 10s, absolute maximum = hierarchy.max (3600s).
11
+ * Values based on the Phase 2/2.2 execution audit (server-side limits:
12
+ * trellis ~5 min, hyper3d ~10 min, wan-pro ~15 min, veo/grok ~3 min).
13
+ */
14
+ export const DEFAULT_TIMEOUT_HIERARCHY = {
15
+ default: 300,
16
+ longRunning: 900,
17
+ max: 3600,
18
+ capabilities: {
19
+ image: 600,
20
+ video: 1800,
21
+ audio: 600,
22
+ threeD: 1800,
23
+ realtime: 300,
24
+ embed: 60,
25
+ },
26
+ overrides: {
27
+ 'trellis-2': 1200, // server ~5 min max
28
+ 'hyper3d-rodin': 1800, // server ~10 min max
29
+ 'seedance-*': 900, // server ~6 min
30
+ 'wan-pro': 1800, // server ~15 min
31
+ },
32
+ };
33
+ export const MIN_TIMEOUT_SECONDS = 10;
34
+ export const MAX_TIMEOUT_SECONDS = 3600;
35
+ /** Validate a per-call timeout_seconds value (>= 10s, <= 3600s). */
36
+ export function validateTimeoutSeconds(value) {
37
+ if (value === undefined)
38
+ return { ok: true };
39
+ if (typeof value !== 'number' || !isFinite(value)) {
40
+ return { ok: false, reason: `timeout_seconds must be a finite number of seconds (got ${value})` };
41
+ }
42
+ if (value < MIN_TIMEOUT_SECONDS) {
43
+ return { ok: false, reason: `timeout_seconds must be >= ${MIN_TIMEOUT_SECONDS}s (got ${value}s)` };
44
+ }
45
+ if (value > MAX_TIMEOUT_SECONDS) {
46
+ return { ok: false, reason: `timeout_seconds must be <= ${MAX_TIMEOUT_SECONDS}s (got ${value}s)` };
47
+ }
48
+ return { ok: true, seconds: value };
49
+ }
50
+ /** Exact or wildcard ("prefix-*") override lookup. */
51
+ export function lookupTimeoutOverride(overrides, model) {
52
+ if (!model || !overrides)
53
+ return undefined;
54
+ if (overrides[model] !== undefined)
55
+ return overrides[model];
56
+ for (const [pattern, value] of Object.entries(overrides)) {
57
+ if (pattern.endsWith('*') && model.startsWith(pattern.slice(0, -1))) {
58
+ return value;
59
+ }
60
+ }
61
+ return undefined;
62
+ }
63
+ /**
64
+ * Resolve the effective timeout with the full v6.5 precedence:
65
+ *
66
+ * per-call (timeout_seconds)
67
+ * > USER model override (config.timeouts.overrides[model])
68
+ * > USER capability override (config.timeouts.capabilities[cap])
69
+ * > built-in model default (DEFAULT hierarchy overrides)
70
+ * > built-in capability default (DEFAULT hierarchy capabilities)
71
+ * > global (longRunning | default)
72
+ *
73
+ * Clamped to [10, user.max || builtin.max].
74
+ */
75
+ export function resolveEffectiveTimeout(opts) {
76
+ const builtin = opts.builtin ?? DEFAULT_TIMEOUT_HIERARCHY;
77
+ let t = opts.perCall; // 1. per-call
78
+ if (t === undefined)
79
+ t = lookupTimeoutOverride(opts.user?.overrides, opts.model); // 2. user model override
80
+ if (t === undefined && opts.capabilityKey)
81
+ t = opts.user?.capabilities?.[opts.capabilityKey]; // 3. user capability
82
+ if (t === undefined)
83
+ t = lookupTimeoutOverride(builtin.overrides, opts.model); // 4. built-in model default
84
+ if (t === undefined && opts.capabilityKey)
85
+ t = builtin.capabilities[opts.capabilityKey]; // 5. built-in capability
86
+ if (t === undefined)
87
+ t = opts.longRunning ? builtin.longRunning : builtin.default; // 6. global
88
+ const ceiling = opts.user?.max ?? builtin.max;
89
+ return Math.min(Math.max(t, MIN_TIMEOUT_SECONDS), ceiling);
90
+ }
91
+ /**
92
+ * Legacy resolution used by the TCR capability timeout test surface:
93
+ * per-call > model override > capability > (longRunning ? longRunning : default).
94
+ * Result is clamped to [10, hierarchy.max].
95
+ */
96
+ export function resolveTimeoutSeconds(opts) {
97
+ const hierarchy = opts.hierarchy ?? DEFAULT_TIMEOUT_HIERARCHY;
98
+ let t = opts.longRunning ? hierarchy.longRunning : hierarchy.default;
99
+ if (opts.capability) {
100
+ const capVal = hierarchy.capabilities[opts.capability];
101
+ if (capVal !== undefined)
102
+ t = capVal;
103
+ }
104
+ if (opts.model) {
105
+ const modelOverride = lookupTimeoutOverride(hierarchy.overrides, opts.model);
106
+ if (modelOverride !== undefined)
107
+ t = modelOverride;
108
+ }
109
+ if (opts.perCall !== undefined)
110
+ t = opts.perCall;
111
+ return Math.min(Math.max(t, MIN_TIMEOUT_SECONDS), hierarchy.max);
112
+ }
113
+ /** Merge user-configured timeout overrides (from config) onto the defaults. */
114
+ export function mergeTimeoutHierarchy(user) {
115
+ if (!user)
116
+ return { ...DEFAULT_TIMEOUT_HIERARCHY };
117
+ return {
118
+ default: user.default ?? DEFAULT_TIMEOUT_HIERARCHY.default,
119
+ longRunning: user.longRunning ?? DEFAULT_TIMEOUT_HIERARCHY.longRunning,
120
+ max: user.max ?? DEFAULT_TIMEOUT_HIERARCHY.max,
121
+ capabilities: { ...DEFAULT_TIMEOUT_HIERARCHY.capabilities, ...(user.capabilities || {}) },
122
+ overrides: { ...DEFAULT_TIMEOUT_HIERARCHY.overrides, ...(user.overrides || {}) },
123
+ };
124
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Tool Capability Registry (v6.5) — capability-centric, declarative.
3
+ *
4
+ * Answers "HOW to execute this capability" — endpoint, transport, execution
5
+ * mode, timeouts, retry/recovery policy, artifact format, backend overrides.
6
+ *
7
+ * The Model Registry remains model-centric ("WHAT exists"). The TCR
8
+ * references/enriches it and does NOT duplicate model fields (pricing,
9
+ * paid_only, modalities stay in the Model Registry).
10
+ *
11
+ * Overrides that the live catalog does NOT expose (special headers, endpoint
12
+ * quirks, token formats) live here — never as `if (model === ...)` scattered
13
+ * across tools.
14
+ */
15
+ import { type UserTimeoutConfig } from './timeout-policy.js';
16
+ export type ExecutionMode = 'SHORT_REQUEST' | 'LONG_BLOCKING' | 'ASYNC_JOB' | 'STREAMING' | 'LOCAL';
17
+ export type RetryPolicy = 'NO_AUTOMATIC_RETRY' | 'SAFE_READ_ONLY' | 'RECOVER_SAME_REQUEST' | 'REPOLL_JOB';
18
+ export interface ToolModelCapability {
19
+ capability: string;
20
+ backend: string;
21
+ modelId?: string;
22
+ transport: {
23
+ endpoint: string;
24
+ method: 'GET' | 'POST' | 'WS';
25
+ mode: ExecutionMode;
26
+ };
27
+ inputs?: Record<string, unknown>;
28
+ outputs?: Record<string, unknown>;
29
+ constraints?: Record<string, unknown>;
30
+ pricing?: Record<string, unknown>;
31
+ execution: {
32
+ defaultTimeoutSeconds?: number;
33
+ maxTimeoutSeconds?: number;
34
+ retryPolicy: RetryPolicy;
35
+ idempotency?: 'SERVER_DEDUP' | 'NONE';
36
+ pollIntervalSeconds?: number;
37
+ supportsCancel?: boolean;
38
+ recovery?: string;
39
+ };
40
+ backendOverrides?: Record<string, unknown>;
41
+ }
42
+ export declare const TOOL_CAPABILITIES: ToolModelCapability[];
43
+ /** Resolve a capability declaration by name (optionally filtered by backend). */
44
+ export declare function resolveCapability(capability: string, backend?: string): ToolModelCapability | undefined;
45
+ export declare function listCapabilities(): string[];
46
+ /**
47
+ * Effective timeout for a capability+model with full v6.5 precedence:
48
+ * per-call > USER model override > USER capability override
49
+ * > built-in model default > built-in capability > global.
50
+ */
51
+ export declare function resolveCapabilityTimeout(capability: string, model?: string, perCall?: number, userConfig?: UserTimeoutConfig | null): number;