@nodaro/shared 1.7.0 → 1.10.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/dist/index.cjs +272 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +148 -40
- package/dist/index.d.ts +148 -40
- package/dist/index.js +259 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/credit-identifiers.test.ts +2 -0
- package/src/__tests__/image-ref-limit.test.ts +2 -0
- package/src/__tests__/llm-models.test.ts +101 -9
- package/src/__tests__/prompt-length-limits.test.ts +3 -0
- package/src/__tests__/seedance2-continuation-ref.test.ts +24 -0
- package/src/__tests__/video-analysis-pricing.test.ts +28 -5
- package/src/__tests__/video-analysis.test.ts +1 -1
- package/src/index.ts +16 -0
- package/src/llm-models.ts +191 -15
- package/src/model-catalog.ts +64 -9
- package/src/model-constants.ts +62 -4
- package/src/node-default-mappings.ts +3 -0
- package/src/node-mappable-fields.ts +6 -0
- package/src/producer-types.ts +10 -0
- package/src/selector.ts +10 -1
- package/src/video-analysis-pricing.ts +14 -10
- package/src/video-analysis.ts +12 -4
package/package.json
CHANGED
|
@@ -16,7 +16,9 @@ describe("imageReferenceLimit", () => {
|
|
|
16
16
|
"flux-2-max": 8,
|
|
17
17
|
"gpt-image-2": 16, // → gpt-image-2-i2i
|
|
18
18
|
"seedream-5-lite": 16, // → seedream-5-lite-i2i
|
|
19
|
+
"seedream-5-pro": 16, // → seedream-5-pro-i2i
|
|
19
20
|
"nano-banana-2": 4,
|
|
21
|
+
"nano-banana-2-lite": 10, // schema maxItems (image_urls)
|
|
20
22
|
flux: 4, // → flux-pro-i2i
|
|
21
23
|
"flux-2-klein": 1, // product cap (schema array slices at 5)
|
|
22
24
|
"flux-2-pro": 4, // product cap (BFL schema accepts 8)
|
|
@@ -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
|
|
25
|
-
expect(LLM_MODELS).toHaveLength(
|
|
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
|
|
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(
|
|
66
|
-
expect(tierCounts.standard).toBe(
|
|
67
|
-
expect(tierCounts.premium).toBe(
|
|
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,20 +428,29 @@ 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",
|
|
433
|
+
// responses-format GPTs — KIE text.format json_schema live-verified
|
|
434
|
+
// 2026-07-14 (text AND vision inputs).
|
|
435
|
+
"gpt-5.4",
|
|
436
|
+
"gpt-5.5",
|
|
437
|
+
"gpt-5.6-luna",
|
|
438
|
+
"gpt-5.6-terra",
|
|
439
|
+
"gpt-5.6-sol",
|
|
423
440
|
].sort(),
|
|
424
441
|
)
|
|
425
442
|
})
|
|
426
443
|
|
|
427
|
-
it("includes Anthropic (forced-tool)
|
|
444
|
+
it("includes Anthropic (forced-tool), Gemini (response_format), and OpenAI (responses text.format) vendors", () => {
|
|
428
445
|
const vendors = new Set(STRUCTURED_VISION_MODELS.map((m) => m.vendor))
|
|
429
446
|
expect(vendors).toContain("anthropic")
|
|
430
447
|
expect(vendors).toContain("google")
|
|
448
|
+
expect(vendors).toContain("openai")
|
|
431
449
|
})
|
|
432
450
|
|
|
433
|
-
it("excludes GPT models — no native structured mode (parse+retry only)", () => {
|
|
451
|
+
it("excludes chat-completions GPT models — no native structured mode there (parse+retry only)", () => {
|
|
434
452
|
const ids = STRUCTURED_VISION_MODELS.map((m) => m.id)
|
|
435
453
|
expect(ids).not.toContain("gpt-5.2")
|
|
436
|
-
expect(ids).not.toContain("gpt-5.4")
|
|
437
454
|
})
|
|
438
455
|
|
|
439
456
|
it("every member is vision-capable and has a structuredOutputMode", () => {
|
|
@@ -443,3 +460,78 @@ describe("STRUCTURED_VISION_MODELS", () => {
|
|
|
443
460
|
}
|
|
444
461
|
})
|
|
445
462
|
})
|
|
463
|
+
|
|
464
|
+
// ---------------------------------------------------------------------------
|
|
465
|
+
// Reasoning effort registry (GPT-5.6 / Claude Sonnet 5 / Claude Opus 4.8)
|
|
466
|
+
// grok-4.5 is DEFERRED — its chat endpoint is not live on the provider yet
|
|
467
|
+
// (2026-07-13); its tests are intentionally omitted here.
|
|
468
|
+
// ---------------------------------------------------------------------------
|
|
469
|
+
describe("reasoning effort registry", () => {
|
|
470
|
+
it("every reasoningEfforts list is a subset of the superset, in ascending order", () => {
|
|
471
|
+
const rank = Object.fromEntries(LLM_REASONING_EFFORTS.map((e, i) => [e, i]))
|
|
472
|
+
for (const m of LLM_MODELS) {
|
|
473
|
+
for (const e of m.reasoningEfforts ?? []) expect(LLM_REASONING_EFFORTS).toContain(e)
|
|
474
|
+
const ranks = (m.reasoningEfforts ?? []).map((e) => rank[e])
|
|
475
|
+
expect([...ranks].sort((a, b) => a - b)).toEqual(ranks)
|
|
476
|
+
}
|
|
477
|
+
})
|
|
478
|
+
it("new models exist with expected tiers", () => {
|
|
479
|
+
expect(getLlmTier("gpt-5.6-luna")).toBe("economy")
|
|
480
|
+
expect(getLlmTier("gpt-5.6-terra")).toBe("standard")
|
|
481
|
+
expect(getLlmTier("gpt-5.6-sol")).toBe("premium")
|
|
482
|
+
expect(getLlmTier("claude-sonnet-5")).toBe("standard")
|
|
483
|
+
expect(getLlmTier("claude-opus-4.8")).toBe("premium")
|
|
484
|
+
expect(getLlmTier("gpt-5.5")).toBe("premium")
|
|
485
|
+
})
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
describe("effectiveReasoningEffort", () => {
|
|
489
|
+
it("passes through a supported level", () => {
|
|
490
|
+
expect(effectiveReasoningEffort("claude-sonnet-5", "max")).toBe("max")
|
|
491
|
+
})
|
|
492
|
+
it("clamps down to the highest supported level ≤ requested", () => {
|
|
493
|
+
expect(effectiveReasoningEffort("gpt-5.4", "xhigh")).toBe("high")
|
|
494
|
+
})
|
|
495
|
+
it("returns undefined when the model has no levels", () => {
|
|
496
|
+
expect(effectiveReasoningEffort("gemini-3-flash", "high")).toBeUndefined()
|
|
497
|
+
})
|
|
498
|
+
it("returns undefined for none on Claude (below its lowest level)", () => {
|
|
499
|
+
expect(effectiveReasoningEffort("claude-sonnet-5", "none")).toBeUndefined()
|
|
500
|
+
})
|
|
501
|
+
it("returns undefined for undefined/garbage input", () => {
|
|
502
|
+
expect(effectiveReasoningEffort("claude-sonnet-5", undefined)).toBeUndefined()
|
|
503
|
+
expect(effectiveReasoningEffort("claude-sonnet-5", "turbo")).toBeUndefined()
|
|
504
|
+
})
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
describe("buildLlmCreditIdentifier effort bump (xhigh/max only)", () => {
|
|
508
|
+
it("economy + max → standard (bare feature)", () => {
|
|
509
|
+
expect(buildLlmCreditIdentifier("llm-chat", "gpt-5.6-luna", "max")).toBe("llm-chat")
|
|
510
|
+
})
|
|
511
|
+
it("standard + xhigh → premium", () => {
|
|
512
|
+
expect(buildLlmCreditIdentifier("llm-chat", "gpt-5.6-terra", "xhigh")).toBe("llm-chat:premium")
|
|
513
|
+
})
|
|
514
|
+
it("premium + max stays premium", () => {
|
|
515
|
+
expect(buildLlmCreditIdentifier("llm-chat", "gpt-5.6-sol", "max")).toBe("llm-chat:premium")
|
|
516
|
+
})
|
|
517
|
+
it("high never bumps", () => {
|
|
518
|
+
expect(buildLlmCreditIdentifier("llm-chat", "claude-sonnet-5", "high")).toBe("llm-chat")
|
|
519
|
+
})
|
|
520
|
+
it("clamp on a partial-list standard model never bumps (sonnet-4.6 @ xhigh → high)", () => {
|
|
521
|
+
expect(buildLlmCreditIdentifier("llm-chat", "claude-sonnet-4.6", "xhigh")).toBe("llm-chat")
|
|
522
|
+
})
|
|
523
|
+
it("bump uses the CLAMPED effort (xhigh on a low/medium/high model clamps to high → no bump)", () => {
|
|
524
|
+
expect(buildLlmCreditIdentifier("llm-chat", "gpt-5.4", "xhigh")).toBe("llm-chat:premium")
|
|
525
|
+
// gpt-5.4 is premium anyway; the real clamp case:
|
|
526
|
+
expect(buildLlmCreditIdentifier("llm-chat", "gemini-3-flash", "max")).toBe("llm-chat:economy")
|
|
527
|
+
})
|
|
528
|
+
it("back-compat: no effort arg → identical to today for every model", () => {
|
|
529
|
+
for (const m of LLM_MODELS) {
|
|
530
|
+
const before = m.tier === "standard" ? "x" : `x:${m.tier}`
|
|
531
|
+
expect(buildLlmCreditIdentifier("x", m.id)).toBe(before)
|
|
532
|
+
}
|
|
533
|
+
})
|
|
534
|
+
it("resolveLlmCreditId reads reasoningEffort from the raw body", () => {
|
|
535
|
+
expect(resolveLlmCreditId("llm-chat", { llmModel: "gpt-5.6-terra", reasoningEffort: "max" })).toBe("llm-chat:premium")
|
|
536
|
+
})
|
|
537
|
+
})
|
|
@@ -28,6 +28,7 @@ describe("per-model prompt length limits", () => {
|
|
|
28
28
|
describe("getMaxImagePromptChars", () => {
|
|
29
29
|
it("returns the verified higher caps (provider supports more than 5000)", () => {
|
|
30
30
|
expect(getMaxImagePromptChars("nano-banana-2")).toBe(20000)
|
|
31
|
+
expect(getMaxImagePromptChars("nano-banana-2-lite")).toBe(20000)
|
|
31
32
|
expect(getMaxImagePromptChars("nano-banana-pro")).toBe(20000)
|
|
32
33
|
expect(getMaxImagePromptChars("gpt-image-2-i2i")).toBe(20000)
|
|
33
34
|
})
|
|
@@ -44,6 +45,8 @@ describe("per-model prompt length limits", () => {
|
|
|
44
45
|
it("falls back to IMAGE_PROMPT_MAX for verified-default + unknown providers", () => {
|
|
45
46
|
expect(getMaxImagePromptChars("flux")).toBe(IMAGE_PROMPT_MAX)
|
|
46
47
|
expect(getMaxImagePromptChars("imagen4")).toBe(IMAGE_PROMPT_MAX)
|
|
48
|
+
expect(getMaxImagePromptChars("seedream-5-pro")).toBe(IMAGE_PROMPT_MAX) // verified 5000 in KIE schema
|
|
49
|
+
expect(getMaxImagePromptChars("seedream-5-pro-i2i")).toBe(IMAGE_PROMPT_MAX) // verified 5000 in KIE schema
|
|
47
50
|
expect(getMaxImagePromptChars("grok-i2i")).toBe(IMAGE_PROMPT_MAX) // 390000 doc = sanity-capped
|
|
48
51
|
expect(getMaxImagePromptChars("flux-kontext")).toBe(IMAGE_PROMPT_MAX) // UNVERIFIED
|
|
49
52
|
expect(getMaxImagePromptChars(undefined)).toBe(IMAGE_PROMPT_MAX)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
SEEDANCE_2_CONTINUATION_REF_SEC,
|
|
4
|
+
SEEDANCE_2_R2V_MIN_REF_VIDEO_SEC,
|
|
5
|
+
SEEDANCE_2_EXTEND_STITCH,
|
|
6
|
+
} from "../model-constants.js"
|
|
7
|
+
|
|
8
|
+
/** KIE hard-rejects r2v reference videos below the floor: "the parameter
|
|
9
|
+
* video duration (seconds) specified in the request must be greater than or
|
|
10
|
+
* equal to 1.8 for model dreamina-seedance-2-0-fast in r2v" (2026-07-13,
|
|
11
|
+
* job dbf95612 — the original 1.0s tails made every generate-video-pro
|
|
12
|
+
* continuation segment fail deterministically). Everything this platform
|
|
13
|
+
* sends as a Seedance-2 video reference cuts SEEDANCE_2_CONTINUATION_REF_SEC
|
|
14
|
+
* seconds; do NOT "optimize" it back below the floor. The private-plugin
|
|
15
|
+
* twin constants (nodaro-cloud-plugins chain.ts TAIL_SEC / bridge-math.ts
|
|
16
|
+
* MIN_REF) are guarded by that repo's r2v-ref-floor.test.ts — keep in sync. */
|
|
17
|
+
describe("seedance-2 continuation-reference floor (do not regress)", () => {
|
|
18
|
+
it("the platform's continuation-ref length clears KIE's r2v minimum", () => {
|
|
19
|
+
expect(SEEDANCE_2_CONTINUATION_REF_SEC).toBeGreaterThanOrEqual(SEEDANCE_2_R2V_MIN_REF_VIDEO_SEC)
|
|
20
|
+
})
|
|
21
|
+
it("the extend node's tail IS the shared continuation-ref length (single source)", () => {
|
|
22
|
+
expect(SEEDANCE_2_EXTEND_STITCH.referenceTailSeconds).toBe(SEEDANCE_2_CONTINUATION_REF_SEC)
|
|
23
|
+
})
|
|
24
|
+
})
|
|
@@ -5,13 +5,16 @@ import {
|
|
|
5
5
|
pickVideoAnalysisBucket, buildVideoAnalysisCreditId, bucketSecondsFromCreditId,
|
|
6
6
|
videoAnalysisNumWindows,
|
|
7
7
|
} from "../video-analysis-pricing.js"
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
VIDEO_ANALYSIS_LLM_MODELS, VIDEO_ANALYSIS_TIERS, VIDEO_ANALYSIS_TIER_ORDER,
|
|
10
|
+
DEFAULT_VIDEO_ANALYSIS_TIER, DEFAULT_VIDEO_ANALYSIS_MODEL, resolveVideoAnalysisModel,
|
|
11
|
+
} from "../llm-models.js"
|
|
9
12
|
|
|
10
13
|
// The measured-rate constants and the $-derived `videoAnalysisBucketCredits`
|
|
11
|
-
// formula
|
|
12
|
-
// tests (
|
|
13
|
-
// cross-check against VIDEO_ANALYSIS_BUCKET_CREDITS
|
|
14
|
-
//
|
|
14
|
+
// formula are PRIVATE, in @nodaroai/cloud-plugins
|
|
15
|
+
// (src/plugins/video-analysis/cost.ts) — its tests (the worked-example
|
|
16
|
+
// bucket-credit values and the cross-check against VIDEO_ANALYSIS_BUCKET_CREDITS
|
|
17
|
+
// below) live in that private package's __tests__/cost.test.ts. This file
|
|
15
18
|
// covers only the NON-monetary duration-bucketing, window-batching, and
|
|
16
19
|
// credit-id-construction logic that stays in the published package.
|
|
17
20
|
|
|
@@ -40,6 +43,26 @@ describe("video-analysis-pricing", () => {
|
|
|
40
43
|
expect(VIDEO_ANALYSIS_LLM_MODELS).toEqual(["gemini-3-flash", "gemini-3.1-pro"])
|
|
41
44
|
})
|
|
42
45
|
|
|
46
|
+
it("tier layer: every tier maps to a real model AND every model is tier-reachable (no vendor leak)", () => {
|
|
47
|
+
// Adding a video-analysis model without a tier would silently leave it
|
|
48
|
+
// unreachable / unnamed — this fails until a tier decision is made.
|
|
49
|
+
const tierTargets = Object.values(VIDEO_ANALYSIS_TIERS)
|
|
50
|
+
for (const m of tierTargets) expect(VIDEO_ANALYSIS_LLM_MODELS).toContain(m)
|
|
51
|
+
for (const m of VIDEO_ANALYSIS_LLM_MODELS) expect(tierTargets).toContain(m)
|
|
52
|
+
expect(new Set(VIDEO_ANALYSIS_TIER_ORDER)).toEqual(new Set(Object.keys(VIDEO_ANALYSIS_TIERS)))
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it("resolveVideoAnalysisModel: tier → model, raw model passthrough, default pro on empty/unknown", () => {
|
|
56
|
+
expect(DEFAULT_VIDEO_ANALYSIS_TIER).toBe("pro")
|
|
57
|
+
expect(DEFAULT_VIDEO_ANALYSIS_MODEL).toBe("gemini-3.1-pro")
|
|
58
|
+
expect(resolveVideoAnalysisModel("pro")).toBe("gemini-3.1-pro")
|
|
59
|
+
expect(resolveVideoAnalysisModel("fast")).toBe("gemini-3-flash")
|
|
60
|
+
expect(resolveVideoAnalysisModel("gemini-3-flash")).toBe("gemini-3-flash") // raw passthrough
|
|
61
|
+
expect(resolveVideoAnalysisModel(undefined)).toBe("gemini-3.1-pro") // default → pro
|
|
62
|
+
expect(resolveVideoAnalysisModel("")).toBe("gemini-3.1-pro")
|
|
63
|
+
expect(resolveVideoAnalysisModel("nonsense")).toBe("gemini-3.1-pro") // unknown → default, never throws
|
|
64
|
+
})
|
|
65
|
+
|
|
43
66
|
// Full drift-detection against the live $-formula lives in
|
|
44
67
|
// backend/src/lib/pricing/__tests__/video-analysis-cost.test.ts (this
|
|
45
68
|
// package cannot see the formula post-S5). This is a lightweight shape
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "../video-analysis.js"
|
|
8
8
|
|
|
9
9
|
const slot: EntitySlot = { slotId: "hero", label: "Protagonist", source: "wired-character", role: "person", description: "tan man, mustache, black tee" }
|
|
10
|
-
const baseScene = { startSec: 0, endSec: 4, label: "Hook", shotType: "Medium Close-Up", camera: "slow push-in", visual: "{slot:hero} juggles a ball", audio: { mode: "speech" as const, content: "As a kid…", voice: "male, warm" } }
|
|
10
|
+
const baseScene = { startSec: 0, endSec: 4, label: "Hook", shotType: "Medium Close-Up", camera: "slow push-in", visual: "{slot:hero} juggles a ball", audio: [{ mode: "speech" as const, content: "As a kid…", voice: "male, warm" }] }
|
|
11
11
|
|
|
12
12
|
describe("windowAnalysisSchema", () => {
|
|
13
13
|
it("accepts a zero-scene window (quiet footage is a VALID result)", () => {
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ export {
|
|
|
19
19
|
MAX_IMAGE_PROMPT_CHARS_BY_PROVIDER,
|
|
20
20
|
getMaxImagePromptChars,
|
|
21
21
|
PROMPT_HARD_CEILING,
|
|
22
|
+
LLM_TEXT_INPUT_MAX,
|
|
22
23
|
MAX_VIDEO_PROMPT_CHARS_BY_PROVIDER,
|
|
23
24
|
getMaxVideoPromptChars,
|
|
24
25
|
NEGATIVE_PROMPT_MAX,
|
|
@@ -92,8 +93,11 @@ export {
|
|
|
92
93
|
SEED_SUPPORT,
|
|
93
94
|
RENDERING_SPEED_SUPPORT,
|
|
94
95
|
GUIDANCE_SCALE_SUPPORT,
|
|
96
|
+
SEEDANCE_2_PROVIDERS,
|
|
95
97
|
SEEDANCE_2_REF_LIMITS,
|
|
96
98
|
SEEDANCE_2_EXTEND_STITCH,
|
|
99
|
+
SEEDANCE_2_R2V_MIN_REF_VIDEO_SEC,
|
|
100
|
+
SEEDANCE_2_CONTINUATION_REF_SEC,
|
|
97
101
|
NATIVE_ADAPTIVE_ASPECT,
|
|
98
102
|
VIDEO_REF_LIMITS_BY_PROVIDER,
|
|
99
103
|
VIDEO_PROVIDERS_REQUIRING_IMAGE,
|
|
@@ -249,18 +253,30 @@ export {
|
|
|
249
253
|
LLM_MODEL_IDS,
|
|
250
254
|
STRUCTURED_VISION_MODELS,
|
|
251
255
|
VIDEO_ANALYSIS_LLM_MODELS,
|
|
256
|
+
VIDEO_ANALYSIS_TIERS,
|
|
257
|
+
type VideoAnalysisTier,
|
|
258
|
+
VIDEO_ANALYSIS_TIER_ORDER,
|
|
259
|
+
DEFAULT_VIDEO_ANALYSIS_TIER,
|
|
260
|
+
DEFAULT_VIDEO_ANALYSIS_MODEL,
|
|
261
|
+
VIDEO_ANALYSIS_TIER_LABELS,
|
|
262
|
+
isVideoAnalysisTier,
|
|
263
|
+
resolveVideoAnalysisModel,
|
|
252
264
|
LLM_FEATURE_DEFAULTS,
|
|
253
265
|
LLM_MODALITY_CAPS,
|
|
266
|
+
LLM_REASONING_EFFORTS,
|
|
267
|
+
EFFORT_TIER_BUMP,
|
|
254
268
|
getLlmModel,
|
|
255
269
|
getLlmTier,
|
|
256
270
|
getLlmModalityCaps,
|
|
257
271
|
buildLlmCreditIdentifier,
|
|
258
272
|
resolveLlmCreditId,
|
|
259
273
|
motionGraphicsFeature,
|
|
274
|
+
effectiveReasoningEffort,
|
|
260
275
|
type LlmTier,
|
|
261
276
|
type LlmFeature,
|
|
262
277
|
type KieApiFormat,
|
|
263
278
|
type LlmModelDef,
|
|
279
|
+
type LlmReasoningEffort,
|
|
264
280
|
} from "./llm-models.js"
|
|
265
281
|
|
|
266
282
|
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
|
|
@@ -31,16 +37,26 @@ export interface LlmModelDef {
|
|
|
31
37
|
maxOutputTokens: number
|
|
32
38
|
/**
|
|
33
39
|
* How this model can be forced into schema-valid structured output.
|
|
34
|
-
* - "anthropic-tool"
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* -
|
|
38
|
-
*
|
|
40
|
+
* - "anthropic-tool" → forced tool_choice on the Claude wire (direct SDK
|
|
41
|
+
* guaranteed; KIE's proxy re-serializes the tool call
|
|
42
|
+
* as a `<tool_calls>` text tag — decoded in llm-client).
|
|
43
|
+
* - "kie-response-format" → KIE chat-completions `response_format: json_schema`
|
|
44
|
+
* (verified enforced for Gemini via KIE).
|
|
45
|
+
* - "responses-json-schema" → KIE codex/v1/responses `text.format: json_schema`
|
|
46
|
+
* (live-verified 2026-07-14 for gpt-5.4/5.5 and the
|
|
47
|
+
* whole GPT-5.6 family, text AND vision inputs).
|
|
48
|
+
* - undefined → no native mode; callers fall back to parse + retry.
|
|
39
49
|
* Capability-driven so `llmCompleteStructured` never hardcodes provider ids.
|
|
40
50
|
*/
|
|
41
|
-
structuredOutputMode?: "anthropic-tool" | "kie-response-format"
|
|
51
|
+
structuredOutputMode?: "anthropic-tool" | "kie-response-format" | "responses-json-schema"
|
|
42
52
|
/** If set, fallback to direct Anthropic SDK with this model ID when KIE.ai fails */
|
|
43
53
|
directFallbackModel?: string
|
|
54
|
+
/** Effort levels this model accepts (ascending). Absent/empty = no effort lever, picker hidden. */
|
|
55
|
+
reasoningEfforts?: readonly LlmReasoningEffort[]
|
|
56
|
+
/** false = model rejects `temperature` (Claude 5-era, GPT-5.6). Absent = accepts. */
|
|
57
|
+
supportsTemperature?: false
|
|
58
|
+
/** Claude-only: KIE is the preferred routing, direct Anthropic the fallback. */
|
|
59
|
+
preferKie?: true
|
|
44
60
|
}
|
|
45
61
|
|
|
46
62
|
export const LLM_MODELS: readonly LlmModelDef[] = [
|
|
@@ -81,6 +97,7 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
|
|
|
81
97
|
supportsImages: true,
|
|
82
98
|
maxOutputTokens: 16384,
|
|
83
99
|
directFallbackModel: "claude-sonnet-4-6",
|
|
100
|
+
reasoningEfforts: ["low", "medium", "high", "max"],
|
|
84
101
|
},
|
|
85
102
|
{
|
|
86
103
|
id: "gpt-5.2",
|
|
@@ -117,6 +134,9 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
|
|
|
117
134
|
supportsImages: true,
|
|
118
135
|
maxOutputTokens: 16384,
|
|
119
136
|
directFallbackModel: "claude-opus-4-7",
|
|
137
|
+
reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
|
|
138
|
+
supportsTemperature: false,
|
|
139
|
+
preferKie: true,
|
|
120
140
|
},
|
|
121
141
|
{
|
|
122
142
|
id: "gpt-5.4",
|
|
@@ -126,8 +146,99 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
|
|
|
126
146
|
kieFormat: "responses",
|
|
127
147
|
kieSlugOrModel: "gpt-5-4",
|
|
128
148
|
vendor: "openai",
|
|
149
|
+
structuredOutputMode: "responses-json-schema",
|
|
150
|
+
supportsImages: true,
|
|
151
|
+
maxOutputTokens: 16384,
|
|
152
|
+
reasoningEfforts: ["low", "medium", "high"],
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: "gpt-5.5",
|
|
156
|
+
displayName: "GPT-5.5",
|
|
157
|
+
desc: "Previous flagship GPT, deep reasoning",
|
|
158
|
+
tier: "premium",
|
|
159
|
+
kieFormat: "responses",
|
|
160
|
+
kieSlugOrModel: "gpt-5-5",
|
|
161
|
+
vendor: "openai",
|
|
162
|
+
structuredOutputMode: "responses-json-schema",
|
|
163
|
+
supportsImages: true,
|
|
164
|
+
maxOutputTokens: 16384,
|
|
165
|
+
reasoningEfforts: ["none", "low", "medium", "high", "xhigh", "max"],
|
|
166
|
+
supportsTemperature: false,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: "gpt-5.6-luna",
|
|
170
|
+
displayName: "GPT-5.6 Luna",
|
|
171
|
+
desc: "Fastest GPT-5.6, high-volume workloads",
|
|
172
|
+
tier: "economy",
|
|
173
|
+
kieFormat: "responses",
|
|
174
|
+
kieSlugOrModel: "gpt-5-6-luna",
|
|
175
|
+
vendor: "openai",
|
|
176
|
+
structuredOutputMode: "responses-json-schema",
|
|
177
|
+
supportsImages: true,
|
|
178
|
+
maxOutputTokens: 16384,
|
|
179
|
+
reasoningEfforts: ["none", "low", "medium", "high", "xhigh", "max"],
|
|
180
|
+
supportsTemperature: false,
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: "gpt-5.6-terra",
|
|
184
|
+
displayName: "GPT-5.6 Terra",
|
|
185
|
+
desc: "Balanced GPT-5.6 for production work",
|
|
186
|
+
tier: "standard",
|
|
187
|
+
kieFormat: "responses",
|
|
188
|
+
kieSlugOrModel: "gpt-5-6-terra",
|
|
189
|
+
vendor: "openai",
|
|
190
|
+
structuredOutputMode: "responses-json-schema",
|
|
191
|
+
supportsImages: true,
|
|
192
|
+
maxOutputTokens: 16384,
|
|
193
|
+
reasoningEfforts: ["none", "low", "medium", "high", "xhigh", "max"],
|
|
194
|
+
supportsTemperature: false,
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: "gpt-5.6-sol",
|
|
198
|
+
displayName: "GPT-5.6 Sol",
|
|
199
|
+
desc: "Flagship GPT-5.6, deepest reasoning",
|
|
200
|
+
tier: "premium",
|
|
201
|
+
kieFormat: "responses",
|
|
202
|
+
kieSlugOrModel: "gpt-5-6-sol",
|
|
203
|
+
vendor: "openai",
|
|
204
|
+
structuredOutputMode: "responses-json-schema",
|
|
205
|
+
supportsImages: true,
|
|
206
|
+
maxOutputTokens: 16384,
|
|
207
|
+
reasoningEfforts: ["none", "low", "medium", "high", "xhigh", "max"],
|
|
208
|
+
supportsTemperature: false,
|
|
209
|
+
},
|
|
210
|
+
// grok-4.5 deferred — KIE chat endpoint not yet live (2026-07-13); add entry + rate row + docs when it activates.
|
|
211
|
+
{
|
|
212
|
+
id: "claude-sonnet-5",
|
|
213
|
+
displayName: "Claude Sonnet 5",
|
|
214
|
+
desc: "Near-Opus quality at Sonnet cost",
|
|
215
|
+
tier: "standard",
|
|
216
|
+
kieFormat: "messages",
|
|
217
|
+
kieSlugOrModel: "claude-sonnet-5",
|
|
218
|
+
vendor: "anthropic",
|
|
219
|
+
structuredOutputMode: "anthropic-tool",
|
|
220
|
+
supportsImages: true,
|
|
221
|
+
maxOutputTokens: 16384,
|
|
222
|
+
directFallbackModel: "claude-sonnet-5",
|
|
223
|
+
reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
|
|
224
|
+
supportsTemperature: false,
|
|
225
|
+
preferKie: true,
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
id: "claude-opus-4.8",
|
|
229
|
+
displayName: "Claude Opus 4.8",
|
|
230
|
+
desc: "Most capable Claude, long-horizon work",
|
|
231
|
+
tier: "premium",
|
|
232
|
+
kieFormat: "messages",
|
|
233
|
+
kieSlugOrModel: "claude-opus-4-8",
|
|
234
|
+
vendor: "anthropic",
|
|
235
|
+
structuredOutputMode: "anthropic-tool",
|
|
129
236
|
supportsImages: true,
|
|
130
237
|
maxOutputTokens: 16384,
|
|
238
|
+
directFallbackModel: "claude-opus-4-8",
|
|
239
|
+
reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
|
|
240
|
+
supportsTemperature: false,
|
|
241
|
+
preferKie: true,
|
|
131
242
|
},
|
|
132
243
|
] as const
|
|
133
244
|
|
|
@@ -136,9 +247,10 @@ export const LLM_MODEL_IDS = LLM_MODELS.map((m) => m.id)
|
|
|
136
247
|
/** Vision models that can return GUARANTEED structured output — the
|
|
137
248
|
* describe-to-picker analyzer forces a schema over an image, so its model
|
|
138
249
|
* pickers AND the backend route gate offer exactly these: Anthropic (forced
|
|
139
|
-
* tool)
|
|
140
|
-
*
|
|
141
|
-
* picker, the config panel, and the route gate
|
|
250
|
+
* tool), Gemini (KIE `response_format`), and GPT responses-format models
|
|
251
|
+
* (KIE `text.format` json_schema — vision+schema live-verified 2026-07-14).
|
|
252
|
+
* Single source of truth so the picker, the config panel, and the route gate
|
|
253
|
+
* can't drift. */
|
|
142
254
|
export const STRUCTURED_VISION_MODELS = LLM_MODELS.filter(
|
|
143
255
|
(m) => m.supportsImages && m.structuredOutputMode != null,
|
|
144
256
|
)
|
|
@@ -201,6 +313,12 @@ export const LLM_MODALITY_CAPS: Record<string, { image: boolean; video: boolean;
|
|
|
201
313
|
"claude-opus-4.7": { image: true, video: false, audio: false },
|
|
202
314
|
"gpt-5.2": { image: true, video: false, audio: false },
|
|
203
315
|
"gpt-5.4": { image: true, video: false, audio: false },
|
|
316
|
+
"gpt-5.5": { image: true, video: false, audio: false },
|
|
317
|
+
"gpt-5.6-luna": { image: true, video: false, audio: false },
|
|
318
|
+
"gpt-5.6-terra": { image: true, video: false, audio: false },
|
|
319
|
+
"gpt-5.6-sol": { image: true, video: false, audio: false },
|
|
320
|
+
"claude-sonnet-5": { image: true, video: false, audio: false },
|
|
321
|
+
"claude-opus-4.8": { image: true, video: false, audio: false },
|
|
204
322
|
}
|
|
205
323
|
|
|
206
324
|
/** Capability lookup with safe default — unknown models get image-only. */
|
|
@@ -217,29 +335,87 @@ export function getLlmTier(id: string): LlmTier {
|
|
|
217
335
|
return getLlmModel(id)?.tier ?? "standard"
|
|
218
336
|
}
|
|
219
337
|
|
|
338
|
+
/** Highest level the model supports that is ≤ the requested level; undefined = treat as Auto. */
|
|
339
|
+
export function effectiveReasoningEffort(
|
|
340
|
+
modelId: string | undefined,
|
|
341
|
+
requested?: string,
|
|
342
|
+
): LlmReasoningEffort | undefined {
|
|
343
|
+
if (!requested || !(requested in EFFORT_RANK)) return undefined
|
|
344
|
+
const levels = getLlmModel(modelId ?? "")?.reasoningEfforts
|
|
345
|
+
if (!levels || levels.length === 0) return undefined
|
|
346
|
+
const req = requested as LlmReasoningEffort
|
|
347
|
+
let best: LlmReasoningEffort | undefined
|
|
348
|
+
for (const l of levels) {
|
|
349
|
+
if (EFFORT_RANK[l] <= EFFORT_RANK[req] && (best === undefined || EFFORT_RANK[l] > EFFORT_RANK[best])) best = l
|
|
350
|
+
}
|
|
351
|
+
return best
|
|
352
|
+
}
|
|
353
|
+
|
|
220
354
|
/**
|
|
221
355
|
* Build a composite credit identifier for an LLM feature.
|
|
222
356
|
* - economy tier → "ai-writer:economy"
|
|
223
357
|
* - standard tier → "ai-writer" (no suffix — backward compatible)
|
|
224
358
|
* - premium tier → "ai-writer:premium"
|
|
359
|
+
* A reasoning effort of "xhigh" or "max" (after clamping to what the model
|
|
360
|
+
* actually supports) bills one tier up (economy→standard, standard→premium;
|
|
361
|
+
* premium stays premium). `high` is the Claude-family server default and
|
|
362
|
+
* never bumps.
|
|
225
363
|
*/
|
|
226
|
-
export function buildLlmCreditIdentifier(feature: string, modelId?: string): string {
|
|
364
|
+
export function buildLlmCreditIdentifier(feature: string, modelId?: string, reasoningEffort?: string): string {
|
|
227
365
|
if (!modelId) return feature
|
|
228
|
-
|
|
366
|
+
let tier = getLlmTier(modelId)
|
|
367
|
+
const eff = effectiveReasoningEffort(modelId, reasoningEffort)
|
|
368
|
+
if (eff !== undefined && EFFORT_TIER_BUMP.has(eff)) {
|
|
369
|
+
if (tier === "economy") tier = "standard"
|
|
370
|
+
else if (tier === "standard") tier = "premium"
|
|
371
|
+
}
|
|
229
372
|
if (tier === "standard") return feature
|
|
230
373
|
return `${feature}:${tier}`
|
|
231
374
|
}
|
|
232
375
|
|
|
233
376
|
/**
|
|
234
|
-
* Resolve llmModel from raw body for creditGuard preHandler
|
|
235
|
-
* Returns the credit identifier for the given feature
|
|
377
|
+
* Resolve llmModel (+ reasoningEffort) from raw body for creditGuard preHandler
|
|
378
|
+
* (before Zod parsing). Returns the credit identifier for the given feature.
|
|
236
379
|
*/
|
|
237
380
|
export function resolveLlmCreditId(feature: string, body: unknown): string {
|
|
238
|
-
const
|
|
239
|
-
return buildLlmCreditIdentifier(feature, llmModel)
|
|
381
|
+
const b = body as Record<string, unknown> | undefined
|
|
382
|
+
return buildLlmCreditIdentifier(feature, b?.llmModel as string | undefined, b?.reasoningEffort as string | undefined)
|
|
240
383
|
}
|
|
241
384
|
|
|
242
385
|
/** Models capable of video-analysis: capability-derived, never hand-listed (route-enum-sync convention). */
|
|
243
386
|
export const VIDEO_ANALYSIS_LLM_MODELS: string[] = LLM_MODELS
|
|
244
387
|
.filter((m) => getLlmModalityCaps(m.id).video && getLlmModalityCaps(m.id).audio)
|
|
245
388
|
.map((m) => m.id)
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Video-analysis quality TIERS — the ONLY analyzer identifiers exposed to users
|
|
392
|
+
* (API, UI, docs). Each maps to an internal analysis model; the underlying
|
|
393
|
+
* vendor/model name is never surfaced. Default is `pro`. The guard test
|
|
394
|
+
* (video-analysis-pricing.test.ts) asserts every tier target is a real
|
|
395
|
+
* VIDEO_ANALYSIS_LLM_MODELS member AND every such model is reachable by a tier,
|
|
396
|
+
* so adding a video model forces a tier decision instead of silently leaking.
|
|
397
|
+
*/
|
|
398
|
+
export const VIDEO_ANALYSIS_TIERS = { fast: "gemini-3-flash", pro: "gemini-3.1-pro" } as const
|
|
399
|
+
export type VideoAnalysisTier = keyof typeof VIDEO_ANALYSIS_TIERS
|
|
400
|
+
/** UI/listing order — recommended (pro) first. */
|
|
401
|
+
export const VIDEO_ANALYSIS_TIER_ORDER = ["pro", "fast"] as const
|
|
402
|
+
export const DEFAULT_VIDEO_ANALYSIS_TIER: VideoAnalysisTier = "pro"
|
|
403
|
+
export const DEFAULT_VIDEO_ANALYSIS_MODEL: string = VIDEO_ANALYSIS_TIERS[DEFAULT_VIDEO_ANALYSIS_TIER]
|
|
404
|
+
/** Neutral, vendor-free display labels for the UI. */
|
|
405
|
+
export const VIDEO_ANALYSIS_TIER_LABELS: Record<VideoAnalysisTier, string> = { fast: "Fast", pro: "Pro" }
|
|
406
|
+
|
|
407
|
+
export function isVideoAnalysisTier(v: string): v is VideoAnalysisTier {
|
|
408
|
+
return Object.prototype.hasOwnProperty.call(VIDEO_ANALYSIS_TIERS, v)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Resolve a user-supplied tier (`"fast"`/`"pro"`) OR a raw internal model id to
|
|
413
|
+
* the internal analysis model id. Empty/unknown → the default tier's model.
|
|
414
|
+
* Real model ids pass through (back-compat for existing stored `llmModel`
|
|
415
|
+
* values); anything else falls back to the default rather than erroring.
|
|
416
|
+
*/
|
|
417
|
+
export function resolveVideoAnalysisModel(input?: string | null): string {
|
|
418
|
+
if (input && isVideoAnalysisTier(input)) return VIDEO_ANALYSIS_TIERS[input]
|
|
419
|
+
if (input && VIDEO_ANALYSIS_LLM_MODELS.includes(input)) return input
|
|
420
|
+
return DEFAULT_VIDEO_ANALYSIS_MODEL
|
|
421
|
+
}
|