@nodaro/shared 1.6.0 → 1.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodaro/shared",
3
- "version": "1.6.0",
3
+ "version": "1.9.0",
4
4
  "description": "Shared types, model catalog, wire contracts, and structural vocabularies for the Nodaro platform and SDK.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -3,11 +3,13 @@ import {
3
3
  LLM_MODEL_IDS,
4
4
  LLM_FEATURE_DEFAULTS,
5
5
  STRUCTURED_VISION_MODELS,
6
+ LLM_REASONING_EFFORTS,
6
7
  getLlmModel,
7
8
  getLlmTier,
8
9
  buildLlmCreditIdentifier,
9
10
  resolveLlmCreditId,
10
11
  motionGraphicsFeature,
12
+ effectiveReasoningEffort,
11
13
  } from "../llm-models.js"
12
14
  import type { LlmModelDef, LlmTier, LlmFeature } from "../llm-models.js"
13
15
 
@@ -21,8 +23,8 @@ import type { LlmModelDef, LlmTier, LlmFeature } from "../llm-models.js"
21
23
  // LLM_MODELS data integrity
22
24
  // ---------------------------------------------------------------------------
23
25
  describe("LLM_MODELS data integrity", () => {
24
- it("should have exactly 7 models", () => {
25
- expect(LLM_MODELS).toHaveLength(7)
26
+ it("should have exactly 13 models", () => {
27
+ expect(LLM_MODELS).toHaveLength(13)
26
28
  })
27
29
 
28
30
  it("each model has all required fields", () => {
@@ -57,14 +59,14 @@ describe("LLM_MODELS data integrity", () => {
57
59
  expect(new Set(ids).size).toBe(ids.length)
58
60
  })
59
61
 
60
- it("has 2 economy, 2 standard, 3 premium models", () => {
62
+ it("has 3 economy, 4 standard, 6 premium models", () => {
61
63
  const tierCounts: Record<LlmTier, number> = { economy: 0, standard: 0, premium: 0 }
62
64
  for (const model of LLM_MODELS) {
63
65
  tierCounts[model.tier]++
64
66
  }
65
- expect(tierCounts.economy).toBe(2)
66
- expect(tierCounts.standard).toBe(2)
67
- expect(tierCounts.premium).toBe(3)
67
+ expect(tierCounts.economy).toBe(3)
68
+ expect(tierCounts.standard).toBe(4)
69
+ expect(tierCounts.premium).toBe(6)
68
70
  })
69
71
 
70
72
  it("all three kieFormats are represented", () => {
@@ -111,6 +113,12 @@ describe("LLM_MODEL_IDS", () => {
111
113
  "gemini-3.1-pro",
112
114
  "claude-opus-4.7",
113
115
  "gpt-5.4",
116
+ "gpt-5.5",
117
+ "gpt-5.6-luna",
118
+ "gpt-5.6-terra",
119
+ "gpt-5.6-sol",
120
+ "claude-sonnet-5",
121
+ "claude-opus-4.8",
114
122
  ]
115
123
  expect(LLM_MODEL_IDS).toEqual(expected)
116
124
  })
@@ -420,6 +428,8 @@ describe("STRUCTURED_VISION_MODELS", () => {
420
428
  "claude-sonnet-4.6",
421
429
  "gemini-3-flash",
422
430
  "gemini-3.1-pro",
431
+ "claude-sonnet-5",
432
+ "claude-opus-4.8",
423
433
  ].sort(),
424
434
  )
425
435
  })
@@ -443,3 +453,78 @@ describe("STRUCTURED_VISION_MODELS", () => {
443
453
  }
444
454
  })
445
455
  })
456
+
457
+ // ---------------------------------------------------------------------------
458
+ // Reasoning effort registry (GPT-5.6 / Claude Sonnet 5 / Claude Opus 4.8)
459
+ // grok-4.5 is DEFERRED — its chat endpoint is not live on the provider yet
460
+ // (2026-07-13); its tests are intentionally omitted here.
461
+ // ---------------------------------------------------------------------------
462
+ describe("reasoning effort registry", () => {
463
+ it("every reasoningEfforts list is a subset of the superset, in ascending order", () => {
464
+ const rank = Object.fromEntries(LLM_REASONING_EFFORTS.map((e, i) => [e, i]))
465
+ for (const m of LLM_MODELS) {
466
+ for (const e of m.reasoningEfforts ?? []) expect(LLM_REASONING_EFFORTS).toContain(e)
467
+ const ranks = (m.reasoningEfforts ?? []).map((e) => rank[e])
468
+ expect([...ranks].sort((a, b) => a - b)).toEqual(ranks)
469
+ }
470
+ })
471
+ it("new models exist with expected tiers", () => {
472
+ expect(getLlmTier("gpt-5.6-luna")).toBe("economy")
473
+ expect(getLlmTier("gpt-5.6-terra")).toBe("standard")
474
+ expect(getLlmTier("gpt-5.6-sol")).toBe("premium")
475
+ expect(getLlmTier("claude-sonnet-5")).toBe("standard")
476
+ expect(getLlmTier("claude-opus-4.8")).toBe("premium")
477
+ expect(getLlmTier("gpt-5.5")).toBe("premium")
478
+ })
479
+ })
480
+
481
+ describe("effectiveReasoningEffort", () => {
482
+ it("passes through a supported level", () => {
483
+ expect(effectiveReasoningEffort("claude-sonnet-5", "max")).toBe("max")
484
+ })
485
+ it("clamps down to the highest supported level ≤ requested", () => {
486
+ expect(effectiveReasoningEffort("gpt-5.4", "xhigh")).toBe("high")
487
+ })
488
+ it("returns undefined when the model has no levels", () => {
489
+ expect(effectiveReasoningEffort("gemini-3-flash", "high")).toBeUndefined()
490
+ })
491
+ it("returns undefined for none on Claude (below its lowest level)", () => {
492
+ expect(effectiveReasoningEffort("claude-sonnet-5", "none")).toBeUndefined()
493
+ })
494
+ it("returns undefined for undefined/garbage input", () => {
495
+ expect(effectiveReasoningEffort("claude-sonnet-5", undefined)).toBeUndefined()
496
+ expect(effectiveReasoningEffort("claude-sonnet-5", "turbo")).toBeUndefined()
497
+ })
498
+ })
499
+
500
+ describe("buildLlmCreditIdentifier effort bump (xhigh/max only)", () => {
501
+ it("economy + max → standard (bare feature)", () => {
502
+ expect(buildLlmCreditIdentifier("llm-chat", "gpt-5.6-luna", "max")).toBe("llm-chat")
503
+ })
504
+ it("standard + xhigh → premium", () => {
505
+ expect(buildLlmCreditIdentifier("llm-chat", "gpt-5.6-terra", "xhigh")).toBe("llm-chat:premium")
506
+ })
507
+ it("premium + max stays premium", () => {
508
+ expect(buildLlmCreditIdentifier("llm-chat", "gpt-5.6-sol", "max")).toBe("llm-chat:premium")
509
+ })
510
+ it("high never bumps", () => {
511
+ expect(buildLlmCreditIdentifier("llm-chat", "claude-sonnet-5", "high")).toBe("llm-chat")
512
+ })
513
+ it("clamp on a partial-list standard model never bumps (sonnet-4.6 @ xhigh → high)", () => {
514
+ expect(buildLlmCreditIdentifier("llm-chat", "claude-sonnet-4.6", "xhigh")).toBe("llm-chat")
515
+ })
516
+ it("bump uses the CLAMPED effort (xhigh on a low/medium/high model clamps to high → no bump)", () => {
517
+ expect(buildLlmCreditIdentifier("llm-chat", "gpt-5.4", "xhigh")).toBe("llm-chat:premium")
518
+ // gpt-5.4 is premium anyway; the real clamp case:
519
+ expect(buildLlmCreditIdentifier("llm-chat", "gemini-3-flash", "max")).toBe("llm-chat:economy")
520
+ })
521
+ it("back-compat: no effort arg → identical to today for every model", () => {
522
+ for (const m of LLM_MODELS) {
523
+ const before = m.tier === "standard" ? "x" : `x:${m.tier}`
524
+ expect(buildLlmCreditIdentifier("x", m.id)).toBe(before)
525
+ }
526
+ })
527
+ it("resolveLlmCreditId reads reasoningEffort from the raw body", () => {
528
+ expect(resolveLlmCreditId("llm-chat", { llmModel: "gpt-5.6-terra", reasoningEffort: "max" })).toBe("llm-chat:premium")
529
+ })
530
+ })
package/src/index.ts CHANGED
@@ -92,6 +92,7 @@ export {
92
92
  SEED_SUPPORT,
93
93
  RENDERING_SPEED_SUPPORT,
94
94
  GUIDANCE_SCALE_SUPPORT,
95
+ SEEDANCE_2_PROVIDERS,
95
96
  SEEDANCE_2_REF_LIMITS,
96
97
  SEEDANCE_2_EXTEND_STITCH,
97
98
  NATIVE_ADAPTIVE_ASPECT,
@@ -251,16 +252,20 @@ export {
251
252
  VIDEO_ANALYSIS_LLM_MODELS,
252
253
  LLM_FEATURE_DEFAULTS,
253
254
  LLM_MODALITY_CAPS,
255
+ LLM_REASONING_EFFORTS,
256
+ EFFORT_TIER_BUMP,
254
257
  getLlmModel,
255
258
  getLlmTier,
256
259
  getLlmModalityCaps,
257
260
  buildLlmCreditIdentifier,
258
261
  resolveLlmCreditId,
259
262
  motionGraphicsFeature,
263
+ effectiveReasoningEffort,
260
264
  type LlmTier,
261
265
  type LlmFeature,
262
266
  type KieApiFormat,
263
267
  type LlmModelDef,
268
+ type LlmReasoningEffort,
264
269
  } from "./llm-models.js"
265
270
 
266
271
  export {
package/src/llm-models.ts CHANGED
@@ -16,6 +16,12 @@
16
16
  export type LlmTier = "economy" | "standard" | "premium"
17
17
  export type KieApiFormat = "chat-completions" | "messages" | "responses"
18
18
 
19
+ export const LLM_REASONING_EFFORTS = ["none", "low", "medium", "high", "xhigh", "max"] as const
20
+ export type LlmReasoningEffort = (typeof LLM_REASONING_EFFORTS)[number]
21
+ /** Levels that bill one tier up. `high` is the Claude-family server default — it never bumps. */
22
+ export const EFFORT_TIER_BUMP: ReadonlySet<LlmReasoningEffort> = new Set(["xhigh", "max"])
23
+ const EFFORT_RANK: Record<LlmReasoningEffort, number> = { none: 0, low: 1, medium: 2, high: 3, xhigh: 4, max: 5 }
24
+
19
25
  export interface LlmModelDef {
20
26
  id: string
21
27
  displayName: string
@@ -41,6 +47,12 @@ export interface LlmModelDef {
41
47
  structuredOutputMode?: "anthropic-tool" | "kie-response-format"
42
48
  /** If set, fallback to direct Anthropic SDK with this model ID when KIE.ai fails */
43
49
  directFallbackModel?: string
50
+ /** Effort levels this model accepts (ascending). Absent/empty = no effort lever, picker hidden. */
51
+ reasoningEfforts?: readonly LlmReasoningEffort[]
52
+ /** false = model rejects `temperature` (Claude 5-era, GPT-5.6). Absent = accepts. */
53
+ supportsTemperature?: false
54
+ /** Claude-only: KIE is the preferred routing, direct Anthropic the fallback. */
55
+ preferKie?: true
44
56
  }
45
57
 
46
58
  export const LLM_MODELS: readonly LlmModelDef[] = [
@@ -81,6 +93,7 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
81
93
  supportsImages: true,
82
94
  maxOutputTokens: 16384,
83
95
  directFallbackModel: "claude-sonnet-4-6",
96
+ reasoningEfforts: ["low", "medium", "high", "max"],
84
97
  },
85
98
  {
86
99
  id: "gpt-5.2",
@@ -117,6 +130,9 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
117
130
  supportsImages: true,
118
131
  maxOutputTokens: 16384,
119
132
  directFallbackModel: "claude-opus-4-7",
133
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
134
+ supportsTemperature: false,
135
+ preferKie: true,
120
136
  },
121
137
  {
122
138
  id: "gpt-5.4",
@@ -128,6 +144,92 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
128
144
  vendor: "openai",
129
145
  supportsImages: true,
130
146
  maxOutputTokens: 16384,
147
+ reasoningEfforts: ["low", "medium", "high"],
148
+ },
149
+ {
150
+ id: "gpt-5.5",
151
+ displayName: "GPT-5.5",
152
+ desc: "Previous flagship GPT, deep reasoning",
153
+ tier: "premium",
154
+ kieFormat: "responses",
155
+ kieSlugOrModel: "gpt-5-5",
156
+ vendor: "openai",
157
+ supportsImages: true,
158
+ maxOutputTokens: 16384,
159
+ reasoningEfforts: ["none", "low", "medium", "high", "xhigh", "max"],
160
+ supportsTemperature: false,
161
+ },
162
+ {
163
+ id: "gpt-5.6-luna",
164
+ displayName: "GPT-5.6 Luna",
165
+ desc: "Fastest GPT-5.6, high-volume workloads",
166
+ tier: "economy",
167
+ kieFormat: "responses",
168
+ kieSlugOrModel: "gpt-5-6-luna",
169
+ vendor: "openai",
170
+ supportsImages: true,
171
+ maxOutputTokens: 16384,
172
+ reasoningEfforts: ["none", "low", "medium", "high", "xhigh", "max"],
173
+ supportsTemperature: false,
174
+ },
175
+ {
176
+ id: "gpt-5.6-terra",
177
+ displayName: "GPT-5.6 Terra",
178
+ desc: "Balanced GPT-5.6 for production work",
179
+ tier: "standard",
180
+ kieFormat: "responses",
181
+ kieSlugOrModel: "gpt-5-6-terra",
182
+ vendor: "openai",
183
+ supportsImages: true,
184
+ maxOutputTokens: 16384,
185
+ reasoningEfforts: ["none", "low", "medium", "high", "xhigh", "max"],
186
+ supportsTemperature: false,
187
+ },
188
+ {
189
+ id: "gpt-5.6-sol",
190
+ displayName: "GPT-5.6 Sol",
191
+ desc: "Flagship GPT-5.6, deepest reasoning",
192
+ tier: "premium",
193
+ kieFormat: "responses",
194
+ kieSlugOrModel: "gpt-5-6-sol",
195
+ vendor: "openai",
196
+ supportsImages: true,
197
+ maxOutputTokens: 16384,
198
+ reasoningEfforts: ["none", "low", "medium", "high", "xhigh", "max"],
199
+ supportsTemperature: false,
200
+ },
201
+ // grok-4.5 deferred — KIE chat endpoint not yet live (2026-07-13); add entry + rate row + docs when it activates.
202
+ {
203
+ id: "claude-sonnet-5",
204
+ displayName: "Claude Sonnet 5",
205
+ desc: "Near-Opus quality at Sonnet cost",
206
+ tier: "standard",
207
+ kieFormat: "messages",
208
+ kieSlugOrModel: "claude-sonnet-5",
209
+ vendor: "anthropic",
210
+ structuredOutputMode: "anthropic-tool",
211
+ supportsImages: true,
212
+ maxOutputTokens: 16384,
213
+ directFallbackModel: "claude-sonnet-5",
214
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
215
+ supportsTemperature: false,
216
+ preferKie: true,
217
+ },
218
+ {
219
+ id: "claude-opus-4.8",
220
+ displayName: "Claude Opus 4.8",
221
+ desc: "Most capable Claude, long-horizon work",
222
+ tier: "premium",
223
+ kieFormat: "messages",
224
+ kieSlugOrModel: "claude-opus-4-8",
225
+ vendor: "anthropic",
226
+ structuredOutputMode: "anthropic-tool",
227
+ supportsImages: true,
228
+ maxOutputTokens: 16384,
229
+ directFallbackModel: "claude-opus-4-8",
230
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
231
+ supportsTemperature: false,
232
+ preferKie: true,
131
233
  },
132
234
  ] as const
133
235
 
@@ -201,6 +303,12 @@ export const LLM_MODALITY_CAPS: Record<string, { image: boolean; video: boolean;
201
303
  "claude-opus-4.7": { image: true, video: false, audio: false },
202
304
  "gpt-5.2": { image: true, video: false, audio: false },
203
305
  "gpt-5.4": { image: true, video: false, audio: false },
306
+ "gpt-5.5": { image: true, video: false, audio: false },
307
+ "gpt-5.6-luna": { image: true, video: false, audio: false },
308
+ "gpt-5.6-terra": { image: true, video: false, audio: false },
309
+ "gpt-5.6-sol": { image: true, video: false, audio: false },
310
+ "claude-sonnet-5": { image: true, video: false, audio: false },
311
+ "claude-opus-4.8": { image: true, video: false, audio: false },
204
312
  }
205
313
 
206
314
  /** Capability lookup with safe default — unknown models get image-only. */
@@ -217,26 +325,51 @@ export function getLlmTier(id: string): LlmTier {
217
325
  return getLlmModel(id)?.tier ?? "standard"
218
326
  }
219
327
 
328
+ /** Highest level the model supports that is ≤ the requested level; undefined = treat as Auto. */
329
+ export function effectiveReasoningEffort(
330
+ modelId: string | undefined,
331
+ requested?: string,
332
+ ): LlmReasoningEffort | undefined {
333
+ if (!requested || !(requested in EFFORT_RANK)) return undefined
334
+ const levels = getLlmModel(modelId ?? "")?.reasoningEfforts
335
+ if (!levels || levels.length === 0) return undefined
336
+ const req = requested as LlmReasoningEffort
337
+ let best: LlmReasoningEffort | undefined
338
+ for (const l of levels) {
339
+ if (EFFORT_RANK[l] <= EFFORT_RANK[req] && (best === undefined || EFFORT_RANK[l] > EFFORT_RANK[best])) best = l
340
+ }
341
+ return best
342
+ }
343
+
220
344
  /**
221
345
  * Build a composite credit identifier for an LLM feature.
222
346
  * - economy tier → "ai-writer:economy"
223
347
  * - standard tier → "ai-writer" (no suffix — backward compatible)
224
348
  * - premium tier → "ai-writer:premium"
349
+ * A reasoning effort of "xhigh" or "max" (after clamping to what the model
350
+ * actually supports) bills one tier up (economy→standard, standard→premium;
351
+ * premium stays premium). `high` is the Claude-family server default and
352
+ * never bumps.
225
353
  */
226
- export function buildLlmCreditIdentifier(feature: string, modelId?: string): string {
354
+ export function buildLlmCreditIdentifier(feature: string, modelId?: string, reasoningEffort?: string): string {
227
355
  if (!modelId) return feature
228
- const tier = getLlmTier(modelId)
356
+ let tier = getLlmTier(modelId)
357
+ const eff = effectiveReasoningEffort(modelId, reasoningEffort)
358
+ if (eff !== undefined && EFFORT_TIER_BUMP.has(eff)) {
359
+ if (tier === "economy") tier = "standard"
360
+ else if (tier === "standard") tier = "premium"
361
+ }
229
362
  if (tier === "standard") return feature
230
363
  return `${feature}:${tier}`
231
364
  }
232
365
 
233
366
  /**
234
- * Resolve llmModel from raw body for creditGuard preHandler (before Zod parsing).
235
- * Returns the credit identifier for the given feature + optional model.
367
+ * Resolve llmModel (+ reasoningEffort) from raw body for creditGuard preHandler
368
+ * (before Zod parsing). Returns the credit identifier for the given feature.
236
369
  */
237
370
  export function resolveLlmCreditId(feature: string, body: unknown): string {
238
- const llmModel = (body as Record<string, unknown>)?.llmModel as string | undefined
239
- return buildLlmCreditIdentifier(feature, llmModel)
371
+ const b = body as Record<string, unknown> | undefined
372
+ return buildLlmCreditIdentifier(feature, b?.llmModel as string | undefined, b?.reasoningEffort as string | undefined)
240
373
  }
241
374
 
242
375
  /** Models capable of video-analysis: capability-derived, never hand-listed (route-enum-sync convention). */
@@ -187,6 +187,10 @@ const VIDEO_RATIOS_HVS345 = ["16:9", "9:16", "1:1", "4:3", "3:4"] as const
187
187
  // (docs.kie.ai/market/bytedance/seedance-2). Kept separate from HVS so the
188
188
  // wider set can't leak to models that don't support it.
189
189
  const VIDEO_RATIOS_SEEDANCE_2 = ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9", "adaptive"] as const
190
+ // HappyHorse 1.1 (t2v + ref2v) — the model's full 9-ratio set incl. 4:5/5:4
191
+ // portrait-social and 21:9/9:21 cinematic (docs.kie.ai/market/happyhorse-1-1).
192
+ // Kept separate so the wider set can't leak to models that don't support it.
193
+ const VIDEO_RATIOS_HAPPYHORSE_11 = ["16:9", "9:16", "1:1", "4:3", "3:4", "4:5", "5:4", "21:9", "9:21"] as const
190
194
 
191
195
  // =============================================================================
192
196
  // IMAGE MODELS
@@ -1377,21 +1381,22 @@ const VIDEO_MODELS: Record<string, ModelCatalogEntry> = {
1377
1381
  { identifier: "wan-2.7-t2v", credits: 19, note: "5s 720p default" },
1378
1382
  ],
1379
1383
  },
1380
- // ── HappyHorse ──
1384
+ // ── HappyHorse (1.1 — ids kept version-less; repointed in place when KIE
1385
+ // delisted 1.0, same param surface, so saved workflows keep working) ──
1381
1386
  "happyhorse": {
1382
1387
  id: "happyhorse",
1383
1388
  kind: "video",
1384
1389
  modes: ["t2v"] as const,
1385
1390
  family: "HappyHorse",
1386
- label: "HappyHorse",
1391
+ label: "HappyHorse 1.1",
1387
1392
  series: "HappyHorse",
1388
- description: "HappyHorse text-to-video — 3–15s at 720p/1080p.",
1393
+ description: "HappyHorse 1.1 text-to-video — 3–15s at 720p/1080p, 9 aspect ratios incl. 21:9/9:21, per-second pricing.",
1389
1394
  useCases: ["motion", "creative"],
1390
- aspectRatios: VIDEO_RATIOS_HVS345,
1395
+ aspectRatios: VIDEO_RATIOS_HAPPYHORSE_11,
1391
1396
  durations: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
1392
1397
  resolutions: ["720p", "1080p"],
1393
1398
  pricing: [
1394
- { identifier: "happyhorse", credits: 13, note: "5s 720p default" },
1399
+ { identifier: "happyhorse", credits: 29, note: "5s 720p default — per-second: ~5.7 cr/s @720p, ~7.3 cr/s @1080p" },
1395
1400
  ],
1396
1401
  },
1397
1402
  "happyhorse-i2v": {
@@ -1399,14 +1404,14 @@ const VIDEO_MODELS: Record<string, ModelCatalogEntry> = {
1399
1404
  kind: "video",
1400
1405
  modes: ["i2v"] as const,
1401
1406
  family: "HappyHorse",
1402
- label: "HappyHorse I2V",
1407
+ label: "HappyHorse 1.1 I2V",
1403
1408
  series: "HappyHorse",
1404
- description: "HappyHorse image-to-video — 3–15s at 720p/1080p, aspect ratio inferred from input image.",
1409
+ description: "HappyHorse 1.1 image-to-video — 3–15s at 720p/1080p, aspect ratio inferred from input image, per-second pricing.",
1405
1410
  useCases: ["motion", "creative"],
1406
1411
  durations: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
1407
1412
  resolutions: ["720p", "1080p"],
1408
1413
  pricing: [
1409
- { identifier: "happyhorse-i2v", credits: 13, note: "5s 720p default" },
1414
+ { identifier: "happyhorse-i2v", credits: 29, note: "5s 720p default — per-second: ~5.7 cr/s @720p, ~7.3 cr/s @1080p" },
1410
1415
  ],
1411
1416
  },
1412
1417
  "happyhorse-ref2v": {
@@ -1414,16 +1419,16 @@ const VIDEO_MODELS: Record<string, ModelCatalogEntry> = {
1414
1419
  kind: "video",
1415
1420
  modes: ["i2v"] as const,
1416
1421
  family: "HappyHorse",
1417
- label: "HappyHorse Ref2V",
1422
+ label: "HappyHorse 1.1 Ref2V",
1418
1423
  series: "HappyHorse",
1419
- description: "HappyHorse reference-to-video — 1–9 reference images, 3–15s at 720p/1080p.",
1424
+ description: "HappyHorse 1.1 reference-to-video — 1–9 reference images, 3–15s at 720p/1080p, per-second pricing.",
1420
1425
  useCases: ["motion", "reference"],
1421
- aspectRatios: VIDEO_RATIOS_HVS345,
1426
+ aspectRatios: VIDEO_RATIOS_HAPPYHORSE_11,
1422
1427
  durations: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
1423
1428
  resolutions: ["720p", "1080p"],
1424
1429
  features: ["reference-image"],
1425
1430
  pricing: [
1426
- { identifier: "happyhorse-ref2v", credits: 15, note: "5s 720p default" },
1431
+ { identifier: "happyhorse-ref2v", credits: 29, note: "5s 720p default — per-second: ~5.7 cr/s @720p, ~7.3 cr/s @1080p" },
1427
1432
  ],
1428
1433
  },
1429
1434
  "happyhorse-edit": {
@@ -1437,7 +1442,7 @@ const VIDEO_MODELS: Record<string, ModelCatalogEntry> = {
1437
1442
  useCases: ["v2v", "restyle"],
1438
1443
  resolutions: ["720p", "1080p"],
1439
1444
  pricing: [
1440
- { identifier: "happyhorse-edit", credits: 20, note: "720p default" },
1445
+ { identifier: "happyhorse-edit", credits: 35, note: "720p default (5s-equivalent flat rate)" },
1441
1446
  ],
1442
1447
  },
1443
1448
  // ── Bytedance video lite/pro ──
@@ -1077,6 +1077,9 @@ export const DURATION_PRICED_PROVIDERS = new Set([
1077
1077
  "seedance-2-fast",
1078
1078
  "seedance-2-mini",
1079
1079
  "grok-imagine-video-1.5",
1080
+ "happyhorse",
1081
+ "happyhorse-i2v",
1082
+ "happyhorse-ref2v",
1080
1083
  ])
1081
1084
 
1082
1085
  /**
@@ -1136,12 +1139,18 @@ export const SEEDANCE_2_REF_LIMITS = {
1136
1139
  * boundary clicks without shifting sync (combineVideos cut+crossfade path).
1137
1140
  */
1138
1141
  export const SEEDANCE_2_EXTEND_STITCH = {
1139
- /** Frames dropped from the END of the source clip. */
1142
+ /** Frames dropped from the END of the source clip (smart-cut FALLBACK —
1143
+ * used only when the PSNR boundary matcher finds no genuine match). */
1140
1144
  trimTailFrames: 4,
1141
- /** Frames dropped from the START of the generated extension. */
1145
+ /** Frames dropped from the START of the generated extension (smart-cut
1146
+ * fallback, see above). */
1142
1147
  trimHeadFrames: 3,
1143
1148
  /** Boundary audio fade length (seconds), timeline-preserving. */
1144
1149
  audioFadeSec: 0.15,
1150
+ /** Seconds of the source's TAIL passed as the @video_1 reference —
1151
+ * spike-validated: a short tail keeps the model focused on continuing
1152
+ * the boundary motion instead of re-staging the whole clip. */
1153
+ referenceTailSeconds: 1,
1145
1154
  } as const
1146
1155
 
1147
1156
  /**
@@ -1243,6 +1252,13 @@ export const RESOLUTION_DURATION_PRICING: Record<string, readonly string[]> = {
1243
1252
  // seedance-2-extend always uses a video ref, so pricing has no -ref
1244
1253
  // dimension — duration tier + ":res" only (rates = seedance-2 -ref + stitch).
1245
1254
  "seedance-2-extend": ["480p", "720p", "1080p"],
1255
+ // HappyHorse 1.1 — per-second billing at two published resolution rates
1256
+ // (kie.ai/happyhorse-1-1: 22.5 cr/s @720p, 29 cr/s @1080p, uniform across
1257
+ // t2v/i2v/ref2v). 720p first = billing default; the KIE-side default is
1258
+ // pinned to 720p via extraParams in backend kie/models.ts so render matches.
1259
+ "happyhorse": ["720p", "1080p"],
1260
+ "happyhorse-i2v": ["720p", "1080p"],
1261
+ "happyhorse-ref2v": ["720p", "1080p"],
1246
1262
  }
1247
1263
 
1248
1264
  /**
@@ -1489,6 +1505,13 @@ export const VIDEO_VARIABLE_PRICING: Record<string, "duration" | "duration+audio
1489
1505
  "grok-imagine-video-1.5": "duration+resolution",
1490
1506
  }
1491
1507
 
1508
+ /** HappyHorse 1.1 per-second tiers — one per allowed duration (3–15s), shared
1509
+ * by all three modes (t2v/i2v/ref2v) which bill at identical published rates. */
1510
+ const HAPPYHORSE_DURATION_TIERS: Array<{ maxSeconds: number; suffix: string }> = Array.from(
1511
+ { length: 13 },
1512
+ (_, i) => ({ maxSeconds: i + 3, suffix: `${i + 3}s` }),
1513
+ )
1514
+
1492
1515
  /**
1493
1516
  * Duration tier breakpoints for variable-priced video models.
1494
1517
  * Maps provider → array of { maxSeconds, suffix } in ascending order.
@@ -1588,6 +1611,14 @@ export const VIDEO_DURATION_TIERS: Record<string, Array<{ maxSeconds: number; su
1588
1611
  { maxSeconds: 14, suffix: "14s" },
1589
1612
  { maxSeconds: 15, suffix: "15s" },
1590
1613
  ],
1614
+ // HappyHorse 1.1 (t2v / i2v / ref2v) — true per-second billing (KIE 22.5 cr/s
1615
+ // @720p, 29 cr/s @1080p, published on kie.ai/happyhorse-1-1). One tier per
1616
+ // allowed second (3–15s) so the composite identifier maps 1:1 to the seeded
1617
+ // price, combined with RESOLUTION_DURATION_PRICING for the ":720p"/":1080p"
1618
+ // suffix (e.g. "happyhorse-i2v:5s:720p").
1619
+ "happyhorse": HAPPYHORSE_DURATION_TIERS,
1620
+ "happyhorse-i2v": HAPPYHORSE_DURATION_TIERS,
1621
+ "happyhorse-ref2v": HAPPYHORSE_DURATION_TIERS,
1591
1622
  }
1592
1623
 
1593
1624
  /**
@@ -15,6 +15,12 @@ export const NODE_MAPPABLE_FIELDS: Readonly<Record<string, readonly string[]>> =
15
15
  // for back-compat with un-migrated workflow JSON). Mirrors text-to-video so
16
16
  // fieldMappings/{} injection AND missing-ref detection work on the live node.
17
17
  "generate-video": ["prompt", "negativePrompt"],
18
+ // Trimmed multi-segment stitch variant of generate-video — prompt only, no
19
+ // negativePrompt field on the node.
20
+ "generate-video-pro": ["prompt"],
21
+ // Span-replace sibling of generate-video-pro — prompt only, no
22
+ // negativePrompt field on the node.
23
+ "edit-video-pro": ["prompt"],
18
24
  "video-analysis": ["analysisFocus", "youtubeUrl"],
19
25
  "video-to-video": ["prompt"],
20
26
  "text-to-speech": ["directText"],
@@ -30,6 +30,16 @@ export const VIDEO_PRODUCER_TYPES: ReadonlySet<string> = new Set([
30
30
  // through to the imageUrl/videoUrl/audioUrl/text default and downstream consumers
31
31
  // could silently misroute the output.
32
32
  "generate-video",
33
+ // Generate Video Pro — Seedance-2-family multi-segment stitch variant of
34
+ // generate-video (same "emits videoUrl" contract; a trimmed provider +
35
+ // handle set). Must mirror generate-video here or its output can't connect
36
+ // downstream (the recurring "cannot connect the outputs" bug class).
37
+ "generate-video-pro",
38
+ // Edit Video Pro — Seedance-2-family span-replace sibling of generate-
39
+ // video-pro (same "emits videoUrl" contract; source video + prompt in,
40
+ // ONE video out). Must mirror generate-video-pro here or its output can't
41
+ // connect downstream (the recurring "cannot connect the outputs" bug class).
42
+ "edit-video-pro",
33
43
  "upload-video",
34
44
  "youtube-video",
35
45
  "combine-videos",
package/src/selector.ts CHANGED
@@ -706,7 +706,16 @@ export function applyRangeIndices(
706
706
  return result
707
707
  }
708
708
 
709
- /** mulberry32 PRNG — deterministic given a seed (any 32-bit signed integer). */
709
+ /** mulberry32 PRNG — deterministic given a seed (any 32-bit signed integer).
710
+ *
711
+ * TWIN WARNING: backend/src/providers/video/audio-fx.ts carries its own
712
+ * private mulberry32 (cosmetically different — `>>> 0` seed masking — but the
713
+ * same sequence). The two are deliberately INDEPENDENT and both frozen: the
714
+ * audio-fx copy seeds every reverb impulse response that the committed
715
+ * characterization goldens (and every customer's rendered reverb waveform)
716
+ * are pinned to. Do NOT "deduplicate" them into one shared export or "sync"
717
+ * one to match the other — a change to either sequence is a silent behavior
718
+ * change in its domain. */
710
719
  function mulberry32(seed: number): () => number {
711
720
  let a = seed | 0
712
721
  return () => {