()
@@ -82,7 +82,7 @@ defineEmits<{ click: [] }>()
diff --git a/apps/website/src/components/learning/HeroSection.vue b/apps/website/src/components/learning/HeroSection.vue
index 8b5655464e..007e8d5b96 100644
--- a/apps/website/src/components/learning/HeroSection.vue
+++ b/apps/website/src/components/learning/HeroSection.vue
@@ -11,7 +11,7 @@ const { locale = 'en' } = defineProps<{ locale?: Locale }>()
class="max-w-9xl mx-auto flex flex-col items-center px-6 pt-24 pb-12 text-center"
>
{{ t('learning.heroTitle.before', locale) }}
ComfyUI
+import type { Locale } from '../../i18n/translations'
+
+import FAQSplit01 from '../blocks/FAQSplit01.vue'
+import { pricingFaqs } from '../../data/pricingFaq'
+import { t } from '../../i18n/translations'
+
+const { locale = 'en' } = defineProps<{ locale?: Locale }>()
+
+const faqs = pricingFaqs.map((faq) => ({
+ id: faq.id,
+ question: faq.question[locale],
+ answer: faq.answer[locale]
+}))
+
+
+
+
+
diff --git a/apps/website/src/components/pricing/PriceSection.vue b/apps/website/src/components/pricing/PriceSection.vue
deleted file mode 100644
index 57d0f87f6e..0000000000
--- a/apps/website/src/components/pricing/PriceSection.vue
+++ /dev/null
@@ -1,393 +0,0 @@
-
-
-
-
-
-
-
- {{ t('pricing.title', locale) }}
-
-
- {{ t('pricing.subtitle', locale) }}
-
-
-
-
-
-
-
-
-
- {{ t(plan.labelKey, locale) }}
-
-
-
-
-
- {{ t('pricing.badge.popular', locale) }}
-
-
-
-
-
-
-
-
- {{ t(plan.summaryKey, locale) }}
-
-
-
-
-
- {{ t(plan.priceKey, locale) }}
-
-
- {{ t('pricing.plan.period', locale) }}
-
-
-
-
-
-
- {{ t(plan.creditsKey, locale) }}
-
-
-
-
-
- {{ t(plan.estimateKey, locale) }}
-
-
-
-
-
-
- {{ t(plan.featureIntroKey, locale) }}
-
-
-
- ✓
-
- {{ t(feature.text, locale) }}
-
-
-
-
-
-
-
- {{ t(plan.ctaKey, locale) }}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ t(plan.labelKey, locale) }}
-
-
-
-
-
- {{ t('pricing.badge.popular', locale) }}
-
-
-
-
-
-
-
-
- {{ t('pricing.enterprise.heading', locale) }}
-
-
-
-
- {{ t(plan.summaryKey, locale) }}
-
-
-
-
-
-
- {{ t(plan.priceKey, locale) }}
-
-
- {{ t('pricing.plan.period', locale) }}
-
-
-
-
- {{ t(plan.creditsKey, locale) }}
-
-
-
- {{ t(plan.estimateKey, locale) }}
-
-
-
-
-
-
- {{ t(plan.ctaKey, locale) }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ t(enterprisePlan.labelKey, locale) }}
-
-
- {{ t('pricing.enterprise.heading', locale) }}
-
-
- {{ t(enterprisePlan.summaryKey, locale) }}
-
-
-
- {{ t(enterprisePlan.ctaKey, locale) }}
-
-
-
-
-
-
- {{ t('pricing.footnote', locale) }}
-
-
-
diff --git a/apps/website/src/components/pricing/PricingCard.vue b/apps/website/src/components/pricing/PricingCard.vue
new file mode 100644
index 0000000000..1c8c762931
--- /dev/null
+++ b/apps/website/src/components/pricing/PricingCard.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/apps/website/src/components/pricing/PricingCredits.vue b/apps/website/src/components/pricing/PricingCredits.vue
new file mode 100644
index 0000000000..1c3fd33efe
--- /dev/null
+++ b/apps/website/src/components/pricing/PricingCredits.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+ {{ label }}
+
+
+
+ {{ estimate }}
+
+
+
diff --git a/apps/website/src/components/pricing/PricingEnterpriseBand.vue b/apps/website/src/components/pricing/PricingEnterpriseBand.vue
new file mode 100644
index 0000000000..43b1bd8b36
--- /dev/null
+++ b/apps/website/src/components/pricing/PricingEnterpriseBand.vue
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+ {{ t('pricing.enterprise.description', locale) }}
+
+
+
+ {{ t('pricing.enterprise.cta', locale) }}
+
+
+
+
diff --git a/apps/website/src/components/pricing/PricingPlanFeatureList.vue b/apps/website/src/components/pricing/PricingPlanFeatureList.vue
index b70bde26e9..4253928478 100644
--- a/apps/website/src/components/pricing/PricingPlanFeatureList.vue
+++ b/apps/website/src/components/pricing/PricingPlanFeatureList.vue
@@ -1,60 +1,81 @@
-
- {{ t(featureIntroKey, locale) }}
-
-
-
+
-
✓
-
- {{ t(feature.text, locale) }}
-
-
-
-
- {{ t(nextUpKey, locale) }}
-
-
- {{ t(andMoreKey, locale) }}
-
+
+ {{ t(group.titleKey, locale) }}
+
+
+
+
+
+
+
+ {{
+ feature.type === 'coming'
+ ? t('pricing.plan.feature.status.coming', locale)
+ : feature.included === false
+ ? t('pricing.plan.feature.status.notIncluded', locale)
+ : t('pricing.plan.feature.status.included', locale)
+ }}:
+
+
+ {{ t(feature.text, locale) }}
+
+
+
+
+
diff --git a/apps/website/src/components/pricing/PricingPlanLabel.vue b/apps/website/src/components/pricing/PricingPlanLabel.vue
new file mode 100644
index 0000000000..d89e0f5a59
--- /dev/null
+++ b/apps/website/src/components/pricing/PricingPlanLabel.vue
@@ -0,0 +1,23 @@
+
+
+
+
+ {{ label }}
+
+
diff --git a/apps/website/src/components/pricing/PricingPrice.vue b/apps/website/src/components/pricing/PricingPrice.vue
new file mode 100644
index 0000000000..b5b7ba6c4f
--- /dev/null
+++ b/apps/website/src/components/pricing/PricingPrice.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+ {{ price }}
+
+
+
+
+ {{ originalPrice }}
+
+
+ {{ period }}
+
+
+
+
+ {{ discount || ' ' }}
+
+
+
+
+ {{ billingNote }}
+
+
+
diff --git a/apps/website/src/components/pricing/PricingSection.vue b/apps/website/src/components/pricing/PricingSection.vue
new file mode 100644
index 0000000000..35e2b9c17e
--- /dev/null
+++ b/apps/website/src/components/pricing/PricingSection.vue
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
+ {{ t('pricing.title', locale) }}
+
+
+ {{ t('pricing.subtitle', locale) }}
+
+
+
+
+
+
+ {{ t('pricing.period.monthly', locale) }}
+
+
+ {{ t('pricing.period.yearly', locale) }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('pricing.badge.popular', locale) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t(plan.ctaKey, locale) }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('pricing.footnote', locale) }}
+
+
+
diff --git a/apps/website/src/components/pricing/PricingTeamCard.vue b/apps/website/src/components/pricing/PricingTeamCard.vue
new file mode 100644
index 0000000000..67f5e2f7bd
--- /dev/null
+++ b/apps/website/src/components/pricing/PricingTeamCard.vue
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
+
+
+ {{ t('pricing.team.description', locale) }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatTeamCreditsShort(tier.credits) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('pricing.plan.team.cta', locale) }}
+
+
+
+
+
+
diff --git a/apps/website/src/components/pricing/PricingTierCard.vue b/apps/website/src/components/pricing/PricingTierCard.vue
deleted file mode 100644
index d6ccde9143..0000000000
--- a/apps/website/src/components/pricing/PricingTierCard.vue
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
diff --git a/apps/website/src/components/pricing/WhatsIncludedSection.vue b/apps/website/src/components/pricing/WhatsIncludedSection.vue
index 90d4eb97c7..e34c0c9d74 100644
--- a/apps/website/src/components/pricing/WhatsIncludedSection.vue
+++ b/apps/website/src/components/pricing/WhatsIncludedSection.vue
@@ -1,5 +1,7 @@
+
+
+
+
+
+
diff --git a/apps/website/src/components/ui/accordion/AccordionContent.vue b/apps/website/src/components/ui/accordion/AccordionContent.vue
new file mode 100644
index 0000000000..f1e4050b97
--- /dev/null
+++ b/apps/website/src/components/ui/accordion/AccordionContent.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
diff --git a/apps/website/src/components/ui/accordion/AccordionItem.vue b/apps/website/src/components/ui/accordion/AccordionItem.vue
new file mode 100644
index 0000000000..59c65add47
--- /dev/null
+++ b/apps/website/src/components/ui/accordion/AccordionItem.vue
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
diff --git a/apps/website/src/components/ui/accordion/AccordionTrigger.vue b/apps/website/src/components/ui/accordion/AccordionTrigger.vue
new file mode 100644
index 0000000000..b8ae074dd2
--- /dev/null
+++ b/apps/website/src/components/ui/accordion/AccordionTrigger.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/website/src/components/ui/badge/index.ts b/apps/website/src/components/ui/badge/index.ts
index 9409498153..ed28e17adf 100644
--- a/apps/website/src/components/ui/badge/index.ts
+++ b/apps/website/src/components/ui/badge/index.ts
@@ -8,7 +8,9 @@ export const badgeVariants = cva({
default: 'bg-transparency-ink-t80',
subtle: 'bg-transparency-white-t4 text-primary-comfy-canvas',
accent:
- 'before:bg-primary-comfy-yellow relative isolate overflow-visible rounded-none bg-transparent px-2 py-0.5 text-[9px] font-bold tracking-wide text-primary-comfy-ink uppercase before:absolute before:inset-0 before:-z-10 before:-skew-x-12 before:rounded-sm'
+ 'before:bg-primary-comfy-yellow relative isolate overflow-visible rounded-none bg-transparent px-2 py-0.5 text-[9px] font-bold tracking-wide text-primary-comfy-ink uppercase before:absolute before:inset-0 before:-z-10 before:-skew-x-12 before:rounded-sm',
+ callout:
+ 'before:bg-primary-comfy-plum text-primary-warm-white relative isolate overflow-visible rounded-none bg-transparent px-2 py-0.5 text-[9px] font-bold tracking-tight uppercase before:absolute before:inset-0 before:-z-10 before:-skew-x-12 before:rounded-sm'
},
size: {
md: 'px-4 py-1 text-xs',
diff --git a/apps/website/src/components/ui/button/Button.vue b/apps/website/src/components/ui/button/Button.vue
index 52be21d0a5..f2bcfa54ef 100644
--- a/apps/website/src/components/ui/button/Button.vue
+++ b/apps/website/src/components/ui/button/Button.vue
@@ -1,6 +1,6 @@
@@ -32,9 +34,10 @@ const {
data-slot="button"
:data-variant="variant"
:data-size="size"
- :as
+ :as="as ?? (href != null && !disabled ? 'a' : 'button')"
:as-child
:disabled
+ :href="disabled ? undefined : href"
:class="cn(buttonVariants({ variant, size }), className)"
>
diff --git a/apps/website/src/components/ui/slider/Slider.vue b/apps/website/src/components/ui/slider/Slider.vue
new file mode 100644
index 0000000000..6d4a9b178b
--- /dev/null
+++ b/apps/website/src/components/ui/slider/Slider.vue
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/website/src/components/ui/toggle-group/ToggleGroup.vue b/apps/website/src/components/ui/toggle-group/ToggleGroup.vue
new file mode 100644
index 0000000000..8c79a87e40
--- /dev/null
+++ b/apps/website/src/components/ui/toggle-group/ToggleGroup.vue
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
diff --git a/apps/website/src/components/ui/toggle-group/ToggleGroupItem.vue b/apps/website/src/components/ui/toggle-group/ToggleGroupItem.vue
new file mode 100644
index 0000000000..72f8f9d527
--- /dev/null
+++ b/apps/website/src/components/ui/toggle-group/ToggleGroupItem.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
diff --git a/apps/website/src/components/ui/toggle/index.ts b/apps/website/src/components/ui/toggle/index.ts
new file mode 100644
index 0000000000..22a3fd50d7
--- /dev/null
+++ b/apps/website/src/components/ui/toggle/index.ts
@@ -0,0 +1,20 @@
+import { cva } from 'class-variance-authority'
+
+export const toggleVariants = cva(
+ "data-[state=on]:bg-primary-comfy-yellow focus-visible:border-primary-comfy-orange focus-visible:ring-primary-comfy-yellow aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:text-primary-warm-white inline-flex items-center justify-center gap-2 rounded-xl text-xs font-bold whitespace-nowrap uppercase transition-[color,box-shadow] duration-300 outline-none focus-visible:ring-3 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:text-primary-comfy-ink [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+ {
+ variants: {
+ variant: {
+ default:
+ 'bg-transparency-white-t4 text-primary-warm-gray hover:cursor-pointer'
+ },
+ size: {
+ default: 'h-9 min-w-20 px-4'
+ }
+ },
+ defaultVariants: {
+ variant: 'default',
+ size: 'default'
+ }
+ }
+)
diff --git a/apps/website/src/data/pricingFaq.ts b/apps/website/src/data/pricingFaq.ts
new file mode 100644
index 0000000000..af67b73519
--- /dev/null
+++ b/apps/website/src/data/pricingFaq.ts
@@ -0,0 +1,167 @@
+import type { LocalizedText } from '../i18n/translations'
+
+interface PricingFaq {
+ id: string
+ question: LocalizedText
+ answer: LocalizedText
+}
+
+export const pricingFaqs: readonly PricingFaq[] = [
+ {
+ id: 'how-does-pricing-work',
+ question: {
+ en: 'How does Comfy Cloud pricing actually work?',
+ 'zh-CN': 'Comfy Cloud 的定价究竟是如何运作的?'
+ },
+ answer: {
+ en: "Every plan includes a monthly pool of credits . Credits are spent on two things: active GPU time while a workflow is running, and Partner Nodes (proprietary models like Nano Banana Pro). You're never charged for idle time. Building or editing a workflow costs nothing. You only spend while a job is actually running.",
+ 'zh-CN':
+ '每个计划都包含每月的积分 池。积分用于两类消耗:工作流运行时的活跃 GPU 时间 ,以及合作伙伴节点 (如 Nano Banana Pro 等专有模型)。空闲时间不会计费。构建或编辑工作流完全免费。只有任务真正运行时才会扣费。'
+ }
+ },
+ {
+ id: 'what-is-a-credit-worth',
+ question: {
+ en: "What's a credit worth? How far does it go?",
+ 'zh-CN': '一个积分价值多少?能用多久?'
+ },
+ answer: {
+ en: 'Credits map to GPU runtime, so mileage depends on the workflow. As a reference point, a five-second video* uses roughly 11 credits , so Standard covers a few hundred per month, Creator about double that, and Pro enough for over a thousand.\n\n*Based on 5s videos using the Wan 2.2 Image-to-Video template at default settings (81 frames, 18fps, 640×640, 4-step sampler). Heavier models, higher resolutions, and the inclusion of Partner nodes use more.',
+ 'zh-CN':
+ '积分对应 GPU 运行时长,因此具体能用多少取决于工作流本身。作为参考:一段 5 秒视频*大约消耗 11 积分 ,因此 Standard 每月可支持数百段,Creator 约为其两倍,Pro 则足以生成一千多段。\n\n*基于使用 Wan 2.2 图生视频模板在默认设置(81 帧、18fps、640×640、4-step sampler)下生成 5 秒视频的估算。更复杂的模型、更高分辨率以及加入合作伙伴节点会消耗更多积分。'
+ }
+ },
+ {
+ id: 'run-out-of-credits',
+ question: {
+ en: 'What happens when I run out of credits?',
+ 'zh-CN': '积分用完了会怎样?'
+ },
+ answer: {
+ en: 'You can buy top-up credits at any time without changing plans. Monthly credits are spent first; top-ups are only drawn down once your monthly allowance is used up. Top-up credits stay valid for 1 year from purchase.',
+ 'zh-CN':
+ '您可以随时购买充值积分 ,无需更换计划。月度积分会优先消耗;只有当月度额度用完后,才会开始使用充值积分。充值积分自购买之日起 1 年 内有效。'
+ }
+ },
+ {
+ id: 'do-credits-roll-over',
+ question: {
+ en: 'Do unused credits roll over?',
+ 'zh-CN': '未使用的积分会顺延吗?'
+ },
+ answer: {
+ en: "Monthly plan credits reset each billing cycle and don't roll over. Top-up credits do persist. They're valid for a year and aren't affected by your monthly reset. Credits work on Comfy Cloud, and on Comfy Desktop only when calling Partner Nodes. Comfy Desktop itself is free.",
+ 'zh-CN':
+ '月度计划积分在每个计费周期重置,不会顺延。但充值积分会保留。 有效期为一年,且不受每月重置的影响。积分可在 Comfy Cloud 上使用,在 Comfy 桌面版上仅在调用合作伙伴节点时使用 。Comfy 桌面版本身免费。'
+ }
+ },
+ {
+ id: 'difference-between-plans',
+ question: {
+ en: "What's the difference between Standard, Creator, and Pro?",
+ 'zh-CN': 'Standard、Creator 和 Pro 有什么区别?'
+ },
+ answer: {
+ en: 'Standard - 30-min max runtime per workflow, 1 concurrent workflow via API. For individuals building workflows.\n\nCreator - Everything in Standard plus the ability to import your own models (from CivitAI or Hugging Face) and run up to 3 workflows concurrently via API.\n\nPro - Everything in Creator plus longer runtime (up to 1 hour) per workflow and up to 5 concurrent workflows via API. For teams running Comfy in production.',
+ 'zh-CN':
+ 'Standard - 单个工作流最长运行 30 分钟,API 支持 1 个并发工作流。适合构建工作流的个人。\n\nCreator - 包含 Standard 的全部功能,并新增导入自有模型 (来自 CivitAI 或 Hugging Face)的能力,API 支持最多 3 个并发工作流。\n\nPro - 包含 Creator 的全部功能,并提供更长的运行时长(最长 1 小时) ,API 支持最多 5 个并发工作流。适合在生产环境中运行 Comfy 的团队。'
+ }
+ },
+ {
+ id: 'how-does-team-plan-work',
+ question: {
+ en: 'How does the Team Plan work?',
+ 'zh-CN': '团队计划是如何运作的?'
+ },
+ answer: {
+ en: 'The Team Plan puts your whole team on one shared credit pool. Every member draws from the same balance, so you\'re not juggling separate subscriptions. Key things to know:\n\nOne pool, shared. Everyone generates against the same credit balance.\nInvite by email. Add teammates, and resend or revoke access any time.\nOwners manage billing. Assign owners who handle payment and buy top-ups for the team.\nUpgrade in place. Move an existing workspace to a team and your workflows, models, and assets stay attached.\n\nChoose your monthly credit commitment that fits your team. Get started today. ',
+ 'zh-CN':
+ '团队计划让整个团队共享一个积分池 。每位成员都从同一余额中扣费,无需分别管理多个订阅。要点:\n\n一池共享。 所有人都从同一积分余额中生成内容。\n邮箱邀请。 添加团队成员,随时重新发送或撤销访问权限。\n所有者管理账单。 指定所有者负责付款,并为团队购买充值积分。\n原地升级。 将现有工作区升级为团队工作区,您的工作流、模型和资产都将保留。\n\n选择适合您团队的每月积分承诺。立即开始。 '
+ }
+ },
+ {
+ id: 'team-spending-controls',
+ question: {
+ en: 'Can I control how my team spends credits?',
+ 'zh-CN': '我可以控制团队消耗积分的方式吗?'
+ },
+ answer: {
+ en: "Today, owners control the shared pool and top-ups. We're actively building finer-grained controls: spending limits at the user, project, and workspace level, per-project budgets and chargebacks , auto-recharge when the pool runs low, and self-serve teams beyond 50 seats .",
+ 'zh-CN':
+ '目前,所有者掌控共享积分池和充值。我们正在积极开发更细粒度的控制功能:用户、项目和工作区级别的消费上限 、按项目预算与分摊 、积分池余额不足时的自动充值 ,以及超过 50 个席位的自助式团队 。'
+ }
+ },
+ {
+ id: 'team-per-seat-pricing',
+ question: {
+ en: 'Is Team pricing per-seat? Can I add a freelancer just for a project?',
+ 'zh-CN':
+ '团队计划是按席位计费吗?我可以为某个项目临时加入一位自由职业者吗?'
+ },
+ answer: {
+ en: 'No. Team pricing is based on your monthly credit commit , not per-seat. Invite a freelancer, they draw from the shared credit pool while they\'re working, then remove them when the project wraps. No charge for adding or removing people. Member count is capped at 50 today; if you hit the cap, contact support for additional seats.',
+ 'zh-CN':
+ '不是。团队定价基于您每月承诺的积分量 ,而非按席位计费。邀请自由职业者后,他们在工作期间从共享积分池中扣费,项目结束后再将其移除即可。添加或移除成员都不收取额外费用。 目前成员数量上限为 50 人;如果您达到上限,请联系支持 以增加席位。'
+ }
+ },
+ {
+ id: 'team-upgrade-carryover',
+ question: {
+ en: 'What carries over when I upgrade my workspace to a Team plan?',
+ 'zh-CN': '将工作区升级为团队计划时,哪些内容会保留?'
+ },
+ answer: {
+ en: "Everything stays. You're upgrading the workspace itself, so workflows, models, run history, and top-up credits all remain attached . The only exception: unused monthly credits from your old plan expire at the end of your current billing cycle, since you're moving to a new credit allowance. Top-up credits carry over. ",
+ 'zh-CN':
+ '全部保留。您升级的是工作区本身,因此工作流、模型、运行历史和充值积分都会保留 。唯一例外:原计划中未使用的月度积分会在当前计费周期结束时失效,因为您将获得新的月度积分额度。充值积分会顺延。 '
+ }
+ },
+ {
+ id: 'team-tier-pricing',
+ question: {
+ en: 'What do the Team plan tiers cost, and how does the discount work?',
+ 'zh-CN': '团队计划各档次的价格是多少?折扣是怎么算的?'
+ },
+ answer: {
+ en: 'Team plans come in five tiers from $200 to $2,500/month , set by a credit-commit slider. A bigger monthly commit means a bigger discount: annual plans go up to 20% off, monthly plans up to 10% . The discount starts at the $400 tier (5% annual / 2.5% monthly) and scales from there.',
+ 'zh-CN':
+ '团队计划共有五个档次,从每月 $200 到 $2,500 ,通过积分承诺滑块进行调整。月度承诺越高,折扣越大:年付计划最高 20% 折扣,月付计划最高 10% 折扣 。折扣自 $400 档位起(年付 5% / 月付 2.5%),并由此递增。'
+ }
+ },
+ {
+ id: 'team-collaboration-features',
+ question: {
+ en: 'What collaboration features are included at launch?',
+ 'zh-CN': '首发时包含哪些协作功能?'
+ },
+ answer: {
+ en: 'At launch, a Team plan gives you shared infrastructure : one credit pool, one bill, one set of admins. Workflow and asset sharing inside the workspace is coming soon. In the meantime, to hand off, share the workflow, export the workflow JSON, or drop a Comfy-generated asset into another canvas.',
+ 'zh-CN':
+ '在首发阶段,团队计划为您提供共享基础设施 :一个积分池、一份账单、一组管理员。工作区内的工作流与资产共享功能即将上线。 在此之前,您可以通过共享工作流、导出工作流 JSON 或将 Comfy 生成的资产拖入另一画布来完成交接。'
+ }
+ },
+ {
+ id: 'team-concurrency',
+ question: {
+ en: 'How does concurrency work on a Team plan? Can multiple members run workflows at the same time?',
+ 'zh-CN': '团队计划的并发是如何运作的?多名成员可以同时运行工作流吗?'
+ },
+ answer: {
+ en: 'Yes. The workspace has 50 concurrent slots (matches the number of members), shared across the team . A ten-person team and a 50-person team both get the same 50 slots. If a few teammates saturate the pool, the rest queue up until slots free.',
+ 'zh-CN':
+ '可以。工作区拥有 50 个并发槽位(与成员上限一致),由整个团队共享 。无论是 10 人团队还是 50 人团队,都享有相同的 50 个槽位。如果少数成员占满了槽位,其他人会排队等待,直到有槽位空出。'
+ }
+ },
+ {
+ id: 'runtime-and-concurrency-limits',
+ question: {
+ en: 'What are the runtime and concurrency limits?',
+ 'zh-CN': '运行时长和并发的限制是什么?'
+ },
+ answer: {
+ en: 'Each workflow has a max runtime of 30 minutes on Standard and Creator, raised to 1 hour on Pro. Jobs over the limit are cancelled automatically to keep the system fair and stable. You can queue up to 100 workflows at once, and run 1 / 3 / 5 concurrently via API on Standard / Creator / Pro. On Team plan, the limit is raised to 50, matching the number of members on the team. Need higher API rate limits? Contact enterprise@comfy.org .',
+ 'zh-CN':
+ 'Standard 和 Creator 上,单个工作流的最长运行时长为 30 分钟 ;Pro 上提升至 1 小时 。超出限制的任务会被自动取消,以保持系统的公平与稳定。您可以同时排队最多 100 个工作流 ,并在 Standard / Creator / Pro 上通过 API 分别并发运行 1 / 3 / 5 个工作流。在团队计划中,此上限提升至 50,与团队成员上限一致。需要更高的 API 速率限制?请联系 enterprise@comfy.org 。'
+ }
+ }
+] as const
diff --git a/apps/website/src/data/pricingPlans.ts b/apps/website/src/data/pricingPlans.ts
new file mode 100644
index 0000000000..cc6fb43751
--- /dev/null
+++ b/apps/website/src/data/pricingPlans.ts
@@ -0,0 +1,108 @@
+import type { TranslationKey } from '../i18n/translations'
+
+import { SHOW_FREE_TIER } from '../config/features'
+import { externalLinks } from '../config/routes'
+
+export type BillingCycle = 'monthly' | 'yearly'
+
+interface PlanFeature {
+ text: TranslationKey
+ included?: boolean
+}
+
+export interface PricingPlan {
+ id: string
+ labelKey: TranslationKey
+ priceKey?: TranslationKey
+ yearlyPriceKey?: TranslationKey
+ yearlyTotalKey?: TranslationKey
+ creditsKey?: TranslationKey
+ estimateKey?: TranslationKey
+ ctaKey: TranslationKey
+ ctaHref: (cycle: BillingCycle) => string
+ features: PlanFeature[]
+ isPopular?: boolean
+}
+
+export const subscribeUrl = (
+ tier: string,
+ cycle: BillingCycle,
+ stop?: string
+): string => {
+ const params = new URLSearchParams({ tier, cycle })
+ if (stop) params.set('stop', stop)
+ return `${externalLinks.cloud}/cloud/subscribe?${params.toString()}`
+}
+
+const freePlan: PricingPlan = {
+ id: 'free',
+ labelKey: 'pricing.plan.free.label',
+ priceKey: 'pricing.plan.free.price',
+ creditsKey: 'pricing.plan.free.credits',
+ estimateKey: 'pricing.plan.free.estimate',
+ ctaKey: 'pricing.plan.free.cta',
+ ctaHref: () => externalLinks.cloud,
+ features: [
+ { text: 'pricing.plan.free.feature1' },
+ { text: 'pricing.plan.free.feature2' }
+ ]
+}
+
+const standardPricingPlans: PricingPlan[] = [
+ {
+ id: 'standard',
+ labelKey: 'pricing.plan.standard.label',
+ priceKey: 'pricing.plan.standard.price',
+ yearlyPriceKey: 'pricing.plan.standard.yearlyPrice',
+ yearlyTotalKey: 'pricing.plan.standard.yearlyTotal',
+ creditsKey: 'pricing.plan.standard.credits',
+ estimateKey: 'pricing.plan.standard.estimate',
+ ctaKey: 'pricing.plan.standard.cta',
+ ctaHref: (cycle) => subscribeUrl('standard', cycle),
+ features: [
+ { text: 'pricing.feature.shortRuntime' },
+ { text: 'pricing.feature.addCredits' },
+ { text: 'pricing.feature.importModels', included: false },
+ { text: 'pricing.feature.longRuntime', included: false }
+ ]
+ },
+ {
+ id: 'creator',
+ labelKey: 'pricing.plan.creator.label',
+ priceKey: 'pricing.plan.creator.price',
+ yearlyPriceKey: 'pricing.plan.creator.yearlyPrice',
+ yearlyTotalKey: 'pricing.plan.creator.yearlyTotal',
+ creditsKey: 'pricing.plan.creator.credits',
+ estimateKey: 'pricing.plan.creator.estimate',
+ ctaKey: 'pricing.plan.creator.cta',
+ ctaHref: (cycle) => subscribeUrl('creator', cycle),
+ features: [
+ { text: 'pricing.feature.shortRuntime' },
+ { text: 'pricing.feature.addCredits' },
+ { text: 'pricing.feature.importModels' },
+ { text: 'pricing.feature.longRuntime', included: false }
+ ],
+ isPopular: true
+ },
+ {
+ id: 'pro',
+ labelKey: 'pricing.plan.pro.label',
+ priceKey: 'pricing.plan.pro.price',
+ yearlyPriceKey: 'pricing.plan.pro.yearlyPrice',
+ yearlyTotalKey: 'pricing.plan.pro.yearlyTotal',
+ creditsKey: 'pricing.plan.pro.credits',
+ estimateKey: 'pricing.plan.pro.estimate',
+ ctaKey: 'pricing.plan.pro.cta',
+ ctaHref: (cycle) => subscribeUrl('pro', cycle),
+ features: [
+ { text: 'pricing.feature.shortRuntime' },
+ { text: 'pricing.feature.addCredits' },
+ { text: 'pricing.feature.importModels' },
+ { text: 'pricing.feature.longRuntime' }
+ ]
+ }
+]
+
+export const pricingPlans: PricingPlan[] = SHOW_FREE_TIER
+ ? [freePlan, ...standardPricingPlans]
+ : standardPricingPlans
diff --git a/apps/website/src/data/teamCreditTiers.ts b/apps/website/src/data/teamCreditTiers.ts
new file mode 100644
index 0000000000..fb32da3263
--- /dev/null
+++ b/apps/website/src/data/teamCreditTiers.ts
@@ -0,0 +1,54 @@
+export interface TeamCreditTier {
+ credits: number
+ basePrice: number
+ monthlyPrice: number
+ yearlyPrice: number
+ videos: number
+}
+
+export const teamCreditTiers: readonly TeamCreditTier[] = [
+ {
+ credits: 42200,
+ basePrice: 200,
+ monthlyPrice: 200,
+ yearlyPrice: 200,
+ videos: 3830
+ },
+ {
+ credits: 84400,
+ basePrice: 400,
+ monthlyPrice: 390,
+ yearlyPrice: 380,
+ videos: 7660
+ },
+ {
+ credits: 147700,
+ basePrice: 700,
+ monthlyPrice: 665,
+ yearlyPrice: 630,
+ videos: 13405
+ },
+ {
+ credits: 295400,
+ basePrice: 1400,
+ monthlyPrice: 1295,
+ yearlyPrice: 1190,
+ videos: 26810
+ },
+ {
+ credits: 527500,
+ basePrice: 2500,
+ monthlyPrice: 2250,
+ yearlyPrice: 2000,
+ videos: 47830
+ }
+] as const
+
+export function formatTeamCreditsLong(n: number): string {
+ return n.toLocaleString('en-US')
+}
+
+export function formatTeamCreditsShort(n: number): string {
+ const k = n / 1000
+ return k % 1 === 0 ? `${k}K` : `${k.toFixed(1)}K`
+}
diff --git a/apps/website/src/i18n/translations.ts b/apps/website/src/i18n/translations.ts
index fd0b4634b9..4724a8e121 100644
--- a/apps/website/src/i18n/translations.ts
+++ b/apps/website/src/i18n/translations.ts
@@ -1189,27 +1189,72 @@ const translations = {
'buildWhat.row2a': { en: "DOESN'T EXIST", 'zh-CN': '尚不存在的' },
'buildWhat.row2b': { en: 'YET', 'zh-CN': '事物' },
- // PriceSection
- 'pricing.title': { en: 'Pricing', 'zh-CN': '价格' },
+ // PricingSection
+ 'pricing.title': { en: 'Choose a plan', 'zh-CN': '价格' },
'pricing.subtitle': {
en: 'Access cloud-powered ComfyUI workflows with straightforward, usage-based pricing.',
'zh-CN': '通过简单透明、按使用量计费的方式,访问云端 ComfyUI 工作流。'
},
'pricing.badge.popular': { en: 'MOST POPULAR', 'zh-CN': '最受欢迎' },
+ 'pricing.period.monthly': { en: 'Monthly', 'zh-CN': '按月' },
+ 'pricing.period.yearly': {
+ en: 'Yearly (Up to 20% off)',
+ 'zh-CN': '按年(最高 20% 优惠)'
+ },
+ 'pricing.period.billedMonthly': { en: 'Billed monthly', 'zh-CN': '按月计费' },
+ 'pricing.period.billedYearly': {
+ en: '{total} billed yearly',
+ 'zh-CN': '按年计费 {total}'
+ },
+ 'pricing.savePercent': {
+ en: 'Save {pct}% ({amount})',
+ 'zh-CN': '节省 {pct}%({amount})'
+ },
+ 'pricing.team.videosEstimate': {
+ en: 'Generates ~{count} 5s videos*',
+ 'zh-CN': '约可生成 {count} 个 5 秒视频*'
+ },
'pricing.plan.period': { en: '/month', 'zh-CN': '/月' },
+ 'pricing.creditsLabel': { en: 'monthly credits', 'zh-CN': '每月积分' },
+
+ 'pricing.feature.shortRuntime': {
+ en: '30 minute max workflow runtime',
+ 'zh-CN': '单个工作流最长运行 30 分钟'
+ },
+ 'pricing.feature.addCredits': {
+ en: 'Add more credits anytime',
+ 'zh-CN': '可随时增加积分'
+ },
+ 'pricing.feature.importModels': {
+ en: 'Import your own models',
+ 'zh-CN': '导入你自己的模型'
+ },
+ 'pricing.feature.longRuntime': {
+ en: 'Longer workflow runtime (up to 1 hr)',
+ 'zh-CN': '更长工作流运行时长(最长 1 小时)'
+ },
+ 'pricing.feature.inviteMembers': {
+ en: 'Invite members',
+ 'zh-CN': '邀请成员'
+ },
+ 'pricing.feature.concurrentWorkflows': {
+ en: 'Members can run workflows concurrently',
+ 'zh-CN': '成员可并行运行工作流'
+ },
+ 'pricing.feature.sharedCreditPool': {
+ en: 'Shared credit pool for all members',
+ 'zh-CN': '所有成员共享积分池'
+ },
+ 'pricing.feature.roleBasedPermissions': {
+ en: 'Role-based permissions',
+ 'zh-CN': '基于角色的权限'
+ },
'pricing.plan.free.label': { en: 'FREE', 'zh-CN': '免费版' },
- 'pricing.plan.free.summary': {
- en: "Explore Comfy's possibilities",
- 'zh-CN': '探索 Comfy 的可能性'
- },
'pricing.plan.free.price': { en: '$0', 'zh-CN': '$0' },
- 'pricing.plan.free.credits': {
- en: 'Includes 400 monthly credits',
- 'zh-CN': '每月包含 400 积分'
- },
+ 'pricing.plan.free.credits': { en: '400', 'zh-CN': '400' },
'pricing.plan.free.estimate': {
- en: '~35 5s videos*',
+ en: 'Generates ~35 5s videos*',
'zh-CN': '约可生成 35 个 5 秒视频*'
},
'pricing.plan.free.cta': { en: 'START FREE', 'zh-CN': '免费开始' },
@@ -1223,113 +1268,88 @@ const translations = {
},
'pricing.plan.standard.label': { en: 'STANDARD', 'zh-CN': '标准版' },
- 'pricing.plan.standard.summary': {
- en: 'For individuals creating workflows',
- 'zh-CN': '面向个人工作流创作者'
- },
'pricing.plan.standard.price': { en: '$20', 'zh-CN': '$20' },
- 'pricing.plan.standard.credits': {
- en: 'Includes 4,200 monthly credits with top-ups available',
- 'zh-CN': '每月包含 4,200 积分,并支持充值'
- },
+ 'pricing.plan.standard.yearlyPrice': { en: '$16', 'zh-CN': '$16' },
+ 'pricing.plan.standard.yearlyTotal': { en: '$192', 'zh-CN': '$192' },
+ 'pricing.plan.standard.credits': { en: '4,200', 'zh-CN': '4,200' },
'pricing.plan.standard.estimate': {
- en: '~380 5s videos*',
+ en: 'Generates ~380 5s videos*',
'zh-CN': '约可生成 380 个 5 秒视频*'
},
'pricing.plan.standard.cta': {
en: 'SUBSCRIBE TO STANDARD',
'zh-CN': '订阅标准版'
},
- 'pricing.plan.standard.featureIntro': {
- en: 'Everything in Free, plus:',
- 'zh-CN': '包含免费版全部能力,另加:'
- },
- 'pricing.plan.standard.feature1': {
- en: '30-minute max runtime per workflow',
- 'zh-CN': '单个工作流最长运行 30 分钟'
- },
- 'pricing.plan.standard.feature2': {
- en: 'Add more credits anytime',
- 'zh-CN': '可随时增加积分'
- },
- 'pricing.plan.standard.feature3': {
- en: 'Run 1 workflow concurrently (via API)',
- 'zh-CN': '通过 API 并发运行 1 个工作流'
- },
'pricing.plan.creator.label': { en: 'CREATOR', 'zh-CN': '创作者版' },
- 'pricing.plan.creator.summary': {
- en: 'Small teams building fine-tuned, repeatable workflows',
- 'zh-CN': '小团队构建精细调优、可复用的工作流'
- },
'pricing.plan.creator.price': { en: '$35', 'zh-CN': '$35' },
- 'pricing.plan.creator.credits': {
- en: 'Includes 7,400 monthly credits with top-ups available',
- 'zh-CN': '每月包含 7,400 积分,并支持充值'
- },
+ 'pricing.plan.creator.yearlyPrice': { en: '$28', 'zh-CN': '$28' },
+ 'pricing.plan.creator.yearlyTotal': { en: '$336', 'zh-CN': '$336' },
+ 'pricing.plan.creator.credits': { en: '7,400', 'zh-CN': '7,400' },
'pricing.plan.creator.estimate': {
- en: '~670 5s videos*',
+ en: 'Generates ~670 5s videos*',
'zh-CN': '约可生成 670 个 5 秒视频*'
},
'pricing.plan.creator.cta': {
en: 'SUBSCRIBE TO CREATOR',
'zh-CN': '订阅创作者版'
},
- 'pricing.plan.creator.featureIntro': {
- en: 'Everything in Standard, plus:',
- 'zh-CN': '包含标准版全部能力,另加:'
- },
- 'pricing.plan.creator.feature1': {
- en: 'Import your own LoRAs',
- 'zh-CN': '导入你自己的 LoRA'
- },
- 'pricing.plan.creator.feature2': {
- en: 'Run up to 3 workflows concurrently (via API)',
- 'zh-CN': '通过 API 最多并发运行 3 个工作流'
- },
'pricing.plan.pro.label': { en: 'PRO', 'zh-CN': '专业版' },
- 'pricing.plan.pro.summary': {
- en: 'For growing teams running Comfy in production',
- 'zh-CN': '面向在生产环境使用 Comfy 的成长型团队'
- },
'pricing.plan.pro.price': { en: '$100', 'zh-CN': '$100' },
- 'pricing.plan.pro.credits': {
- en: 'Includes 21,100 monthly credits with top-ups available',
- 'zh-CN': '每月包含 21,100 积分,并支持充值'
- },
+ 'pricing.plan.pro.yearlyPrice': { en: '$80', 'zh-CN': '$80' },
+ 'pricing.plan.pro.yearlyTotal': { en: '$960', 'zh-CN': '$960' },
+ 'pricing.plan.pro.credits': { en: '21,100', 'zh-CN': '21,100' },
'pricing.plan.pro.estimate': {
- en: '~1,915 5s videos*',
+ en: 'Generates ~1,915 5s videos*',
'zh-CN': '约可生成 1,915 个 5 秒视频*'
},
'pricing.plan.pro.cta': { en: 'SUBSCRIBE TO PRO', 'zh-CN': '订阅专业版' },
- 'pricing.plan.pro.featureIntro': {
- en: 'Everything in Creator, plus:',
- 'zh-CN': '包含创作者版全部能力,另加:'
+
+ 'pricing.plan.team.label': { en: 'TEAM', 'zh-CN': '团队版' },
+ 'pricing.plan.team.cta': {
+ en: 'SUBSCRIBE TO TEAM',
+ 'zh-CN': '订阅团队版'
},
- 'pricing.plan.pro.feature1': {
- en: 'Longer workflow runtime (up to 1 hour)',
- 'zh-CN': '更长工作流运行时长(最长 1 小时)'
+ 'pricing.plan.team.everythingInProPlus': {
+ en: 'Everything in Pro, plus:',
+ 'zh-CN': '包含专业版的全部功能,另加:'
},
- 'pricing.plan.pro.feature2': {
- en: 'Run up to 5 workflows concurrently (via API)',
- 'zh-CN': '通过 API 最多并发运行 5 个工作流'
+ 'pricing.team.description': {
+ en: 'Built for teams collaborating on workflows together.',
+ 'zh-CN': '为协作开发工作流的团队打造。'
+ },
+ 'pricing.plan.team.comingSoon': {
+ en: 'Coming soon...',
+ 'zh-CN': '即将推出…'
+ },
+ 'pricing.plan.team.sharedWorkflowsAndAssets': {
+ en: 'Shared workflows & assets',
+ 'zh-CN': '共享工作流与资产'
+ },
+ 'pricing.plan.team.projects': {
+ en: 'Projects',
+ 'zh-CN': '项目'
+ },
+ 'pricing.plan.feature.status.coming': {
+ en: 'Coming soon',
+ 'zh-CN': '即将推出'
+ },
+ 'pricing.plan.feature.status.included': {
+ en: 'Included',
+ 'zh-CN': '已包含'
+ },
+ 'pricing.plan.feature.status.notIncluded': {
+ en: 'Not included',
+ 'zh-CN': '未包含'
},
'pricing.enterprise.label': { en: 'ENTERPRISE', 'zh-CN': '企业版' },
- 'pricing.enterprise.heading': {
- en: 'Looking for Enterprise Solutions?',
- 'zh-CN': '在寻找企业级解决方案?'
- },
'pricing.enterprise.description': {
- en: 'For teams running Comfy in production, and at scale.',
- 'zh-CN': '面向在生产环境和规模化场景中运行 Comfy 的团队。'
- },
- 'pricing.enterprise.cta': { en: 'LEARN MORE', 'zh-CN': '了解更多' },
- 'pricing.enterprise.featureIntro': {
- en: 'Everything in Pro, plus:',
- 'zh-CN': '包含专业版全部能力,另加:'
+ en: 'Need more members? Looking for more flexibility or custom features?',
+ 'zh-CN': '需要更多成员?想要更多灵活性或定制功能?'
},
+ 'pricing.enterprise.cta': { en: 'Contact Us', 'zh-CN': '联系我们' },
'pricing.enterprise.feature1': {
en: 'Annual commitments with bulk pricing and custom compute packages',
'zh-CN': '支持年度承诺、批量定价与定制算力套餐'
@@ -1357,6 +1377,10 @@ const translations = {
en: "What's included\nin the Comfy plan",
'zh-CN': 'Comfy 计划\n包含哪些内容'
},
+ 'pricing.included.comingSoon': {
+ en: '(coming soon)',
+ 'zh-CN': '(即将推出)'
+ },
'pricing.included.feature1.title': {
en: 'Machine Setup',
'zh-CN': '机器配置'
@@ -1370,9 +1394,9 @@ const translations = {
'zh-CN': '单个任务时限'
},
'pricing.included.feature2.description': {
- en: 'On our Standard and Creator plans, each workflow has a maximum run time of 30 minutes. On the Pro plan, the limit is increased to 1 hour. Jobs exceeding that limit are automatically cancelled to ensure fair usage and system stability.',
+ en: 'Each workflow run has a maximum duration of 30 minutes. On the Pro plan, the time limit is increased to 1 hour. Jobs exceeding that limit are automatically cancelled to ensure fair usage and system stability.',
'zh-CN':
- 'Standard 和 Creator 计划下,每个工作流最长运行时间为 30 分钟。Pro 计划的时限可延长至 1 小时。超时任务将自动取消,以确保公平使用和系统稳定。'
+ '每个工作流运行的最长时长为 30 分钟。Pro 计划的时限可延长至 1 小时。超时任务将自动取消,以确保公平使用和系统稳定。'
},
'pricing.included.feature3.title': {
en: 'Usage',
@@ -1397,9 +1421,9 @@ const translations = {
'zh-CN': '随时加购积分'
},
'pricing.included.feature5.description': {
- en: 'Purchase additional credits at any time. Top-up credits are valid for 1 year from the date of purchase and do not roll over with your monthly plan.',
+ en: 'Purchase additional credits at any time. Unused top-ups roll over to the next month automatically for up to 1 year.',
'zh-CN':
- '可随时购买额外积分。充值积分自购买之日起 1 年内有效,且不会随月度计划结转。'
+ '可随时购买额外积分。未使用的充值积分将自动顺延至下个月,最长可保留 1 年。'
},
'pricing.included.feature6.title': {
en: 'Pre-installed models',
@@ -1416,16 +1440,16 @@ const translations = {
'pricing.included.feature7.description': {
en: "Comfy Cloud currently supports a variety of the most-used custom nodes from the ComfyUI community. We're expanding support regularly based on demand and compatibility.",
'zh-CN':
- 'Comfy Cloud 目前支持 ComfyUI 社区中最常用的多种自定义节点,并根据需求和兼容性持续扩展支持范围。'
+ 'Comfy Cloud 目前支持 ComfyUI 社区中最常用的多种自定义节点。我们会根据需求和兼容性持续扩展支持范围。'
},
'pricing.included.feature8.title': {
en: 'Partner Nodes',
'zh-CN': '合作伙伴节点'
},
'pricing.included.feature8.description': {
- en: 'Run proprietary models through Comfy\'s Partner Nodes , such as Nano Banana. The amount of credits each node uses depends on the model and parameters you set in the node, but these credits are the same ones that your monthly subscription comes with. These credits can also be used across Comfy Cloud and Comfy Desktop. Read more about Partner nodes here .',
+ en: 'Run proprietary models through Comfy\'s Partner Nodes , such as Nano Banana. The amount of credits each node uses depends on the model and parameters you set in the node, but these credits are the same ones that your monthly subscription comes with. These credits can also be used across Comfy Cloud and local ComfyUI . Read more about Partner nodes here .',
'zh-CN':
- '通过 Comfy 的合作伙伴节点 运行专有模型 ,如 Nano Banana。每个节点消耗的积分取决于所用模型和参数设置,且与月度订阅积分通用。积分可在 Comfy Cloud 和 Comfy 桌面版间通用。了解更多关于合作伙伴节点的信息请点击此处 。'
+ '通过 Comfy 的合作伙伴节点 运行专有模型 ,如 Nano Banana。每个节点消耗的积分取决于所用模型和参数设置,且与月度订阅积分通用。积分可在 Comfy Cloud 和本地 ComfyUI 间通用。了解更多关于合作伙伴节点的信息请点击此处 。'
},
'pricing.included.feature9.title': {
en: 'Job queue',
@@ -1445,23 +1469,19 @@ const translations = {
'Creator 或 Pro 计划用户可从 CivitAI 或 Huggingface 导入自己的模型和 LoRA,打造专属风格。'
},
'pricing.included.feature11.title': {
- en: 'Run Workflows via API',
- 'zh-CN': '通过 API 运行工作流'
- },
- 'pricing.included.feature11.description': {
- en: 'Run Comfy workflows programmatically via API, with concurrency limits based on your plan. Perfect for integrating ComfyUI into your applications, automating batch processing, or building production pipelines. For higher rate limits, reach out to enterprise@comfy.org .',
- 'zh-CN':
- '通过 API 以编程方式运行 Comfy 工作流,并发上限由您的计划决定。非常适合将 ComfyUI 集成到您的应用、自动化批量处理或构建生产级流水线。如需更高的速率限制,请联系 enterprise@comfy.org 。'
- },
- 'pricing.included.feature12.title': {
en: 'Parallel job execution',
'zh-CN': '并行任务执行'
},
- 'pricing.included.feature12.description': {
+ 'pricing.included.feature11.description': {
en: 'Run multiple workflows in parallel to speed up your pipeline.',
'zh-CN': '并行运行多个工作流,加速你的流程。'
},
+ 'pricing.faq.heading': {
+ en: 'Q&A',
+ 'zh-CN': '问答'
+ },
+
// VideoPlayer
'player.play': { en: 'Play', 'zh-CN': '播放' },
'player.pause': { en: 'Pause', 'zh-CN': '暂停' },
diff --git a/apps/website/src/pages/cloud/pricing.astro b/apps/website/src/pages/cloud/pricing.astro
index babec9f655..b05b6c3e22 100644
--- a/apps/website/src/pages/cloud/pricing.astro
+++ b/apps/website/src/pages/cloud/pricing.astro
@@ -1,10 +1,12 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro'
-import PriceSection from '../../components/pricing/PriceSection.vue'
+import PricingSection from '../../components/pricing/PricingSection.vue'
import WhatsIncludedSection from '../../components/pricing/WhatsIncludedSection.vue'
+import FAQSection from '../../components/pricing/FAQSection.vue'
---
-
+
+
diff --git a/apps/website/src/pages/zh-CN/cloud/pricing.astro b/apps/website/src/pages/zh-CN/cloud/pricing.astro
index 9f17d85a12..0feb9f6014 100644
--- a/apps/website/src/pages/zh-CN/cloud/pricing.astro
+++ b/apps/website/src/pages/zh-CN/cloud/pricing.astro
@@ -1,10 +1,12 @@
---
import BaseLayout from '../../../layouts/BaseLayout.astro'
-import PriceSection from '../../../components/pricing/PriceSection.vue'
+import PricingSection from '../../../components/pricing/PricingSection.vue'
import WhatsIncludedSection from '../../../components/pricing/WhatsIncludedSection.vue'
+import FAQSection from '../../../components/pricing/FAQSection.vue'
---
-
+
+
diff --git a/apps/website/src/styles/global.css b/apps/website/src/styles/global.css
index 6d2e2e044a..caa184c653 100644
--- a/apps/website/src/styles/global.css
+++ b/apps/website/src/styles/global.css
@@ -62,6 +62,7 @@
@theme {
--color-site-dropdown: #332b38;
--color-primary-comfy-yellow: #f2ff59;
+ --color-primary-comfy-orange: #fabc25;
--color-primary-comfy-ink: #211927;
--color-primary-comfy-ink-light: #2a2330;
--color-primary-comfy-canvas: #c2bfb9;
diff --git a/browser_tests/fixtures/ComfyPage.ts b/browser_tests/fixtures/ComfyPage.ts
index fc767db226..84934c1999 100644
--- a/browser_tests/fixtures/ComfyPage.ts
+++ b/browser_tests/fixtures/ComfyPage.ts
@@ -56,12 +56,16 @@ class ComfyPropertiesPanel {
readonly panelTitle: Locator
readonly searchBox: Locator
readonly titleEditor: TitleEditor
+ readonly toggleButton: Locator
constructor(readonly page: Page) {
this.root = page.getByTestId(TestIds.propertiesPanel.root)
this.panelTitle = this.root.locator('h3')
this.searchBox = this.root.getByPlaceholder(/^Search/)
this.titleEditor = new TitleEditor(this.root)
+ this.toggleButton = page.getByRole('button', {
+ name: 'Toggle properties panel'
+ })
}
}
diff --git a/browser_tests/fixtures/components/SidebarTab.ts b/browser_tests/fixtures/components/SidebarTab.ts
index a2c6a15a31..27ecc485c7 100644
--- a/browser_tests/fixtures/components/SidebarTab.ts
+++ b/browser_tests/fixtures/components/SidebarTab.ts
@@ -352,20 +352,11 @@ export class AssetsSidebarTab extends SidebarTab {
this.listViewItems = page.locator(
'.sidebar-content-container [role="button"][tabindex="0"]'
)
- this.selectionFooter = page
- .locator('.sidebar-content-container')
- .locator('..')
- .locator('[class*="h-18"]')
- this.selectionCountButton = page.getByText(/Assets Selected: \d+/)
- this.deselectAllButton = page.getByText('Deselect all')
- this.deleteSelectedButton = page
- .getByTestId('assets-delete-selected')
- .or(page.locator('button:has(.icon-\\[lucide--trash-2\\])').last())
- .first()
- this.downloadSelectedButton = page
- .getByTestId('assets-download-selected')
- .or(page.locator('button:has(.icon-\\[lucide--download\\])').last())
- .first()
+ this.selectionFooter = page.getByTestId('assets-selection-bar')
+ this.selectionCountButton = page.getByText(/\d+ selected/)
+ this.deselectAllButton = page.getByTestId('assets-deselect-selected')
+ this.deleteSelectedButton = page.getByTestId('assets-delete-selected')
+ this.downloadSelectedButton = page.getByTestId('assets-download-selected')
this.backToAssetsButton = page.getByText('Back to all assets')
this.skeletonLoaders = page.locator(
'.sidebar-content-container .animate-pulse'
diff --git a/browser_tests/fixtures/selectors.ts b/browser_tests/fixtures/selectors.ts
index 81a7a9a839..34beb002e2 100644
--- a/browser_tests/fixtures/selectors.ts
+++ b/browser_tests/fixtures/selectors.ts
@@ -112,6 +112,10 @@ export const TestIds = {
root: 'properties-panel',
errorsTab: 'panel-tab-errors'
},
+ assets: {
+ browserModal: 'asset-browser-modal',
+ card: 'asset-card'
+ },
subgraphEditor: {
hiddenSection: 'subgraph-editor-hidden-section',
iconEye: 'icon-eye',
diff --git a/browser_tests/tests/bottomPanelShortcuts.spec.ts b/browser_tests/tests/bottomPanelShortcuts.spec.ts
index 767e4b87c2..d4df7cae8b 100644
--- a/browser_tests/tests/bottomPanelShortcuts.spec.ts
+++ b/browser_tests/tests/bottomPanelShortcuts.spec.ts
@@ -223,4 +223,23 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
await expect(comfyPage.settingDialog.root).toBeVisible()
await expect(comfyPage.settingDialog.category('Keybinding')).toBeVisible()
})
+
+ test('should focus keybindings search when opening manage shortcuts', async ({
+ comfyPage
+ }) => {
+ const { bottomPanel } = comfyPage
+
+ await bottomPanel.keyboardShortcutsButton.click()
+ await bottomPanel.shortcuts.manageButton.click()
+
+ await expect(comfyPage.settingDialog.root).toBeVisible()
+ await expect(comfyPage.settingDialog.category('Keybinding')).toBeVisible()
+
+ await expect(
+ comfyPage.page.getByPlaceholder('Search Keybindings...')
+ ).toBeFocused()
+ await expect(
+ comfyPage.page.getByPlaceholder('Search Settings...')
+ ).not.toBeFocused()
+ })
})
diff --git a/browser_tests/tests/browseModelAssets.spec.ts b/browser_tests/tests/browseModelAssets.spec.ts
new file mode 100644
index 0000000000..cd313297fc
--- /dev/null
+++ b/browser_tests/tests/browseModelAssets.spec.ts
@@ -0,0 +1,61 @@
+import { expect } from '@playwright/test'
+
+import type { Asset } from '@comfyorg/ingest-types'
+import { createCloudAssetsFixture } from '@e2e/fixtures/assetApiFixture'
+import { STABLE_CHECKPOINT } from '@e2e/fixtures/data/assetFixtures'
+
+const CLOUD_ASSETS: Asset[] = [STABLE_CHECKPOINT]
+
+const test = createCloudAssetsFixture(CLOUD_ASSETS)
+
+test.describe('Browse Model Assets - Use button', { tag: '@cloud' }, () => {
+ test.beforeEach(async ({ comfyPage }) => {
+ await comfyPage.settings.setSetting('Comfy.Assets.UseAssetAPI', true)
+ await comfyPage.nodeOps.clearGraph()
+ })
+
+ test.afterEach(async ({ comfyPage }) => {
+ await comfyPage.nodeOps.clearGraph()
+ })
+
+ test('Use button ghost-places a loader populated with the model', async ({
+ comfyPage
+ }) => {
+ await comfyPage.command.executeCommand('Comfy.BrowseModelAssets')
+
+ const modal = comfyPage.page.locator(
+ '[data-component-id="AssetBrowserModal"]'
+ )
+ await expect(modal).toBeVisible()
+
+ const card = comfyPage.page.locator(
+ `[data-component-id="AssetCard"][data-asset-id="${STABLE_CHECKPOINT.id}"]`
+ )
+ await expect(card).toBeVisible()
+ await card.getByRole('button', { name: 'Use' }).click()
+
+ // Dialog closes and the ghost is armed; the node is not placed until the
+ // user clicks the canvas.
+ await expect(modal).toBeHidden()
+ await expect
+ .poll(() => comfyPage.nodeOps.getGraphNodesCount(), { timeout: 1000 })
+ .toBe(0)
+
+ const canvasBox = (await comfyPage.canvas.boundingBox())!
+ await comfyPage.canvas.click({
+ position: { x: canvasBox.width / 2, y: canvasBox.height / 2 }
+ })
+
+ await expect.poll(() => comfyPage.nodeOps.getGraphNodesCount()).toBe(1)
+ await expect
+ .poll(() => comfyPage.nodeOps.getSelectedGraphNodesCount())
+ .toBe(1)
+
+ const [loader] = await comfyPage.nodeOps.getNodeRefsByType(
+ 'CheckpointLoaderSimple'
+ )
+ expect(loader).toBeDefined()
+ const widget = await loader.getWidgetByName('ckpt_name')
+ expect(await widget.getValue()).toBe(STABLE_CHECKPOINT.name)
+ })
+})
diff --git a/browser_tests/tests/cloud-asset-promoted-widget.spec.ts b/browser_tests/tests/cloud-asset-promoted-widget.spec.ts
new file mode 100644
index 0000000000..63ccd73168
--- /dev/null
+++ b/browser_tests/tests/cloud-asset-promoted-widget.spec.ts
@@ -0,0 +1,99 @@
+import { expect } from '@playwright/test'
+import type { Page } from '@playwright/test'
+
+import {
+ assetRequestIncludesTag,
+ createCloudAssetsFixture
+} from '@e2e/fixtures/assetApiFixture'
+import {
+ STABLE_CHECKPOINT,
+ STABLE_CHECKPOINT_2
+} from '@e2e/fixtures/data/assetFixtures'
+import { TestIds } from '@e2e/fixtures/selectors'
+
+const WORKFLOW = 'missing/missing_model_promoted_widget'
+const HOST_NODE_ID = 2
+const WIDGET_NAME = 'ckpt_name'
+const SELECTED_MODEL = STABLE_CHECKPOINT_2.name
+
+const test = createCloudAssetsFixture([STABLE_CHECKPOINT, STABLE_CHECKPOINT_2])
+
+interface WidgetSnapshot {
+ type: string
+ value: string
+ hasLayout: boolean
+}
+
+async function getHostWidgetSnapshot(page: Page): Promise {
+ return await page.evaluate(
+ ({ nodeId, widgetName }) => {
+ const node = window.app!.graph.getNodeById(nodeId)
+ const widget = node?.widgets?.find((widget) => widget.name === widgetName)
+
+ return {
+ type: widget?.type ?? '',
+ value: String(widget?.value ?? ''),
+ hasLayout: widget?.last_y != null
+ }
+ },
+ { nodeId: HOST_NODE_ID, widgetName: WIDGET_NAME }
+ )
+}
+
+test.describe(
+ 'Promoted subgraph asset widgets',
+ { tag: ['@cloud', '@canvas', '@widget'] },
+ () => {
+ test.afterEach(async ({ comfyPage }) => {
+ await comfyPage.nodeOps.clearGraph()
+ })
+
+ test('legacy asset browser selection updates the promoted host widget value', async ({
+ cloudAssetRequests,
+ comfyPage
+ }) => {
+ await comfyPage.settings.setSetting('Comfy.Assets.UseAssetAPI', true)
+ await comfyPage.workflow.loadWorkflow(WORKFLOW)
+
+ await expect
+ .poll(
+ () =>
+ cloudAssetRequests.some((url) =>
+ assetRequestIncludesTag(url, 'checkpoints')
+ ),
+ { timeout: 10_000 }
+ )
+ .toBe(true)
+ await expect
+ .poll(() => getHostWidgetSnapshot(comfyPage.page))
+ .toMatchObject({
+ type: 'asset',
+ hasLayout: true
+ })
+ const initialWidget = await getHostWidgetSnapshot(comfyPage.page)
+ expect(initialWidget.value).not.toBe(SELECTED_MODEL)
+
+ const hostNode = await comfyPage.nodeOps.getNodeRefById(HOST_NODE_ID)
+ await hostNode.centerOnNode()
+ const promotedWidget = await hostNode.getWidgetByName(WIDGET_NAME)
+ await promotedWidget.click()
+
+ const modal = comfyPage.page.getByTestId(TestIds.assets.browserModal)
+ await expect(modal).toBeVisible()
+
+ const assetCard = modal
+ .getByTestId(TestIds.assets.card)
+ .filter({ hasText: SELECTED_MODEL })
+ .first()
+ await expect(assetCard).toBeVisible()
+ await assetCard.getByRole('button', { name: 'Use' }).click()
+
+ await expect(modal).toBeHidden()
+ await expect
+ .poll(() =>
+ getHostWidgetSnapshot(comfyPage.page).then((widget) => widget.value)
+ )
+ .toBe(SELECTED_MODEL)
+ })
+ }
+)
diff --git a/browser_tests/tests/cloud.spec.ts b/browser_tests/tests/cloud.spec.ts
index 3aa339af6b..2ebacf7fe0 100644
--- a/browser_tests/tests/cloud.spec.ts
+++ b/browser_tests/tests/cloud.spec.ts
@@ -1,6 +1,9 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
+const APP_URL = process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188'
+const SHARE_AUTH_STORAGE_KEY = 'Comfy.PreservedQuery.share_auth'
+
/**
* Cloud distribution E2E tests.
*
@@ -14,15 +17,31 @@ test.describe('Cloud distribution UI', { tag: '@cloud' }, () => {
test('cloud build redirects unauthenticated users to login', async ({
page
}) => {
- await page.goto('http://localhost:8188')
+ await page.goto(APP_URL)
// Cloud build has an auth guard that redirects to /cloud/login.
// This route only exists in the cloud distribution — it's tree-shaken
// in the OSS build. Its presence confirms the cloud build is active.
await expect(page).toHaveURL(/\/cloud\/login/, { timeout: 10_000 })
})
+ test('preserves share auth attribution before redirecting logged-out users', async ({
+ page
+ }) => {
+ await page.goto(new URL('/?share=abc', APP_URL).toString())
+
+ await expect(page).toHaveURL(/\/cloud\/login/, { timeout: 10_000 })
+ await expect
+ .poll(() =>
+ page.evaluate(
+ (key) => sessionStorage.getItem(key),
+ SHARE_AUTH_STORAGE_KEY
+ )
+ )
+ .toBe(JSON.stringify({ share: 'abc' }))
+ })
+
test('cloud login page renders sign-in options', async ({ page }) => {
- await page.goto('http://localhost:8188')
+ await page.goto(APP_URL)
await expect(page).toHaveURL(/\/cloud\/login/, { timeout: 10_000 })
// Verify cloud-specific login UI is rendered
await expect(page.getByRole('button', { name: /google/i })).toBeVisible()
diff --git a/browser_tests/tests/maskEditor.spec.ts b/browser_tests/tests/maskEditor.spec.ts
index d6a084a6f9..0db46ed66f 100644
--- a/browser_tests/tests/maskEditor.spec.ts
+++ b/browser_tests/tests/maskEditor.spec.ts
@@ -254,21 +254,8 @@ test.describe('Mask Editor', { tag: '@vue-nodes' }, () => {
}) => {
const dialog = await maskEditor.openDialog()
- let maskUploadCount = 0
let imageUploadCount = 0
- await comfyPage.page.route('**/upload/mask', (route) => {
- maskUploadCount++
- return route.fulfill({
- status: 200,
- contentType: 'application/json',
- body: JSON.stringify({
- name: `test-mask-${maskUploadCount}.png`,
- subfolder: 'clipspace',
- type: 'input'
- })
- })
- })
await comfyPage.page.route('**/upload/image', (route) => {
imageUploadCount++
return route.fulfill({
@@ -288,20 +275,17 @@ test.describe('Mask Editor', { tag: '@vue-nodes' }, () => {
await expect(dialog).toBeHidden()
- // The save pipeline uploads multiple layers (mask + image variants)
+ // The save pipeline uploads four layers (masked, paint, painted, paintedMasked)
+ // through the unified /upload/image endpoint.
expect(
- maskUploadCount + imageUploadCount,
- 'save should trigger upload calls'
- ).toBeGreaterThan(0)
+ imageUploadCount,
+ 'save should upload all four layers via /upload/image'
+ ).toBe(4)
})
test('save failure keeps dialog open', async ({ comfyPage, maskEditor }) => {
const dialog = await maskEditor.openDialog()
- // Fail all upload routes
- await comfyPage.page.route('**/upload/mask', (route) =>
- route.fulfill({ status: 500 })
- )
await comfyPage.page.route('**/upload/image', (route) =>
route.fulfill({ status: 500 })
)
diff --git a/browser_tests/tests/maskEditorLoadSave.spec.ts b/browser_tests/tests/maskEditorLoadSave.spec.ts
index 100259851d..1c418c2331 100644
--- a/browser_tests/tests/maskEditorLoadSave.spec.ts
+++ b/browser_tests/tests/maskEditorLoadSave.spec.ts
@@ -34,19 +34,17 @@ test.describe('Mask Editor load/save', { tag: '@vue-nodes' }, () => {
let observedContentType = ''
let observedBodyLength = 0
- await comfyPage.page.route('**/upload/mask', async (route) => {
+ await comfyPage.page.route('**/upload/image', async (route) => {
const request = route.request()
- observedContentType = (await request.headerValue('content-type')) ?? ''
- observedBodyLength = request.postDataBuffer()?.byteLength ?? 0
+ if (!observedContentType) {
+ observedContentType = (await request.headerValue('content-type')) ?? ''
+ observedBodyLength = request.postDataBuffer()?.byteLength ?? 0
+ }
await route.fulfill(
fulfillJson(successResponse('clipspace-mask-123.png'))
)
})
- await comfyPage.page.route('**/upload/image', (route) =>
- route.fulfill(fulfillJson(successResponse('clipspace-painted-123.png')))
- )
-
await dialog.getByRole('button', { name: 'Save' }).click()
await expect(dialog).toBeHidden()
expect(observedContentType).toContain('multipart/form-data')
@@ -69,24 +67,11 @@ test.describe('Mask Editor load/save', { tag: '@vue-nodes' }, () => {
await expect(dialog).toBeVisible()
})
- test('Save failure on partial upload keeps dialog open', async ({
- comfyPage,
- maskEditor
- }) => {
+ test('Save failure keeps dialog open', async ({ comfyPage, maskEditor }) => {
const dialog = await maskEditor.openDialog()
await maskEditor.drawStrokeAndExpectPixels(dialog)
- // The saver uploads sequentially: mask layer first, then image layers.
- // Let the mask upload succeed and the image upload fail to exercise both
- // endpoints and verify the dialog stays open after a partial failure.
- let maskUploadHit = false
let imageUploadHit = false
- await comfyPage.page.route('**/upload/mask', (route) => {
- maskUploadHit = true
- return route.fulfill(
- fulfillJson(successResponse('clipspace-mask-999.png'))
- )
- })
await comfyPage.page.route('**/upload/image', (route) => {
imageUploadHit = true
return route.fulfill({ status: 500 })
@@ -95,7 +80,6 @@ test.describe('Mask Editor load/save', { tag: '@vue-nodes' }, () => {
const saveButton = dialog.getByRole('button', { name: 'Save' })
await saveButton.click()
- await expect.poll(() => maskUploadHit).toBe(true)
await expect.poll(() => imageUploadHit).toBe(true)
await expect(dialog).toBeVisible()
await expect(saveButton).toBeVisible()
diff --git a/browser_tests/tests/propertiesPanel/errorsTabMissingModels.spec.ts b/browser_tests/tests/propertiesPanel/errorsTabMissingModels.spec.ts
index c071201417..c41fd6145e 100644
--- a/browser_tests/tests/propertiesPanel/errorsTabMissingModels.spec.ts
+++ b/browser_tests/tests/propertiesPanel/errorsTabMissingModels.spec.ts
@@ -143,7 +143,7 @@ test.describe('Errors tab - Missing models', { tag: '@ui' }, () => {
const objectInfo = await response.json()
const ckptName =
objectInfo.CheckpointLoaderSimple.input.required.ckpt_name
- ckptName[0] = [...ckptName[0], 'fake_model.safetensors']
+ ckptName[0] = [...ckptName[0], FAKE_MODEL_NAME]
await route.fulfill({ response, json: objectInfo })
})
@@ -151,21 +151,11 @@ test.describe('Errors tab - Missing models', { tag: '@ui' }, () => {
const url = new URL(response.url())
return url.pathname.endsWith('/object_info') && response.ok()
})
- const modelFoldersResponse = comfyPage.page.waitForResponse(
- (response) => {
- const url = new URL(response.url())
- return url.pathname.endsWith('/experiment/models') && response.ok()
- }
- )
const refreshButton = comfyPage.page.getByTestId(
TestIds.dialogs.missingModelRefresh
)
- await Promise.all([
- objectInfoResponse,
- modelFoldersResponse,
- refreshButton.click()
- ])
+ await Promise.all([objectInfoResponse, refreshButton.click()])
await expect(
comfyPage.page.getByTestId(TestIds.dialogs.missingModelsGroup)
).toBeHidden()
diff --git a/browser_tests/tests/sidebar/assets.spec.ts b/browser_tests/tests/sidebar/assets.spec.ts
index 9957776296..f8de38815f 100644
--- a/browser_tests/tests/sidebar/assets.spec.ts
+++ b/browser_tests/tests/sidebar/assets.spec.ts
@@ -13,10 +13,6 @@ import type {
// Legacy coverage backed by AssetsHelper's shadow backend. New assets-sidebar
// browser coverage should use typed route mocks in assetsSidebarTab.spec.ts.
-// ---------------------------------------------------------------------------
-// Shared fixtures
-// ---------------------------------------------------------------------------
-
const SAMPLE_JOBS: RawJobListItem[] = [
createMockJob({
id: 'job-alpha',
@@ -180,12 +176,10 @@ test.describe('Assets sidebar - tab navigation', () => {
const tab = comfyPage.menu.assetsTab
await tab.open()
- // Switch to Imported
await tab.switchToImported()
await expect(tab.importedTab).toHaveAttribute('aria-selected', 'true')
await expect(tab.generatedTab).toHaveAttribute('aria-selected', 'false')
- // Switch back to Generated
await tab.switchToGenerated()
await expect(tab.generatedTab).toHaveAttribute('aria-selected', 'true')
})
@@ -194,11 +188,9 @@ test.describe('Assets sidebar - tab navigation', () => {
const tab = comfyPage.menu.assetsTab
await tab.open()
- // Type search in Generated tab
await tab.searchInput.fill('landscape')
await expect(tab.searchInput).toHaveValue('landscape')
- // Switch to Imported tab
await tab.switchToImported()
await expect(tab.searchInput).toHaveValue('')
})
@@ -235,10 +227,8 @@ test.describe('Assets sidebar - grid view display', () => {
await tab.open()
await tab.switchToImported()
- // Wait for imported assets to render
await expect(tab.assetCards.first()).toBeVisible()
- // Imported tab should show the mocked files
await expect.poll(() => tab.assetCards.count()).toBeGreaterThanOrEqual(1)
})
@@ -286,11 +276,9 @@ test.describe('Assets sidebar - view mode toggle', () => {
const tab = comfyPage.menu.assetsTab
await tab.open()
- // Open settings menu and select list view
await tab.openSettingsMenu()
await tab.listViewOption.click()
- // List view items should now be visible
await expect(tab.listViewItems.first()).toBeVisible()
})
@@ -298,16 +286,13 @@ test.describe('Assets sidebar - view mode toggle', () => {
const tab = comfyPage.menu.assetsTab
await tab.open()
- // Switch to list view
await tab.openSettingsMenu()
await tab.listViewOption.click()
await expect(tab.listViewItems.first()).toBeVisible()
- // Switch back to grid view (settings popover is still open)
await tab.gridViewOption.click()
await tab.waitForAssets()
- // Grid cards (with data-selected attribute) should be visible again
await expect(tab.assetCards.first()).toBeVisible()
})
})
@@ -342,10 +327,8 @@ test.describe('Assets sidebar - search', () => {
const initialCount = await tab.assetCards.count()
- // Search for a specific filename that matches only one asset
await tab.searchInput.fill('landscape')
- // Wait for filter to reduce the count
await expect.poll(() => tab.assetCards.count()).toBeLessThan(initialCount)
})
@@ -355,7 +338,6 @@ test.describe('Assets sidebar - search', () => {
const initialCount = await tab.assetCards.count()
- // Filter then clear
await tab.searchInput.fill('landscape')
await expect.poll(() => tab.assetCards.count()).toBeLessThan(initialCount)
@@ -391,10 +373,8 @@ test.describe('Assets sidebar - selection', () => {
const tab = comfyPage.menu.assetsTab
await tab.open()
- // Click first asset card
await tab.assetCards.first().click()
- // Should have data-selected="true"
await expect(tab.selectedCards).toHaveCount(1)
})
@@ -405,11 +385,9 @@ test.describe('Assets sidebar - selection', () => {
const cards = tab.assetCards
await expect.poll(() => cards.count()).toBeGreaterThanOrEqual(2)
- // Click first card
await cards.first().click()
await expect(tab.selectedCards).toHaveCount(1)
- // Ctrl+click second card
await cards.nth(1).click({ modifiers: ['ControlOrMeta'] })
await expect(tab.selectedCards).toHaveCount(2)
})
@@ -420,10 +398,8 @@ test.describe('Assets sidebar - selection', () => {
const tab = comfyPage.menu.assetsTab
await tab.open()
- // Select an asset
await tab.assetCards.first().click()
- // Footer should show selection count
await expect(tab.selectionCountButton).toBeVisible()
})
@@ -431,15 +407,10 @@ test.describe('Assets sidebar - selection', () => {
const tab = comfyPage.menu.assetsTab
await tab.open()
- // Select an asset
await tab.assetCards.first().click()
await expect(tab.selectedCards).toHaveCount(1)
- // Hover over the selection count button to reveal "Deselect all"
- await tab.selectionCountButton.hover()
await expect(tab.deselectAllButton).toBeVisible()
-
- // Click "Deselect all"
await tab.deselectAllButton.click()
await expect(tab.selectedCards).toHaveCount(0)
})
@@ -448,14 +419,11 @@ test.describe('Assets sidebar - selection', () => {
const tab = comfyPage.menu.assetsTab
await tab.open()
- // Select an asset
await tab.assetCards.first().click()
await expect(tab.selectedCards).toHaveCount(1)
- // Switch to Imported tab
await tab.switchToImported()
- // Switch back - selection should be cleared
await tab.switchToGenerated()
await tab.waitForAssets()
await expect(tab.selectedCards).toHaveCount(0)
@@ -481,10 +449,8 @@ test.describe('Assets sidebar - context menu', () => {
const tab = comfyPage.menu.assetsTab
await tab.open()
- // Right-click first asset
await tab.assetCards.first().click({ button: 'right' })
- // Context menu should appear with standard items
const contextMenu = comfyPage.page.locator('.p-contextmenu')
await expect(contextMenu).toBeVisible()
})
@@ -565,8 +531,6 @@ test.describe('Assets sidebar - context menu', () => {
test('Cancelling export-workflow filename prompt does not show an error toast', async ({
comfyPage
}) => {
- // job-gamma is the first card; its detail carries a valid workflow so
- // extraction succeeds and the filename prompt opens.
await comfyPage.assets.mockJobDetail('job-gamma', JOB_GAMMA_DETAIL)
const tab = comfyPage.menu.assetsTab
@@ -614,8 +578,6 @@ test.describe('Assets sidebar - context menu', () => {
test('Export-workflow shows a warning toast when the asset has no workflow', async ({
comfyPage
}) => {
- // Strip the workflow field so extraction yields null and the export
- // action returns { success: false, error: 'No workflow…' }.
const { workflow: _, ...detailWithoutWorkflow } = JOB_GAMMA_DETAIL
await comfyPage.assets.mockJobDetail('job-gamma', detailWithoutWorkflow)
@@ -625,7 +587,6 @@ test.describe('Assets sidebar - context menu', () => {
await tab.assetCards.first().click({ button: 'right' })
await tab.contextMenuItem('Export workflow').click()
- // Filename prompt should be skipped: extraction fails before the prompt.
await expect(comfyPage.toast.toastWarnings).toBeVisible()
await expect(comfyPage.toast.toastSuccesses).toBeHidden({ timeout: 1500 })
})
@@ -639,23 +600,18 @@ test.describe('Assets sidebar - context menu', () => {
const cards = tab.assetCards
await expect.poll(() => cards.count()).toBeGreaterThanOrEqual(2)
- // Dismiss any toasts that appeared after asset loading
await tab.dismissToasts()
- // Multi-select: use keyboard.down/up so useKeyModifier('Control') detects
- // the modifier — click({ modifiers }) only sets the mouse event flag and
- // does not fire a keydown event that VueUse tracks.
+ // useKeyModifier('Control') needs keyboard events, not click modifiers.
await cards.first().click()
await comfyPage.page.keyboard.down('Control')
await cards.nth(1).click()
await comfyPage.page.keyboard.up('Control')
- // Verify multi-selection took effect and footer is stable before right-clicking
await expect(tab.selectedCards).toHaveCount(2)
await expect(tab.selectionFooter).toBeVisible()
- // Use dispatchEvent instead of click({ button: 'right' }) to avoid any
- // overlay intercepting the event, and assert directly without toPass.
+ // dispatchEvent avoids the selection footer intercepting a right click.
const contextMenu = comfyPage.page.locator('.p-contextmenu')
await cards.first().dispatchEvent('contextmenu', {
bubbles: true,
@@ -664,7 +620,6 @@ test.describe('Assets sidebar - context menu', () => {
})
await expect(contextMenu).toBeVisible()
- // Bulk menu should show bulk download action
await expect(tab.contextMenuItem('Download all')).toBeVisible()
})
})
@@ -692,7 +647,6 @@ test.describe('Assets sidebar - bulk actions', () => {
await tab.assetCards.first().click()
- // Download button in footer should be visible
await expect(tab.downloadSelectedButton).toBeVisible()
})
@@ -704,7 +658,6 @@ test.describe('Assets sidebar - bulk actions', () => {
await tab.assetCards.first().click()
- // Delete button in footer should be visible
await expect(tab.deleteSelectedButton).toBeVisible()
})
@@ -712,21 +665,67 @@ test.describe('Assets sidebar - bulk actions', () => {
const tab = comfyPage.menu.assetsTab
await tab.open()
- // Select the two single-output assets (job-alpha, job-beta).
- // The count reflects total outputs, not cards — job-gamma has
- // outputs_count: 2 which would inflate the total.
const cards = tab.assetCards
await expect.poll(() => cards.count()).toBeGreaterThanOrEqual(3)
- // Cards are sorted newest-first: gamma (idx 0), beta (1), alpha (2)
await cards.nth(1).click()
await comfyPage.page.keyboard.down('Control')
await cards.nth(2).click()
await comfyPage.page.keyboard.up('Control')
- // Selection count should show the count
await expect(tab.selectionCountButton).toBeVisible()
- await expect(tab.selectionCountButton).toHaveText(/Assets Selected:\s*2\b/)
+ await expect(tab.selectionCountButton).toHaveText(/\b2 selected\b/)
+ })
+
+ test('Selection count sums the outputs of a stacked asset', async ({
+ comfyPage
+ }) => {
+ const tab = comfyPage.menu.assetsTab
+ await tab.open()
+
+ await tab.assetCards.first().click()
+
+ await expect(tab.selectionCountButton).toBeVisible()
+ await expect(tab.selectionCountButton).toHaveText(/\b2 selected\b/)
+ })
+
+ test('Selection bar stays capped, not stretched, on a wide panel', async ({
+ comfyPage
+ }) => {
+ await comfyPage.page.setViewportSize({ width: 1600, height: 900 })
+ const tab = comfyPage.menu.assetsTab
+ await tab.open()
+
+ const gutter = comfyPage.page.locator('.p-splitter-gutter').first()
+ await expect(gutter).toBeVisible()
+ const gutterBox = await gutter.boundingBox()
+ if (!gutterBox) {
+ throw new Error('sidebar splitter gutter has no bounding box')
+ }
+ await comfyPage.page.mouse.move(
+ gutterBox.x + gutterBox.width / 2,
+ gutterBox.y + gutterBox.height / 2
+ )
+ await comfyPage.page.mouse.down()
+ await comfyPage.page.mouse.move(900, gutterBox.y + gutterBox.height / 2, {
+ steps: 12
+ })
+ await comfyPage.page.mouse.up()
+
+ await tab.assetCards.first().click()
+ await expect(tab.selectionFooter).toBeVisible()
+
+ const sidebar = comfyPage.page.locator('.side-bar-panel').first()
+ await expect
+ .poll(async () => (await sidebar.boundingBox())?.width ?? 0)
+ .toBeGreaterThan(520)
+ await expect
+ .poll(async () => {
+ const bar = await tab.selectionFooter.boundingBox()
+ const side = await sidebar.boundingBox()
+ return bar && side ? side.width - bar.width : 0
+ })
+ .toBeGreaterThan(100)
})
})
@@ -833,8 +832,7 @@ test.describe('Assets sidebar - pagination', () => {
await comfyPage.assets.mockOutputHistory(manyJobs)
await comfyPage.setup()
- // Capture the first history fetch (terminal statuses only).
- // Queue polling also hits /jobs but with status=in_progress,pending.
+ // Queue polling also calls /jobs, so wait for completed history only.
const firstRequest = comfyPage.page.waitForRequest((req) => {
if (!/\/api\/jobs\?/.test(req.url())) return false
const url = new URL(req.url())
@@ -1002,9 +1000,7 @@ const MIXED_MEDIA_JOBS: RawJobListItem[] = [
})
]
-// Filter button is guarded by isCloud (compile-time). The cloud CI project
-// cannot use comfyPageFixture (auth required). Enable once cloud E2E infra
-// supports authenticated comfyPage setup.
+// Filter button is guarded by isCloud; cloud CI needs authenticated setup.
test.describe('Assets sidebar - media type filter', () => {
test.fixme(true, 'Requires DISTRIBUTION=cloud build with auth bypass')
@@ -1040,12 +1036,9 @@ test.describe('Assets sidebar - media type filter', () => {
'All three mixed-media jobs should render'
).toHaveCount(3)
- // Open filter menu and enable only image filter (selecting a filter
- // restricts to that type only, hiding unselected types)
await tab.openFilterMenu()
await tab.filterCheckbox('Image').click()
- // Only the image asset should remain
await expect(tab.assetCards).toHaveCount(1, { timeout: 5000 })
await expect(tab.getAssetCardByName('photo.png')).toBeVisible()
})
@@ -1056,12 +1049,10 @@ test.describe('Assets sidebar - media type filter', () => {
const initialCount = await tab.assetCards.count()
- // Enable image filter to restrict to images only
await tab.openFilterMenu()
await tab.filterCheckbox('Image').click()
await expect(tab.assetCards).toHaveCount(1, { timeout: 5000 })
- // Uncheck image filter to remove all filters (restores all assets)
await tab.filterCheckbox('Image').click()
await expect(tab.assetCards).toHaveCount(initialCount, { timeout: 5000 })
})
diff --git a/browser_tests/tests/sidebar/assetsSidebarTab.spec.ts b/browser_tests/tests/sidebar/assetsSidebarTab.spec.ts
index 270c050a3b..becfffc4cb 100644
--- a/browser_tests/tests/sidebar/assetsSidebarTab.spec.ts
+++ b/browser_tests/tests/sidebar/assetsSidebarTab.spec.ts
@@ -214,7 +214,7 @@ test.describe('FE-130 assets sidebar route mocks', () => {
await tab.open()
await tab.getAssetCardByName('alpha').click()
- await expect(tab.selectionCountButton).toHaveText(/Assets Selected:\s*1\b/)
+ await expect(tab.selectionCountButton).toHaveText(/\b1 selected\b/)
await expect(tab.deleteSelectedButton).toBeVisible()
await expect(tab.downloadSelectedButton).toBeVisible()
@@ -222,7 +222,7 @@ test.describe('FE-130 assets sidebar route mocks', () => {
await tab.getAssetCardByName('beta').click()
await comfyPage.page.keyboard.up('Control')
- await expect(tab.selectionCountButton).toHaveText(/Assets Selected:\s*2\b/)
+ await expect(tab.selectionCountButton).toHaveText(/\b2 selected\b/)
await expect(tab.deleteSelectedButton).toBeVisible()
await expect(tab.downloadSelectedButton).toBeVisible()
})
diff --git a/browser_tests/tests/sidebar/modelLibrary.spec.ts b/browser_tests/tests/sidebar/modelLibrary.spec.ts
index b49adc7cd5..96ca924e7f 100644
--- a/browser_tests/tests/sidebar/modelLibrary.spec.ts
+++ b/browser_tests/tests/sidebar/modelLibrary.spec.ts
@@ -233,4 +233,64 @@ test.describe('Model library sidebar - empty state', () => {
await expect(tab.folderNodes).toHaveCount(0)
await expect(tab.leafNodes).toHaveCount(0)
})
+
+ test.describe('Model library sidebar - add node', () => {
+ test.beforeEach(async ({ comfyPage }) => {
+ await comfyPage.modelLibrary.mockFoldersWithFiles(MOCK_FOLDERS)
+ await comfyPage.setup()
+ await comfyPage.nodeOps.clearGraph()
+ })
+
+ test.afterEach(async ({ comfyPage }) => {
+ await comfyPage.modelLibrary.clearMocks()
+ })
+
+ test('Clicking a model defers creation until placed on the canvas', async ({
+ comfyPage
+ }) => {
+ const tab = comfyPage.menu.modelLibraryTab
+ await tab.open()
+ await tab.getFolderByLabel('checkpoints').click()
+ await expect(tab.getLeafByLabel('sd_xl_base_1.0')).toBeVisible()
+
+ await tab.getLeafByLabel('sd_xl_base_1.0').click()
+
+ await expect
+ .poll(() => comfyPage.nodeOps.getGraphNodesCount(), { timeout: 1000 })
+ .toBe(0)
+
+ const canvasBox = (await comfyPage.canvas.boundingBox())!
+ await comfyPage.canvas.click({
+ position: { x: canvasBox.width / 2, y: canvasBox.height / 2 }
+ })
+
+ await expect.poll(() => comfyPage.nodeOps.getGraphNodesCount()).toBe(1)
+ await expect
+ .poll(() => comfyPage.nodeOps.getSelectedGraphNodesCount())
+ .toBe(1)
+
+ const [loader] = await comfyPage.nodeOps.getNodeRefsByType(
+ 'CheckpointLoaderSimple'
+ )
+ expect(loader).toBeDefined()
+ const widget = await loader.getWidgetByName('ckpt_name')
+ expect(await widget.getValue()).toBe('sd_xl_base_1.0.safetensors')
+ })
+
+ test('Ghost preview shows the model in the loader widget before placing', async ({
+ comfyPage
+ }) => {
+ const tab = comfyPage.menu.modelLibraryTab
+ await tab.open()
+ await tab.getFolderByLabel('checkpoints').click()
+ await expect(tab.getLeafByLabel('sd_xl_base_1.0')).toBeVisible()
+
+ await tab.getLeafByLabel('sd_xl_base_1.0').click()
+
+ const ghost = comfyPage.page.locator(
+ '[data-node-id="preview-CheckpointLoaderSimple"]'
+ )
+ await expect(ghost).toContainText('sd_xl_base_1.0.safetensors')
+ })
+ })
})
diff --git a/browser_tests/tests/subgraph/subgraphLifecycle.spec.ts b/browser_tests/tests/subgraph/subgraphLifecycle.spec.ts
index 1eaf7293da..d35529da1f 100644
--- a/browser_tests/tests/subgraph/subgraphLifecycle.spec.ts
+++ b/browser_tests/tests/subgraph/subgraphLifecycle.spec.ts
@@ -1,6 +1,9 @@
+import type { ConsoleMessage } from '@playwright/test'
import { expect } from '@playwright/test'
+import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
+import { TestIds } from '@e2e/fixtures/selectors'
import { getPseudoPreviewWidgets } from '@e2e/fixtures/utils/promotedWidgets'
const domPreviewSelector = '.image-preview'
@@ -95,4 +98,225 @@ test.describe('Subgraph Lifecycle', { tag: ['@subgraph'] }, () => {
await expect(comfyPage.page.locator(domPreviewSelector)).toHaveCount(0)
})
})
+
+ test.describe('Detach Race Repro', { tag: ['@vue-nodes'] }, () => {
+ const SUBGRAPH_NODE_TITLE = 'New Subgraph'
+
+ // Queues legacy onNodeRemoved/onSelectionChange so unpack completes first,
+ // widening the race window so a guard regression deterministically surfaces.
+ async function deferLegacyHandlers(comfyPage: ComfyPage) {
+ return await comfyPage.page.evaluateHandle(() => {
+ const graph = window.app!.graph!
+ const canvas = window.app!.canvas!
+ const queue: Array<() => void> = []
+ const originalNodeRemoved = graph.onNodeRemoved
+ const originalSelectionChange = canvas.onSelectionChange
+ graph.onNodeRemoved = function (node) {
+ queue.push(() => originalNodeRemoved?.call(this, node))
+ }
+ canvas.onSelectionChange = function (selected) {
+ queue.push(() => originalSelectionChange?.call(this, selected))
+ }
+ return {
+ drain: () => {
+ for (const fn of queue.splice(0)) fn()
+ },
+ restore: () => {
+ graph.onNodeRemoved = originalNodeRemoved
+ canvas.onSelectionChange = originalSelectionChange
+ }
+ }
+ })
+ }
+
+ type DeferredHandlers = Awaited>
+
+ // Defers only the legacy selection-change callback, so the detached host
+ // node lingers in the reactive selection while onNodeRemoved still runs
+ // normally and clears it from the canvas. This isolates the panel render
+ // path: a panel mounted during this window reads the stale selection.
+ async function deferSelectionChange(
+ comfyPage: ComfyPage
+ ): Promise {
+ return await comfyPage.page.evaluateHandle(() => {
+ const canvas = window.app!.canvas!
+ const queue: Array<() => void> = []
+ const original = canvas.onSelectionChange
+ canvas.onSelectionChange = function (selected) {
+ queue.push(() => original?.call(this, selected))
+ }
+ return {
+ drain: () => {
+ for (const fn of queue.splice(0)) fn()
+ },
+ restore: () => {
+ canvas.onSelectionChange = original
+ }
+ }
+ })
+ }
+
+ function isNullGraphErrorText(text: string): boolean {
+ return text.includes('NullGraphError') || text.endsWith('has no graph')
+ }
+
+ // Vue's default errorHandler routes render throws to console.error,
+ // not pageerror - listen to both.
+ function captureNullGraphErrors(comfyPage: ComfyPage) {
+ const captured: string[] = []
+ const onPageError = (err: Error) => {
+ if (
+ err.name === 'NullGraphError' ||
+ isNullGraphErrorText(err.message ?? '')
+ ) {
+ captured.push(`pageerror ${err.name}: ${err.message}`)
+ }
+ }
+ const onConsoleMessage = (msg: ConsoleMessage) => {
+ if (msg.type() !== 'error') return
+ const text = msg.text()
+ if (isNullGraphErrorText(text)) {
+ captured.push(`console.error: ${text}`)
+ }
+ }
+ comfyPage.page.on('pageerror', onPageError)
+ comfyPage.page.on('console', onConsoleMessage)
+ return {
+ getErrors: () => [...captured],
+ stop: () => {
+ comfyPage.page.off('pageerror', onPageError)
+ comfyPage.page.off('console', onConsoleMessage)
+ }
+ }
+ }
+
+ async function unpackViaContextMenu(comfyPage: ComfyPage, title: string) {
+ const fixture = await comfyPage.vueNodes.getFixtureByTitle(title)
+ await comfyPage.contextMenu.openForVueNode(fixture.header)
+ await comfyPage.contextMenu.clickMenuItemExact('Unpack Subgraph')
+ }
+
+ async function reopenRightSidePanel(comfyPage: ComfyPage) {
+ const { propertiesPanel } = comfyPage.menu
+ await propertiesPanel.toggleButton.click()
+ await expect(propertiesPanel.root).toBeHidden()
+ await propertiesPanel.toggleButton.click()
+ await comfyPage.nextFrame()
+ }
+
+ // Unpacks the subgraph behind deferred teardown, runs an optional
+ // interaction while the node is detached but not yet cleaned up, then
+ // drains the deferred handlers and reports any NullGraphErrors seen.
+ async function unpackAndCaptureNullGraphErrors(
+ comfyPage: ComfyPage,
+ options: {
+ defer: (comfyPage: ComfyPage) => Promise
+ duringWindow?: (comfyPage: ComfyPage) => Promise
+ }
+ ): Promise {
+ const subgraphNode =
+ comfyPage.vueNodes.getNodeByTitle(SUBGRAPH_NODE_TITLE)
+ const errors = captureNullGraphErrors(comfyPage)
+ const deferred = await options.defer(comfyPage)
+ try {
+ await unpackViaContextMenu(comfyPage, SUBGRAPH_NODE_TITLE)
+ await expect(subgraphNode).toHaveCount(0)
+ await options.duringWindow?.(comfyPage)
+ await deferred.evaluate((handlers) => handlers.drain())
+ // Let drained-handler reactive flushes settle before stop().
+ await comfyPage.nextFrame()
+ return errors.getErrors()
+ } finally {
+ await deferred.evaluate((handlers) => handlers.restore())
+ await deferred.dispose()
+ errors.stop()
+ }
+ }
+
+ test.beforeEach(async ({ comfyPage }) => {
+ await comfyPage.settings.setSetting('Comfy.RightSidePanel.IsOpen', true)
+ await comfyPage.workflow.loadWorkflow(
+ 'subgraphs/subgraph-with-promoted-text-widget'
+ )
+ const subgraphNode =
+ comfyPage.vueNodes.getNodeByTitle(SUBGRAPH_NODE_TITLE)
+ await expect(subgraphNode).toBeVisible()
+
+ const fixture =
+ await comfyPage.vueNodes.getFixtureByTitle(SUBGRAPH_NODE_TITLE)
+ await fixture.header.click()
+ await expect(
+ comfyPage.page.getByTestId(TestIds.propertiesPanel.root)
+ ).toBeVisible()
+ await comfyPage.nextFrame()
+ })
+
+ test('unpack does not surface NullGraphError on the LGraphNode render path', async ({
+ comfyPage
+ }) => {
+ const nullGraphErrors = await unpackAndCaptureNullGraphErrors(comfyPage, {
+ defer: deferLegacyHandlers
+ })
+ expect(
+ nullGraphErrors,
+ 'LGraphNode render path: detach race must not surface NullGraphError'
+ ).toEqual([])
+ })
+
+ test('unpack does not surface NullGraphError from the TabSubgraphInputs panel', async ({
+ comfyPage
+ }) => {
+ const nullGraphErrors = await unpackAndCaptureNullGraphErrors(comfyPage, {
+ defer: deferLegacyHandlers
+ })
+ expect(
+ nullGraphErrors,
+ 'TabSubgraphInputs panel: detach race must not surface NullGraphError'
+ ).toEqual([])
+ })
+
+ test('unpack with subgraph editor open does not surface NullGraphError from the SubgraphEditor panel', async ({
+ comfyPage
+ }) => {
+ await comfyPage.page.getByTestId(TestIds.subgraphEditor.toggle).click()
+ await comfyPage.nextFrame()
+
+ const nullGraphErrors = await unpackAndCaptureNullGraphErrors(comfyPage, {
+ defer: deferLegacyHandlers
+ })
+ expect(
+ nullGraphErrors,
+ 'SubgraphEditor panel: detach race must not surface NullGraphError'
+ ).toEqual([])
+ })
+
+ test('reopening the right side panel after unpack does not surface NullGraphError', async ({
+ comfyPage
+ }) => {
+ const nullGraphErrors = await unpackAndCaptureNullGraphErrors(comfyPage, {
+ defer: deferSelectionChange,
+ duringWindow: reopenRightSidePanel
+ })
+ expect(
+ nullGraphErrors,
+ 'TabSubgraphInputs remount: stale selection must not surface NullGraphError'
+ ).toEqual([])
+ })
+
+ test('reopening the right side panel with the subgraph editor open does not surface NullGraphError', async ({
+ comfyPage
+ }) => {
+ await comfyPage.page.getByTestId(TestIds.subgraphEditor.toggle).click()
+ await comfyPage.nextFrame()
+
+ const nullGraphErrors = await unpackAndCaptureNullGraphErrors(comfyPage, {
+ defer: deferSelectionChange,
+ duringWindow: reopenRightSidePanel
+ })
+ expect(
+ nullGraphErrors,
+ 'SubgraphEditor remount: stale selection must not surface NullGraphError'
+ ).toEqual([])
+ })
+ })
})
diff --git a/browser_tests/tests/vueNodes/groups/groups.spec.ts b/browser_tests/tests/vueNodes/groups/groups.spec.ts
index a5ef085a99..9d922a58c0 100644
--- a/browser_tests/tests/vueNodes/groups/groups.spec.ts
+++ b/browser_tests/tests/vueNodes/groups/groups.spec.ts
@@ -280,3 +280,36 @@ test.describe('Vue Node Groups', { tag: ['@screenshot', '@vue-nodes'] }, () => {
await expect.poll(bypassCount, "won't toggle double selected node").toBe(7)
})
})
+
+test.describe(
+ 'Vue Node Group Context Menu',
+ { tag: ['@vue-nodes', '@canvas'] },
+ () => {
+ test('right-clicking a group opens the Vue context menu instead of the legacy menu', async ({
+ comfyPage
+ }) => {
+ // Deselect so the right-click selects the group itself.
+ await comfyPage.keyboard.selectAll()
+ await comfyPage.page.keyboard.press(CREATE_GROUP_HOTKEY)
+ await expect
+ .poll(() => comfyPage.page.evaluate(() => graph!.groups.length))
+ .toBe(1)
+ await comfyPage.page.mouse.click(100, 100)
+ await comfyPage.nextFrame()
+
+ const groupPos = await getGroupTitlePosition(comfyPage, 'Group')
+ await comfyPage.page.mouse.click(groupPos.x, groupPos.y, {
+ button: 'right'
+ })
+
+ await expect(comfyPage.contextMenu.primeVueMenu).toBeVisible()
+ await expect(comfyPage.contextMenu.litegraphContextMenu).toBeHidden()
+ await expect(comfyPage.contextMenu.litegraphMenu).toBeHidden()
+
+ // Group-only action confirms it is the group menu.
+ await expect(
+ comfyPage.contextMenu.primeVueMenu.getByText('Fit Group To Nodes')
+ ).toBeVisible()
+ })
+ }
+)
diff --git a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts
index fdef92810c..381c57b074 100644
--- a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts
+++ b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts
@@ -335,6 +335,30 @@ test.describe('Vue Node Moving', { tag: '@vue-nodes' }, () => {
await comfyPage.canvasOps.moveMouseToEmptyArea()
})
+ test('pointerCancel stops autopan', async ({ comfyPage }) => {
+ const ksampler = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
+ await ksampler.header.click({ trial: true })
+ await comfyPage.page.mouse.down()
+
+ const getOffset = () => comfyPage.canvasOps.getOffset()
+ const initialOffset = await getOffset()
+ await comfyPage.page.mouse.move(10, 10, { steps: 20 })
+ await expect.poll(getOffset, 'drag with autopan').not.toEqual(initialOffset)
+
+ await test.step('move outside pan range and cancel drag', async () => {
+ await comfyPage.page.mouse.move(400, 400, { steps: 20 })
+ await ksampler.header.evaluate((node) =>
+ node.dispatchEvent(new PointerEvent('pointercancel', { bubbles: true }))
+ )
+ })
+
+ const secondaryOffset = await getOffset()
+
+ await comfyPage.page.mouse.move(10, 10, { steps: 20 })
+ await comfyPage.nextFrame()
+ expect(await getOffset(), 'drag canceled').toEqual(secondaryOffset)
+ })
+
test(
'@mobile should allow moving nodes by dragging on touch devices',
{ tag: '@screenshot' },
diff --git a/package.json b/package.json
index 079590ae00..bb4ef07edd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@comfyorg/comfyui-frontend",
- "version": "1.47.2",
+ "version": "1.47.3",
"private": true,
"description": "Official front-end implementation of ComfyUI",
"homepage": "https://comfy.org",
diff --git a/packages/registry-types/src/comfyRegistryTypes.ts b/packages/registry-types/src/comfyRegistryTypes.ts
index 182adf6eea..d7e82f2217 100644
--- a/packages/registry-types/src/comfyRegistryTypes.ts
+++ b/packages/registry-types/src/comfyRegistryTypes.ts
@@ -36,7 +36,7 @@ export interface paths {
put?: never;
/**
* Create a new customer
- * @description Creates a new customer using the provided token. No request body is needed as user information is extracted from the token.
+ * @description Creates a new customer. User identity is taken from the bearer token; the optional request body carries a Cloudflare Turnstile token used for server-side bot verification at signup (BE-1490). The body is optional — clients that do not run the Turnstile widget (e.g. the local OSS build) may omit it.
*/
post: operations["createCustomer"];
delete?: never;
@@ -239,6 +239,30 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/admin/sync-api-key-deletion": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Revoke a registry API key by hash (reverse delete-sync)
+ * @description Reverse-direction delete-sync (cloud → comfy-api registry, BE-1542). When a
+ * workspace API key is deleted in cloud (the new source of truth), cloud calls
+ * this endpoint so the comfy-api registry's api_keys row is removed too,
+ * keeping the two stores convergent. Idempotent: deleting an unknown hash is a
+ * no_op. M2M/admin-only; carries the key hash, never plaintext.
+ */
+ post: operations["SyncApiKeyDeletion"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/admin/generate-token": {
parameters: {
query?: never;
@@ -361,6 +385,26 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/customers/usage/timeseries": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Get customer's usage time series
+ * @description Returns the authenticated customer's gross usage spend, grouped by model, endpoint, or product, as one stacked data point per billing period, plus a per-group breakdown and a summary, for rendering a custom usage dashboard. Sourced from invoice line items (real USD). Replaces the embeddable iframe.
+ */
+ get: operations["GetCustomerUsageTimeSeries"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/customers/balance": {
parameters: {
query?: never;
@@ -1808,6 +1852,26 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/proxy/ideogram/ideogram-v4/generate": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Proxy request to Ideogram 4.0 for image generation
+ * @description Forwards text-to-image generation requests to Ideogram's 4.0 API and returns the results.
+ */
+ post: operations["ideogramV4Generate"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/proxy/kling/v1/account/costs": {
parameters: {
query?: never;
@@ -1893,6 +1957,57 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/proxy/kling/text-to-video/kling-3.0-turbo": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** KlingAI 3.0 Turbo Create Video from Text */
+ post: operations["klingV2CreateVideoFromText"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/proxy/kling/image-to-video/kling-3.0-turbo": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** KlingAI 3.0 Turbo Create Video from Image */
+ post: operations["klingV2CreateVideoFromImage"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/proxy/kling/tasks": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** KlingAI 3.0 Turbo Query Task by ID */
+ get: operations["klingV2QueryTask"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/proxy/kling/v1/videos/video-extend": {
parameters: {
query?: never;
@@ -2384,6 +2499,46 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/proxy/bfl/v1/flux-tools/vto-v1": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Proxy request to BFL Flux Tools VTO v1 for virtual try-on
+ * @description Forwards virtual try-on requests to BFL's Flux Tools VTO v1 API and returns the results. Person and garment images are mapped to the underlying input image slots.
+ */
+ post: operations["bflVtoV1Generate"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/proxy/bfl/v1/flux-tools/erase-v1": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Proxy request to BFL Flux Tools Erase v1 for object removal
+ * @description Forwards erase requests to BFL's Flux Tools Erase v1 API and returns the results. Uses an input image and a mask identifying the object or region to remove.
+ */
+ post: operations["bflEraseV1Generate"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/proxy/bfl/flux-pro-1.0-expand/generate": {
parameters: {
query?: never;
@@ -2919,6 +3074,26 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/proxy/runway/video_to_video": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Runway Video to Video Generation
+ * @description Edits a video into a new video using Runway's API
+ */
+ post: operations["runwayVideoToVideo"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/proxy/veo/generate": {
parameters: {
query?: never;
@@ -3658,6 +3833,41 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/proxy/tripo/v2/openapi/import": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Import an External 3D Model into Tripo
+ * @description Composite endpoint for Tripo model import (PN-328). The client first uploads the
+ * model file to ComfyUI API storage (POST /customers/storage) and then calls this
+ * endpoint with the resulting download URL. The backend performs the full Tripo
+ * import flow server-side: downloads the file from storage, obtains short-lived STS
+ * upload credentials from Tripo, uploads the file to Tripo's object storage (SigV4),
+ * and creates an import_model task referencing the uploaded object. Returns Tripo's
+ * create-task response; poll /proxy/tripo/v2/openapi/task/{task_id} for completion.
+ * The resulting task id is usable with Tripo post-processing tasks (texture_model,
+ * animate_rig, convert_model, ...).
+ *
+ * This is a synthetic comfy-api route (Tripo has no single-call equivalent); it
+ * exists so that Tripo's temporary storage credentials never leave the backend and
+ * no model binary ever travels inside this request.
+ *
+ * The url host must be ComfyUI API storage (storage.googleapis.com).
+ * Supported formats: glb, fbx, obj, stl. Maximum file size: 150MB.
+ */
+ post: operations["tripoImportModel"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/proxy/tripo/v2/openapi/task": {
parameters: {
query?: never;
@@ -4038,6 +4248,66 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/proxy/byteplus/api/v3/files": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Upload a file to BytePlus ModelArk
+ * @description Proxies POST https://ark.ap-southeast.bytepluses.com/api/v3/files. Uploads a binary file to ModelArk for later use (e.g. video understanding). See https://docs.byteplus.com/en/docs/ModelArk/1870405 for upstream details.
+ */
+ post: operations["byteplusFileUpload"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/proxy/byteplus/api/v3/files/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Retrieve BytePlus ModelArk file information
+ * @description Proxies GET https://ark.ap-southeast.bytepluses.com/api/v3/files/{id}. See https://docs.byteplus.com/en/docs/ModelArk/1870406 for upstream details.
+ */
+ get: operations["byteplusFileGet"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/proxy/byteplus/api/v3/responses": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Create a BytePlus ModelArk model response
+ * @description Proxies POST https://ark.ap-southeast.bytepluses.com/api/v3/responses. Creates a model response that supports text, image, video and file inputs, tool calls, structured output and deep-reasoning. See https://docs.byteplus.com/en/docs/ModelArk/1585128 for the upstream tutorial and request reference; the response object is documented at https://docs.byteplus.com/en/docs/ModelArk/1783703.
+ */
+ post: operations["byteplusResponseCreate"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/proxy/seedance/visual-validate/sessions": {
parameters: {
query?: never;
@@ -4755,6 +5025,212 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/proxy/anthropic/v1/messages": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Create a message via Anthropic Claude
+ * @description Forwards a Messages API request to Anthropic's `/v1/messages` endpoint
+ * and returns the model's reply. Supports both JSON responses and
+ * Server-Sent Events streaming (selected via `stream: true` in the body).
+ */
+ post: operations["anthropicCreateMessage"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/proxy/openrouter/api/v1/chat/completions": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Create a chat completion via OpenRouter
+ * @description Forwards a Chat Completions request to OpenRouter's `/api/v1/chat/completions`
+ * endpoint and returns the model's reply. Streaming (`stream: true`) is
+ * rejected: billing relies on the `usage.cost` value OpenRouter returns,
+ * which is not guaranteed on every SSE stream. Billing is based on the
+ * `usage.cost` field in the response body.
+ */
+ post: operations["openrouterCreateChatCompletion"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/proxy/krea/generate/image/krea/krea-2/medium": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Krea 2 Medium
+ * @description Best for expressive illustrations.
+ */
+ post: operations["kreaGenerateImageMedium"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/proxy/krea/generate/image/krea/krea-2/medium-turbo": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Krea 2 Medium Turbo
+ * @description Faster, more affordable variant of Krea 2 Medium.
+ */
+ post: operations["kreaGenerateImageMediumTurbo"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/proxy/krea/generate/image/krea/krea-2/large": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Krea 2 Large
+ * @description Best for expressive photorealism.
+ */
+ post: operations["kreaGenerateImageLarge"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/proxy/krea/assets": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Upload an asset
+ * @description Upload an asset
+ */
+ post: operations["kreaUploadAsset"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/proxy/krea/jobs/{job_id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Get a job by ID
+ * @description Get a job by ID
+ */
+ get: operations["kreaGetJob"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/proxy/beeble/v1/switchx/generations": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Start SwitchX Generation
+ * @description Start a SwitchX compositing job.
+ */
+ post: operations["beebleCreateSwitchXJob"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/proxy/beeble/v1/uploads": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Create Beeble Upload URL
+ * @description Create a presigned upload URL for a media file. The returned beeble_uri can be used as source_uri, reference_image_uri, or alpha_uri in SwitchX generation calls.
+ */
+ post: operations["beebleCreateUpload"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/proxy/beeble/v1/switchx/generations/{job_id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Get SwitchX Job Status
+ * @description Poll the status of a SwitchX job.
+ */
+ get: operations["beebleGetSwitchXStatus"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/proxy/quiver/v1/svgs/generations": {
parameters: {
query?: never;
@@ -4963,6 +5439,63 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/proxy/bria/v2/video/edit/green_screen": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Apply a green screen to a video using Bria
+ * @description Initiates an asynchronous green-screen (chroma key) job for a video using Bria's API.
+ * The original background is replaced with a solid broadcast-green, chroma-green, or blue
+ * screen suitable for compositing.
+ *
+ * Returns HTTP 202 with request_id and status_url. Poll the status endpoint for results.
+ *
+ * Supported input containers: .mp4, .mov, .webm, .avi, .gif
+ * Supported input codecs: H.264, H.265 (HEVC), VP9, AV1, PhotoJPEG
+ * Max input duration: 60 seconds. Input resolution up to 16000x16000.
+ */
+ post: operations["briaVideoGreenScreen"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/proxy/bria/v2/video/edit/replace_background": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Replace the background of a video using Bria
+ * @description Initiates an asynchronous background-replacement job for a video using Bria's API.
+ * The original background is composited out and replaced with the supplied background
+ * image or video.
+ *
+ * Returns HTTP 202 with request_id and status_url. Poll the status endpoint for results.
+ *
+ * Supported input containers: .mp4, .mov, .webm, .avi, .gif
+ * Supported input codecs: H.264, H.265 (HEVC), VP9, AV1, PhotoJPEG
+ * Max input duration: 60 seconds. Input resolution up to 16000x16000.
+ * The background asset must match the foreground aspect ratio.
+ */
+ post: operations["briaVideoReplaceBackground"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/proxy/bria/v2/image/edit/remove_background": {
parameters: {
query?: never;
@@ -5878,8 +6411,9 @@ export interface paths {
* Generate music from video
* @description Generate music from a video using Sonilo video-to-music AI.
* Accepts either a video file upload or a video URL, with an optional prompt.
- * Returns a streaming NDJSON response with duration, titles, audio chunks, and completion events.
- * Max video duration: 6 minutes. Max upload size: 1024MB.
+ * Returns a streaming NDJSON response with one or more parallel audio streams
+ * (titles, audio chunks, and completion events).
+ * Max video duration: 6 minutes. Max upload size: 300MB.
*/
post: operations["soniloVideoToMusicGenerate"];
delete?: never;
@@ -5900,8 +6434,8 @@ export interface paths {
/**
* Generate music from text prompt
* @description Generate music from a text prompt using Sonilo text-to-music AI.
- * Requires a prompt describing the desired music. Duration is optional and will be inferred if not provided.
- * Returns a streaming NDJSON response with duration, titles, audio chunks, and completion events.
+ * Requires a prompt describing the desired music and a caller-specified duration.
+ * Returns a streaming NDJSON response with titles, audio chunks, and completion events.
*/
post: operations["soniloTextToMusicGenerate"];
delete?: never;
@@ -5914,6 +6448,16 @@ export interface paths {
export type webhooks = Record;
export interface components {
schemas: {
+ /**
+ * @description One of a customer's API keys, for cloud's migrate-on-miss to seed into
+ * workspace_api_keys by hash. M2M/admin-only; carries the hash, never plaintext.
+ */
+ MigrationAPIKey: {
+ key_hash: string;
+ key_prefix?: string;
+ name?: string;
+ description?: string;
+ };
FreepikMagnificUpscalerCreativeRequest: {
/** @description Base64 image or URL to upscale. The resulted image can't exceed maximum allowed size of 25.3 million pixels. */
image: string;
@@ -6270,6 +6814,11 @@ export interface components {
* @enum {string}
*/
SubscriptionDuration: "MONTHLY" | "ANNUAL";
+ /**
+ * @description State of a withheld first-time free tier credit grant. "verification_required" means the grant was denied pending account verification (e.g. unverified email or a blocked email domain). "deferred" means the grant was temporarily deferred and may succeed on a later request. Absent when no grant was withheld.
+ * @enum {string}
+ */
+ FreeTierGrantState: "verification_required" | "deferred";
FeaturesResponse: {
/**
* @description The conversion rate for partner nodes
@@ -6389,6 +6938,86 @@ export interface components {
error: string;
message: string;
};
+ /** @description Grouped gross spend per billing period, breakdown, and summary for a customer. */
+ CustomerUsageTimeSeries: {
+ /**
+ * @description Dimension the spend is grouped by.
+ * @enum {string}
+ */
+ group_by: "model" | "endpoint" | "product";
+ /**
+ * @description Bucket size of the time series.
+ * @enum {string}
+ */
+ granularity: "hour" | "day" | "month";
+ /**
+ * Format: date-time
+ * @description Inclusive start of the returned range.
+ */
+ starting_on: string;
+ /**
+ * Format: date-time
+ * @description Exclusive end of the returned range.
+ */
+ ending_before: string;
+ /** @description Distinct group keys present in the range, ordered by spend descending. */
+ groups: string[];
+ /** @description One entry per (billing period, group) with non-zero gross spend. */
+ buckets: components["schemas"]["UsageBucket"][];
+ /** @description Per-group totals over the whole range, ordered by spend descending. */
+ breakdown: components["schemas"]["UsageBreakdownRow"][];
+ summary: components["schemas"]["UsageSummary"];
+ };
+ UsageBucket: {
+ /**
+ * Format: date-time
+ * @description Start of the billing period this bucket belongs to.
+ */
+ period_start: string;
+ /**
+ * Format: date-time
+ * @description End of the billing period this bucket belongs to.
+ */
+ period_end: string;
+ /** @description Group value (e.g. model name) this bucket belongs to. */
+ group_key: string;
+ /**
+ * Format: double
+ * @description Gross spend in this period for this group, in microamount (1/1,000,000 USD).
+ */
+ cost_micros: number;
+ };
+ UsageBreakdownRow: {
+ group_key: string;
+ /**
+ * Format: double
+ * @description Total gross spend for this group over the range, in microamount.
+ */
+ cost_micros: number;
+ /**
+ * Format: double
+ * @description Fraction of total spend attributable to this group (0-1).
+ */
+ share: number;
+ };
+ UsageSummary: {
+ /**
+ * Format: double
+ * @description Total gross spend over the range, in microamount.
+ */
+ spend_micros: number;
+ balance?: components["schemas"]["UsageBalance"];
+ };
+ /** @description Current remaining balance, mirroring /customers/balance. */
+ UsageBalance: {
+ /** Format: double */
+ amount_micros?: number;
+ /** Format: double */
+ prepaid_balance_micros?: number;
+ /** Format: double */
+ cloud_credit_balance_micros?: number;
+ currency?: string;
+ };
CreateCouponRequest: {
/** @description Name of the coupon displayed to customers */
name?: string;
@@ -6822,6 +7451,11 @@ export interface components {
/** @description The pip freeze output */
pip_freeze?: string;
};
+ /** @description Optional request body for customer creation (BE-1490). Carries the Cloudflare Turnstile token produced by the frontend widget, verified server-side at signup. All fields are optional; clients that do not run the Turnstile widget may send an empty body or omit the body entirely. */
+ CreateCustomerRequest: {
+ /** @description The Cloudflare Turnstile token (cf-turnstile-response) produced by the frontend widget. Verified server-side against Cloudflare siteverify. Omit or leave empty for clients without Turnstile (e.g. local OSS), which are exempt from the verification requirement. */
+ turnstile_token?: string;
+ };
Customer: {
/** @description The firebase UID of the user */
id: string;
@@ -7051,6 +7685,23 @@ export interface components {
style_type?: string;
}[];
};
+ /** @description Parameters for the Ideogram 4.0 (V4) text-to-image generation proxy request. Supply exactly one of text_prompt or json_prompt. */
+ IdeogramV4Request: {
+ /** @description Natural-language prompt. Enables Magic Prompt automatically. Supply exactly one of text_prompt or json_prompt. */
+ text_prompt?: string;
+ /** @description Structured V4 prompt. Disables Magic Prompt; consumed directly. Supply exactly one of text_prompt or json_prompt. */
+ json_prompt?: {
+ [key: string]: unknown;
+ };
+ /**
+ * @description Output resolution in WIDTHxHEIGHT. Omit to let the model pick an aspect ratio. Supported 2K values: 2048x2048, 1440x2880, 2880x1440, 1664x2496, 2496x1664, 1792x2240, 2240x1792, 1440x2560, 2560x1440, 1600x2560, 2560x1600, 1728x2304, 2304x1728, 1296x3168, 3168x1296, 1152x2944, 2944x1152, 1248x3328, 3328x1248, 1280x3072, 3072x1280.
+ * @example 2048x2048
+ */
+ resolution?: string;
+ rendering_speed?: components["schemas"]["RenderingSpeed"];
+ /** @description Opt into post-generation copyright detection (Hive likeness and logo checks). */
+ enable_copyright_detection?: boolean;
+ };
IdeogramV3RemixRequest: {
/** Format: binary */
image?: string;
@@ -7425,6 +8076,150 @@ export interface components {
/** @description Customized Task ID. Must be unique within a single user account. */
external_task_id?: string;
};
+ /** @description General configuration such as callback address and watermark options. */
+ KlingV2Options: {
+ /** @description Callback notification URL for task results. The server notifies when the task status changes. */
+ callback_url?: string;
+ /** @description Customized Task ID. Does not overwrite the system-generated task ID but can be used for queries. Must be unique within a single user account. */
+ external_task_id?: string;
+ /** @description Whether to generate watermarked results simultaneously. Custom watermarks are not supported. */
+ watermark_info?: {
+ /** @description true means generate watermarked result, false means do not generate. Default false. */
+ enabled?: boolean;
+ };
+ };
+ KlingV2Text2VideoRequest: {
+ /** @description Prompt that may include both positive and negative descriptions. Recommended length under 2500 characters. Multi-shot videos use the format "shot n, m, words; shot n, m, words;". */
+ prompt: string;
+ /** @description Output configuration such as resolution, aspect ratio and duration. */
+ settings?: {
+ /** @description Clarity of the generated video. One of "720p" or "1080p". Default "720p". */
+ resolution?: string;
+ /** @description Aspect ratio (width:height) of the generated frames. One of "16:9", "9:16" or "1:1". Default "16:9". */
+ aspect_ratio?: string;
+ /** @description Video length in seconds. Supported values 3 through 15. Default 5. */
+ duration?: number;
+ };
+ options?: components["schemas"]["KlingV2Options"];
+ };
+ KlingV2Image2VideoRequest: {
+ /** @description Collection of references such as prompts and images. Fields related to the same material belong in the same object. */
+ contents: {
+ /** @description Reference type. One of "prompt" or "first_frame". */
+ type?: string;
+ /** @description Text prompt. Provided when type is "prompt". May include positive and negative descriptions; cannot exceed 2500 characters. */
+ text?: string;
+ /** @description First-frame material. Provided when type is "first_frame". May be a URL or Base64 content. Supports .jpg, .jpeg and .png up to 50MB; width and height must be at least 300px with an aspect ratio between 1:2.5 and 2.5:1. */
+ url?: string;
+ }[];
+ /** @description Output configuration such as resolution and duration. */
+ settings?: {
+ /** @description Clarity of the generated video. One of "720p" or "1080p". Default "720p". */
+ resolution?: string;
+ /** @description Video length in seconds. Supported values 3 through 15. Default 5. */
+ duration?: number;
+ };
+ options?: components["schemas"]["KlingV2Options"];
+ };
+ /** @description Response returned when a Kling 3.0 Turbo task is created. */
+ KlingV2CreateTaskResponse: {
+ /** @description Error code. 0 indicates success. */
+ code?: number;
+ /** @description Error message. */
+ message?: string;
+ /** @description Request ID generated by the system. */
+ request_id?: string;
+ data?: {
+ /** @description The created task ID. */
+ id?: string;
+ /** @description Task status. One of "submitted", "processing", "succeeded" or "failed". */
+ status?: string;
+ /**
+ * Format: int64
+ * @description Task creation time. Unix timestamp in milliseconds.
+ */
+ create_time?: number;
+ /**
+ * Format: int64
+ * @description Task update time. Unix timestamp in milliseconds.
+ */
+ update_time?: number;
+ /** @description The custom task ID for this task, if any. */
+ external_id?: string;
+ };
+ };
+ /** @description A generated output. The fields present depend on `type` (video, image, audio, voice or element). */
+ KlingV2Output: {
+ /** @description Output content type. One of "video", "image", "audio", "voice" or "element". */
+ type?: string;
+ /** @description Output ID generated by the system. */
+ id?: string;
+ /** @description URL of the generated result (hotlink-protected). Cleared after 30 days. */
+ url?: string;
+ /** @description URL of the watermarked result (hotlink-protected). */
+ watermark_url?: string;
+ /** @description Duration of the generated video in seconds. */
+ duration?: string;
+ /** @description Grouping marker, present only for grouped images. */
+ group_id?: string;
+ /** @description MP3 URL of the generated audio (hotlink-protected). */
+ mp3_url?: string;
+ /** @description WAV URL of the generated audio (hotlink-protected). */
+ wav_url?: string;
+ /** @description Duration of the generated MP3 audio in seconds. */
+ mp3_duration?: string;
+ /** @description Duration of the generated WAV audio in seconds. */
+ wav_duration?: string;
+ /** @description Name of the generated material. */
+ name?: string;
+ /** @description Source of the material. "kling" denotes the official library; numbers are creator IDs. */
+ owned_by?: string;
+ /** @description Status of the material. One of "succeeded" or "deleted". */
+ status?: string;
+ };
+ /** @description A single Kling 3.0 Turbo task record. */
+ KlingV2Task: {
+ /** @description The task ID. */
+ id?: string;
+ /** @description Task status. One of "submitted", "processing", "succeeded" or "failed". */
+ status?: string;
+ /** @description Task status information, displaying the failure reason when the task fails. */
+ message?: string;
+ /**
+ * Format: int64
+ * @description Task creation time. Unix timestamp in milliseconds.
+ */
+ create_time?: number;
+ /**
+ * Format: int64
+ * @description Task update time. Unix timestamp in milliseconds.
+ */
+ update_time?: number;
+ /** @description The custom task ID for this task, if any. */
+ external_id?: string;
+ /** @description Generated outputs for the task. */
+ outputs?: components["schemas"]["KlingV2Output"][];
+ /** @description Billing details for the task. */
+ billing?: {
+ /** @description Consumption account type. "cash" for balance, "unit" for a resource package. */
+ charge_type?: string;
+ /** @description Consumption amount, accurate to two decimal places. */
+ amount?: string;
+ /** @description Consumable resource bundle type (only present when charge_type is "unit"). One of "video", "image" or "audio". */
+ package_type?: string;
+ }[];
+ };
+ /** @description Response returned when querying Kling 3.0 Turbo tasks by ID. */
+ KlingV2QueryTaskResponse: {
+ /** @description Error code. 0 indicates success. */
+ code?: number;
+ /** @description Error message. */
+ message?: string;
+ /** @description Request ID generated by the system. */
+ request_id?: string;
+ /** @description Tasks matching the query. */
+ data?: components["schemas"]["KlingV2Task"][];
+ };
KlingVideoExtendRequest: {
/** @description The ID of the video to be extended. Supports videos generated by text-to-video, image-to-video, and previous video extension operations. Cannot exceed 3 minutes total duration after extension. */
video_id?: string;
@@ -8366,6 +9161,74 @@ export interface components {
* @enum {string}
*/
BFLStatus: "Task not found" | "Pending" | "Request Moderated" | "Content Moderated" | "Ready" | "Error";
+ /** @description Request body for the BFL Flux Tools VTO v1 virtual try-on API. */
+ BFLVtoV1Request: {
+ /**
+ * @description Text prompt for VTO generation.
+ * @example TRY-ON: The person of image 1 wearing the garments of image 2.
+ */
+ prompt: string;
+ /** @description Person image (maps internally to input_image). */
+ person: string;
+ /** @description Image of one or more garments (maps internally to input_image_2). */
+ garment: string;
+ /**
+ * @description Optional seed for reproducibility.
+ * @example 42
+ */
+ seed?: number;
+ /**
+ * @description Tolerance level for input and output moderation. Between 0 and 5 for public use.
+ * @default 2
+ */
+ safety_tolerance: number;
+ /**
+ * @description Output format for the generated image.
+ * @default jpeg
+ */
+ output_format: components["schemas"]["BFLOutputFormat"];
+ /**
+ * Format: uri
+ * @description URL to receive webhook notifications.
+ */
+ webhook_url?: string;
+ /** @description Optional secret for webhook signature verification. */
+ webhook_secret?: string;
+ };
+ /** @description Request body for the BFL Flux Tools Erase v1 object removal API. */
+ BFLEraseV1Request: {
+ /** @description Base64-encoded input image or HTTP(S) image URL. */
+ image: string;
+ /** @description Base64-encoded black/white mask or HTTP(S) image URL. White pixels indicate the object to remove; black pixels are preserved. Must have the same dimensions as the input image. */
+ mask: string;
+ /**
+ * @description Number of pixels to dilate the mask by before removal. Dilation helps cover object edges. Maximum is 25 pixels.
+ * @default 10
+ */
+ dilate_pixels: number;
+ /**
+ * @description Optional seed for reproducibility.
+ * @example 42
+ */
+ seed?: number;
+ /**
+ * @description Tolerance level for input and output moderation. Between 0 and 5, 0 being most strict, 5 being least strict.
+ * @default 2
+ */
+ safety_tolerance: number;
+ /**
+ * @description Output format for the generated image.
+ * @default png
+ */
+ output_format: components["schemas"]["BFLOutputFormat"];
+ /**
+ * Format: uri
+ * @description URL to receive webhook notifications.
+ */
+ webhook_url?: string;
+ /** @description Optional secret for webhook signature verification. */
+ webhook_secret?: string;
+ };
/** @description Request body for the BFL Flux 2 Pro image generation API. */
BFLFlux2ProGenerateRequest: {
/** @description Text description of the image to generate. */
@@ -8733,7 +9596,7 @@ export interface components {
* OutputFormat
* @enum {string}
*/
- BFLOutputFormat: "jpeg" | "png";
+ BFLOutputFormat: "jpeg" | "png" | "webp";
/** ValidationError */
BFLValidationError: {
/** Location */
@@ -10424,10 +11287,10 @@ export interface components {
request?: components["schemas"]["LumaGenerationRequest"] | components["schemas"]["LumaImageGenerationRequest"] | components["schemas"]["LumaUpscaleVideoGenerationRequest"] | components["schemas"]["LumaAudioGenerationRequest"];
};
/**
- * @description Output aspect ratio
+ * @description Output aspect ratio. The ray-3.2 video models support the subset 9:16, 3:4, 1:1, 4:3, 16:9, 21:9.
* @enum {string}
*/
- LumaAgentsAspectRatio: "3:1" | "2:1" | "16:9" | "3:2" | "1:1" | "2:3" | "9:16" | "1:2" | "1:3";
+ LumaAgentsAspectRatio: "3:1" | "2:1" | "21:9" | "16:9" | "3:2" | "4:3" | "1:1" | "3:4" | "2:3" | "9:16" | "1:2" | "1:3";
/**
* @description Style preset
* @enum {string}
@@ -10439,10 +11302,10 @@ export interface components {
*/
LumaAgentsOutputFormat: "png" | "jpeg";
/**
- * @description The kind of generation to perform
+ * @description The kind of generation to perform. image/image_edit are produced by the uni-1 / uni-1-max models; video/video_edit/video_reframe are produced by the ray-3.2 model.
* @enum {string}
*/
- LumaAgentsGenerationType: "image" | "image_edit";
+ LumaAgentsGenerationType: "image" | "image_edit" | "video" | "video_edit" | "video_reframe";
/**
* @description Current state of the generation
* @enum {string}
@@ -10453,14 +11316,16 @@ export interface components {
* @enum {string}
*/
LumaAgentsFailureCode: "content_moderated" | "generation_failed" | "budget_exhausted" | "output_not_found";
- /** @description Reference image for style/content guidance or guided generation */
+ /** @description Reference to an image or video. Used for style/content guidance, guided generation, video-edit/video-reframe sources, and guide keyframes. Provide exactly one of generation_id, url, or data. */
LumaAgentsImageRef: {
- /** @description Base64-encoded image data */
+ /** @description Base64-encoded image or video data */
data?: string;
- /** @description MIME type (e.g. image/jpeg). Required with data. */
+ /** @description MIME type. Required with data (and with url for video sources, e.g. video/mp4 on video_edit / video_reframe). */
media_type?: string;
- /** @description Publicly accessible image URL */
+ /** @description Publicly accessible image or video URL */
url?: string;
+ /** @description UUID of a prior completed generation to reuse as the source. Used by ray-3.2 video_edit / video_reframe. */
+ generation_id?: string;
};
/** @description The Luma Agents generation request object */
LumaAgentsGenerationRequest: {
@@ -10469,15 +11334,40 @@ export interface components {
aspect_ratio?: components["schemas"]["LumaAgentsAspectRatio"];
/** @description Reference images for style/content guidance. Up to 9 for type 'image', up to 8 for type 'image_edit'. */
image_ref?: components["schemas"]["LumaAgentsImageRef"][];
- /** @description Model to use */
+ /** @description Model to use. uni-1 / uni-1-max for image generation, ray-3.2 for video generation, editing, and reframing. */
model?: string;
output_format?: components["schemas"]["LumaAgentsOutputFormat"];
source?: components["schemas"]["LumaAgentsImageRef"];
style?: components["schemas"]["LumaAgentsStyle"];
type?: components["schemas"]["LumaAgentsGenerationType"];
+ video?: components["schemas"]["LumaAgentsVideoOptions"];
/** @description Enable web search grounding */
web_search?: boolean;
};
+ /**
+ * @description Output resolution for ray-3.2 video. Defaults to 720p. 360p is the draft tier. HDR requires 720p or 1080p.
+ * @enum {string}
+ */
+ LumaAgentsVideoResolution: "360p" | "540p" | "720p" | "1080p";
+ /**
+ * @description Clip duration for ray-3.2 video / video_edit. Defaults to 5s. HDR generation (type video) is restricted to 5s.
+ * @enum {string}
+ */
+ LumaAgentsVideoDuration: "5s" | "10s";
+ /** @description Video output settings for ray-3.2. Only the fields that affect validation and billing are modelled here; additional fields (edit controls, end_frame, loop, source_position) are forwarded to Luma untouched. */
+ LumaAgentsVideoOptions: {
+ resolution?: components["schemas"]["LumaAgentsVideoResolution"];
+ duration?: components["schemas"]["LumaAgentsVideoDuration"];
+ /** @description Render in HDR. Requires 720p/1080p. Rejected for video_reframe. */
+ hdr?: boolean;
+ /** @description Export an EXR file alongside the MP4. Requires hdr true. Rejected for video_reframe. */
+ exr_export?: boolean;
+ /** @description Loop the generated clip. Create-only (type video). */
+ loop?: boolean;
+ start_frame?: components["schemas"]["LumaAgentsImageRef"];
+ /** @description Guide-frame images. A single keyframe makes a type "video" request a single-keyframe extend, which always bills as one 5s block. */
+ keyframes?: components["schemas"]["LumaAgentsImageRef"][];
+ };
/** @description A generated output entry */
LumaAgentsGenerationOutput: {
/** @description Media type (e.g. image) */
@@ -10887,6 +11777,68 @@ export interface components {
/** @description Task ID */
id?: string;
};
+ /** @description Request to edit an input video into a new video using Runway's API. */
+ RunwayVideoToVideoRequest: {
+ /** @description Model to use for generation */
+ model: components["schemas"]["RunwayVideoToVideoModelEnum"];
+ /** @description A non-empty string up to 1000 characters describing what should appear in the output. */
+ promptText: string;
+ /** @description The input video to edit (HTTPS URL, Runway upload URI, or data URI). Must be 30 seconds or shorter. */
+ videoUri: string;
+ /** @description Timed guidance images placed at specific points in the input video. Up to 5 keyframes. */
+ keyframes?: components["schemas"]["RunwayVideoToVideoKeyframe"][];
+ /** @description A list of up to 5 image keyframes for guiding the edit at specific points in the video. */
+ promptImage?: components["schemas"]["RunwayVideoToVideoPromptImage"][];
+ /**
+ * Format: int64
+ * @description Random seed for generation.
+ */
+ seed?: number;
+ /** @description Settings that affect the behavior of the content moderation system. */
+ contentModeration?: components["schemas"]["RunwayContentModeration"];
+ };
+ /** @description Timed guidance image placed at a specific point in the input video. */
+ RunwayVideoToVideoKeyframe: {
+ /** @description A HTTPS URL, Runway or data URI containing an encoded image. */
+ uri: string;
+ /** @description Absolute timestamp in seconds from the start of the input video when this guidance image should apply. */
+ seconds: number;
+ } | {
+ /** @description A HTTPS URL, Runway or data URI containing an encoded image. */
+ uri: string;
+ /** @description Position as a fraction [0.0, 1.0] of the input video duration when this guidance image should apply. */
+ at: number;
+ };
+ /** @description An image keyframe for guiding the edit at a specific point in the video. */
+ RunwayVideoToVideoPromptImage: {
+ /** @description A HTTPS URL, Runway or data URI containing an encoded image. */
+ uri: string;
+ position: components["schemas"]["RunwayVideoToVideoPromptImagePosition"];
+ };
+ /** @description The position in the output video where the image should apply. */
+ RunwayVideoToVideoPromptImagePosition: ("first" | "last") | {
+ /** @enum {string} */
+ type: "timestamp";
+ /** @description Absolute timestamp in seconds from the start of the output video. */
+ timestampSeconds: number;
+ } | {
+ /** @enum {string} */
+ type: "position";
+ /** @description Position as a fraction [0.0, 1.0] of the total video duration. */
+ positionPercentage: number;
+ };
+ /** @description Settings that affect the behavior of the content moderation system. */
+ RunwayContentModeration: {
+ /**
+ * @description When set to `low`, the content moderation system will be less strict about preventing generations that include recognizable public figures.
+ * @enum {string}
+ */
+ publicFigureThreshold?: "auto" | "low";
+ };
+ RunwayVideoToVideoResponse: {
+ /** @description Task ID */
+ id?: string;
+ };
RunwayTextToImageResponse: {
/** @description Task ID */
id?: string;
@@ -10919,6 +11871,11 @@ export interface components {
* @enum {string}
*/
RunwayModelEnum: "gen4_turbo" | "gen3a_turbo";
+ /**
+ * @description Available Runway models for video-to-video generation.
+ * @enum {string}
+ */
+ RunwayVideoToVideoModelEnum: "aleph2";
/** @description Represents an image with its position in the video sequence. */
RunwayPromptImageDetailedObject: {
/** @description A HTTPS URL or data URI containing an encoded image. */
@@ -12575,39 +13532,90 @@ export interface components {
authors?: string[];
};
Rodin3DGenerateRequest: {
- /** @description The reference images to generate 3D Assets. */
- images: string;
- /** @description Text prompt used by the upstream Rodin API. Required by upstream for text-to-3D requests (no images uploaded); optional for image-to-3D requests where it acts as additional guidance. */
+ /** @description Images to be used in generation, up to 5 images. As the form data request will preserve the order of the images, the first image will be the image for material generation. For Image-to-3D generation: required (one or more images are needed, maximum 5 images). For Text-to-3D generation: null. */
+ images?: string;
+ /** @description A textual prompt to guide the model generation. For Image-to-3D generation: optional (if not provided, an AI-generated prompt based on the provided images will be used). For Text-to-3D generation: required. */
prompt?: string;
- /** @description Seed. */
+ /** @description Default is false. If true, the original transparency channel of the images will be used when processing the image. */
+ use_original_alpha?: boolean;
+ condition_mode?: components["schemas"]["RodinConditionModeType"];
+ /** @description Optional. A seed value for randomization in the mesh generation, ranging from 0 to 65535 (both inclusive). If not provided, the seed will be randomly generated. */
seed?: number;
- tier?: components["schemas"]["RodinTierType"];
+ geometry_file_format?: components["schemas"]["RodinGeometryFileFormatType"];
material?: components["schemas"]["RodinMaterialType"];
quality?: components["schemas"]["RodinQualityType"];
+ /** @description Optional. Customize poly count for generation, providing more accurate control over mesh face count. When mesh_mode = Raw: range from 500 to 1,000,000 (default 500,000). When mesh_mode = Quad: range from 1,000 to 200,000 (default 18,000). When this parameter is invoked, the `quality` parameter will not take effect. */
+ quality_override?: number;
+ tier?: components["schemas"]["RodinTierType"];
+ /** @description Optional. When generating the human-like model, this parameter controls the generation result to T/A pose. When true, your model will be either T pose or A pose. */
+ TAPose?: boolean;
+ /** @description Optional. This is a controlnet that controls the maximum size of the generated model. This array must contain 3 elements, Width (Y-axis), Height (Z-axis), and Length (X-axis), in this exact fixed sequence (y, z, x). */
+ bbox_condition?: number[];
mesh_mode?: components["schemas"]["RodinMeshModeType"];
- /** @description Optional list of upstream addon flags (e.g. "HighPack"). */
- addons?: string[];
+ /** @description Optional. Default is true. If true, the generated models will be simplified. This parameter takes effect when mesh_mode is set to Raw. */
+ mesh_simplify?: boolean;
+ /** @description Optional. Default is false. If true, the generated models will be smoothed (similar to Rodin Gen-1). This parameter takes effect when mesh_mode is set to Quad. */
+ mesh_smooth?: boolean;
+ /** @description Optional. The default is []. Possible value is `HighPack`. By selecting HighPack: generate 4K resolution texture instead of the default 2K. If Quad mode, the number of faces will be ~16 times the number of faces selected in the `quality` parameter. */
+ addons?: components["schemas"]["RodinAddonType"][];
+ /** @description Optional. Default is false. If true, an additional high-quality render image will be provided in the download list. */
+ preview_render?: boolean;
+ /** @description Optional. Default is false. If true, high-quality texture will be provided. */
+ hd_texture?: boolean;
+ /** @description Optional. Default is false. If true, this parameter applies images preprocessing to remove lighting information from textures. */
+ texture_delight?: boolean;
+ texture_mode?: components["schemas"]["RodinTextureModeType"];
+ /** @description Optional. Default is false. If true, micro detail scale is applied. This parameter is only available in the Gen-2.5-Extreme-High tier. */
+ is_micro?: boolean;
+ /** @description Optional. Default is false. If true, this parameter will determine whether the generated model is symmetric. */
+ is_symmetric?: boolean;
+ geometry_instruct_mode?: components["schemas"]["RodinGeometryInstructModeType"];
};
/**
- * @description Rodin Tier para options
+ * @description Tier of generation. The default value is Regular. Sketch: fast generation with basic details, suitable for initial concepts. Regular: balanced quality and speed, ideal for general use. Detail: enhanced details compared to Regular, recommended for intricate results (longer processing time). Smooth: clearer and sharper output than Regular, with slightly longer processing time. Set the value to `Gen-2` to invoke Gen-2 generation. Use the `Gen-2.5-*` values to invoke Gen-2.5 generation: Gen-2.5-Extreme-Low (quick simple assets), Gen-2.5-Low (clean assets and small hardsurface props), Gen-2.5-Medium (moderately complex models), Gen-2.5-High (high-quality assets with richer structural representation and smooth surfaces), Gen-2.5-Extreme-High (high-frequency detail reproduction).
* @enum {string}
*/
- RodinTierType: "Regular" | "Sketch" | "Detail" | "Smooth";
+ RodinTierType: "Regular" | "Sketch" | "Detail" | "Smooth" | "Gen-2" | "Gen-2.5-Extreme-Low" | "Gen-2.5-Low" | "Gen-2.5-Medium" | "Gen-2.5-High" | "Gen-2.5-Extreme-High";
/**
- * @description Rodin Material para options
+ * @description Optional. The material type. Default is PBR. PBR: Physically Based Materials, including base color texture, metallicness texture, normal texture and roughness texture, providing high realism and physically accurate behavior over dynamic lighting. Shaded: only base color texture with baked lighting, providing stylized visuals. All: both PBR and Shaded will be delivered. None: asset without material.
* @enum {string}
*/
- RodinMaterialType: "PBR" | "Shaded";
+ RodinMaterialType: "PBR" | "Shaded" | "All" | "None";
/**
- * @description Rodin Quality para options
+ * @description Optional. The face count of the generated model. Default is medium.
* @enum {string}
*/
RodinQualityType: "extra-low" | "low" | "medium" | "high";
/**
- * @description Rodin Mesh_Mode para options
+ * @description Optional. It controls the type of faces of generated models. Default is Quad. The Raw mode generates triangular face models. The Quad mode generates quadrilateral face models. When its value is Raw, `quality` will be fixed to medium and `addons` will be fixed to []. For Rodin Sketch tier, only triangular faces can be generated.
* @enum {string}
*/
RodinMeshModeType: "Quad" | "Raw";
+ /**
+ * @description Useful only for multi-image 3D generation. Optional. Chooses the mode of the multi-image generation. Default is concat. For `fuse` mode (uploading images of multiple objects), fuse mode will extract and fuse all the features of all the objects from the images for generation. For `concat` mode (uploading images of a single object), concat mode will inform the Rodin model to expect these images to be multi-view images of a single object.
+ * @enum {string}
+ */
+ RodinConditionModeType: "fuse" | "concat";
+ /**
+ * @description Optional. The format of the output geometry file. Default is glb.
+ * @enum {string}
+ */
+ RodinGeometryFileFormatType: "glb" | "usdz" | "fbx" | "obj" | "stl";
+ /**
+ * @description Optional. Higher values invest more thinking effort and produce better results, at the cost of longer generation time.
+ * @enum {string}
+ */
+ RodinTextureModeType: "legacy" | "extreme-low" | "low" | "medium" | "high";
+ /**
+ * @description Optional. Default is `faithful`. The Creative mode enhances generative robustness while ensuring output consistency, allowing for more flexible and creative generation while maintaining quality and consistency across outputs. Available for Gen-2.5-Medium and Gen-2.5-High tiers.
+ * @enum {string}
+ */
+ RodinGeometryInstructModeType: "faithful" | "creative";
+ /**
+ * @description Possible value is `HighPack`. By selecting HighPack: generate 4K resolution texture instead of the default 2K. If Quad mode, the number of faces will be ~16 times the number of faces selected in the `quality` parameter. Additional 1 credit per generation.
+ * @enum {string}
+ */
+ RodinAddonType: "HighPack";
Rodin3DCheckStatusRequest: {
/** @description subscription from generate endpoint */
subscription_key: string;
@@ -12617,13 +13625,15 @@ export interface components {
task_uuid: string;
};
Rodin3DGenerateResponse: {
- /** @description message */
+ /** @description Error message, if any. Possible values include NO_ACTIVE_SUBSCRIPTION, SUBSCRIPTION_PLAN_TOO_LOW, INSUFFICIENT_FUND, INVALID_REQUEST, USER_NOT_FOUND, GROUP_NOT_FOUND, PERMISSION_DENIED, UNKNOWN. */
+ error?: string | null;
+ /** @description Success message or detailed error information. */
message?: string;
- /** @description prompt */
+ /** @description Echoed prompt (when applicable). */
prompt?: string;
- /** @description Time */
+ /** @description Submission timestamp. */
submit_time?: string;
- /** @description Task UUID */
+ /** @description Task UUID. Use this for status/download requests. */
uuid?: string;
jobs?: components["schemas"]["RodinGenerateJobsData"];
};
@@ -14446,7 +15456,7 @@ export interface components {
* @description The ID of the model to call. Available models include seedance-1-5-pro-251215, seedance-1-0-pro-250528, seedance-1-0-pro-fast-251015, seedance-1-0-lite-t2v-250428, seedance-1-0-lite-i2v-250428
* @enum {string}
*/
- model: "seedance-1-5-pro-251215" | "seedance-1-0-pro-250528" | "seedance-1-0-lite-t2v-250428" | "seedance-1-0-lite-i2v-250428" | "seedance-1-0-pro-fast-251015" | "dreamina-seedance-2-0-260128" | "dreamina-seedance-2-0-fast-260128";
+ model: "seedance-1-5-pro-251215" | "seedance-1-0-pro-250528" | "seedance-1-0-lite-t2v-250428" | "seedance-1-0-lite-i2v-250428" | "seedance-1-0-pro-fast-251015" | "dreamina-seedance-2-0-260128" | "dreamina-seedance-2-0-fast-260128" | "dreamina-seedance-2-0-mini";
/** @description The input content for the model to generate a video */
content: components["schemas"]["BytePlusVideoGenerationContent"][];
/**
@@ -14473,7 +15483,7 @@ export interface components {
* Note: Seedance 2.0 & 2.0 fast do not support 1080p.
* @enum {string}
*/
- resolution?: "480p" | "720p" | "1080p";
+ resolution?: "480p" | "720p" | "1080p" | "4k";
/**
* @description Aspect ratio of the generated video. Seedance 2.0 & 2.0 fast, 1.5 pro default: adaptive.
* @enum {string}
@@ -14594,6 +15604,679 @@ export interface components {
total_tokens?: number;
};
};
+ /** @description Multipart upload payload for POST /api/v3/files. The binary `file` is required; everything else mirrors the upstream optional fields. See https://docs.byteplus.com/en/docs/ModelArk/1870405. */
+ BytePlusFileUploadRequest: {
+ /**
+ * Format: binary
+ * @description The binary file to upload.
+ */
+ file: string;
+ /**
+ * @description Purpose of the uploaded file. `user_data` is a general-purpose value
+ * and the only one currently documented by BytePlus.
+ * @default user_data
+ */
+ purpose: string;
+ /**
+ * @description Unix timestamp (seconds, UTC) at which the file should be expired.
+ * Range: [now + 86400, now + 2592000] (1 day to 30 days).
+ * Default: now + 604800 (7 days).
+ */
+ expire_at?: number;
+ preprocess_configs?: components["schemas"]["BytePlusFilePreprocessConfigs"];
+ };
+ /** @description Preprocessing rules applied to the uploaded file by file type. */
+ BytePlusFilePreprocessConfigs: {
+ video?: {
+ /**
+ * Format: float
+ * @description Number of frames per second sampled from the video at upload
+ * time. Higher values capture more detail but consume more tokens
+ * during inference (range [10k, 80k] tokens per video).
+ * @default 1
+ */
+ fps: number | null;
+ /**
+ * @description Video-understanding model ID or endpoint ID whose frame-sampling
+ * strategy should be applied during preprocessing. If omitted, the
+ * pre-`seed-1.8` default strategy is used.
+ */
+ model?: string;
+ } | null;
+ } | null;
+ /** @description File object returned by POST /api/v3/files and GET /api/v3/files/{id}. See https://docs.byteplus.com/en/docs/ModelArk/1873424. */
+ BytePlusFile: {
+ /** @description Fixed to `file`. */
+ object?: string;
+ /** @description The unique ID of the file. */
+ id?: string;
+ /** @description The purpose of the file. */
+ purpose?: string;
+ /** @description File size in bytes. Returned only when status is `active`. */
+ bytes?: number;
+ /** @description Unix timestamp (seconds) when the file was uploaded. */
+ created_at?: number;
+ /** @description Unix timestamp (seconds) when the file expires. */
+ expire_at?: number;
+ /** @description MIME type of the file. Returned only when status is `active`. */
+ mime_type?: string;
+ /**
+ * @description Processing status of the file.
+ * @enum {string}
+ */
+ status?: "processing" | "active" | "failed";
+ /** @description Error details returned only when status is `failed`. */
+ error?: {
+ /** @description Error code. */
+ code?: string;
+ /** @description Error description. */
+ message?: string;
+ } | null;
+ preprocess_configs?: components["schemas"]["BytePlusFilePreprocessConfigs"];
+ };
+ /** @description One entry in the `input` array of a Responses request. Discriminated by the `type` field. See https://docs.byteplus.com/en/docs/ModelArk/1585128. */
+ BytePlusResponseInputItem: components["schemas"]["BytePlusResponseInputMessage"] | components["schemas"]["BytePlusResponseInputFunctionCall"] | components["schemas"]["BytePlusResponseInputFunctionCallOutput"] | components["schemas"]["BytePlusResponseInputReasoning"];
+ /** @description A message sent to the model (or a stored prior message when `status` is set). Role precedence: developer/system > user; assistant messages represent prior model output. */
+ BytePlusResponseInputMessage: {
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "message";
+ /** @enum {string} */
+ role: "user" | "system" | "assistant" | "developer";
+ content: string | components["schemas"]["BytePlusResponseMessageContent"][];
+ /** @description Enables Continuation mode. Set the last message's role to `assistant` and `partial` to true; the model continues from its existing content. */
+ partial?: boolean;
+ /**
+ * @description Status of a previously stored message. Only used when echoing previous inputs back to the model.
+ * @enum {string}
+ */
+ status?: "in_progress" | "completed" | "incomplete";
+ } & {
+ [key: string]: unknown;
+ };
+ /** @description One content item inside a message. Discriminated by `type`: `input_text` for text, `input_image` for images, `input_video` for videos, `input_file` for PDF/file uploads. File-backed types may reference a Files API id via `file_id`. */
+ BytePlusResponseMessageContent: components["schemas"]["BytePlusResponseInputText"] | components["schemas"]["BytePlusResponseInputImage"] | components["schemas"]["BytePlusResponseInputVideo"] | components["schemas"]["BytePlusResponseInputFile"];
+ BytePlusResponseInputText: {
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "input_text";
+ text: string;
+ /** @description Translation-scenario configuration. Only supported by seed-translation-250728. */
+ translation_options?: {
+ source_language?: string;
+ target_language: string;
+ } & {
+ [key: string]: unknown;
+ };
+ } & {
+ [key: string]: unknown;
+ };
+ BytePlusResponseInputImage: {
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "input_image";
+ /** @description ID of a file uploaded via the Files API. Must be `active`. */
+ file_id?: string;
+ /** @description Image URL or `data:image/...;base64,...` payload. */
+ image_url?: string;
+ /**
+ * @default auto
+ * @enum {string}
+ */
+ detail: "high" | "low" | "auto";
+ /** @description Optional pixel-count bounds. Overrides `detail` when set. Image pixel count must stay in [196, 36_000_000]. */
+ image_pixel_limit?: ({
+ max_pixels?: number;
+ min_pixels?: number;
+ } & {
+ [key: string]: unknown;
+ }) | null;
+ } & {
+ [key: string]: unknown;
+ };
+ BytePlusResponseInputVideo: {
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "input_video";
+ /** @description ID of a file uploaded via the Files API. Must be `active`. */
+ file_id?: string;
+ /** @description Video URL or `data:video/...;base64,...` payload. */
+ video_url?: string;
+ /**
+ * Format: float
+ * @description Frames-per-second extracted from the video.
+ */
+ fps?: number;
+ } & {
+ [key: string]: unknown;
+ };
+ /** @description File input (currently PDF only). Provide exactly one of `file_id`, `file_data`, or `file_url`. `filename` is required with `file_data`. */
+ BytePlusResponseInputFile: {
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "input_file";
+ /** @description ID of a file uploaded via the Files API. Must be `active`. */
+ file_id?: string;
+ /** @description Base64-encoded file (single file <= 50 MB). */
+ file_data?: string;
+ /** @description Required when `file_data` is set. */
+ filename?: string;
+ /** @description Publicly accessible URL (single file <= 50 MB). */
+ file_url?: string;
+ } & {
+ [key: string]: unknown;
+ };
+ /** @description A tool/function invocation produced by the model in a prior turn. */
+ BytePlusResponseInputFunctionCall: {
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "function_call";
+ /** @description JSON-encoded string of the function arguments. */
+ arguments: string;
+ /** @description Unique ID of the tool call generated by the model. */
+ call_id: string;
+ name: string;
+ status?: string;
+ } & {
+ [key: string]: unknown;
+ };
+ /** @description Output returned by a tool, paired with the function_call via call_id. */
+ BytePlusResponseInputFunctionCallOutput: {
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "function_call_output";
+ call_id: string;
+ output: string;
+ status?: string;
+ } & {
+ [key: string]: unknown;
+ };
+ /** @description Chain-of-thought block. Used both as input (manual reasoning injection for seed-1-8, seed-2-0, deepseek-v3-2) and as a response output item. As input, prefer `previous_response_id` in multi-turn conversations. */
+ BytePlusResponseInputReasoning: {
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "reasoning";
+ id?: string;
+ /** @enum {string} */
+ status?: "in_progress" | "completed" | "incomplete";
+ summary?: ({
+ /**
+ * @default summary_text
+ * @enum {string}
+ */
+ type: "summary_text";
+ text?: string;
+ } & {
+ [key: string]: unknown;
+ })[];
+ /** @description Original (un-summarized) reasoning content. Returned on output items; not used on input. */
+ content?: ({
+ /**
+ * @default reasoning_text
+ * @enum {string}
+ */
+ type: "reasoning_text";
+ text?: string;
+ } & {
+ [key: string]: unknown;
+ })[];
+ /** @description Encrypted+compressed original reasoning content. Returned only when `reasoning.encrypted_content` is in the request's `include` list. Supported from seed-2-0-pro-260328. */
+ encrypted_content?: string;
+ } & {
+ [key: string]: unknown;
+ };
+ BytePlusResponseCreateRequest: {
+ /** @description Model ID or Endpoint ID. See https://docs.byteplus.com/en/docs/ModelArk/1330310 for the model list and https://docs.byteplus.com/en/docs/ModelArk/1099522 for Endpoint IDs. */
+ model: string;
+ /** @description Text content or list of input items provided to the model. */
+ input: string | components["schemas"]["BytePlusResponseInputItem"][];
+ /** @description System/developer message prepended as the first instruction. Not compatible with `caching` — if `caching.type` is `enabled`, setting `instructions` returns an error. */
+ instructions?: string | null;
+ /** @description ID of the previous response, used to continue a multi-turn conversation. Insert ~100ms between requests to avoid failures. */
+ previous_response_id?: string | null;
+ /** @description Unix timestamp (seconds, UTC) at which the stored response and cache expire. Range (creation_time, creation_time + 604800]. Default: creation_time + 259200 (3 days). */
+ expire_at?: number;
+ /** @description Maximum output tokens (response + chain-of-thought). */
+ max_output_tokens?: number | null;
+ /** @description Controls deep-thinking mode. */
+ thinking?: {
+ /**
+ * @description `enabled`: always reason before responding.
+ * `disabled`: respond without additional reasoning.
+ * `auto`: model decides per-query.
+ * @enum {string}
+ */
+ type?: "enabled" | "disabled" | "auto";
+ } & {
+ [key: string]: unknown;
+ };
+ /** @description Limits the workload of deep thinking. */
+ reasoning?: {
+ /**
+ * @description `minimal` disables thinking entirely. With `thinking.type =
+ * disabled`, only `minimal` is allowed.
+ * @enum {string}
+ */
+ effort?: "minimal" | "low" | "medium" | "high";
+ } & {
+ [key: string]: unknown;
+ };
+ /** @description Additional output fields to include. Currently supported: `reasoning.encrypted_content` (encrypted+compressed reasoning for manual multi-turn reuse). */
+ include?: string[];
+ /** @description Context-cache configuration. */
+ caching?: {
+ /** @enum {string} */
+ type?: "enabled" | "disabled";
+ /**
+ * @description When true, only create the public prefix cache; the model does not respond.
+ * @default false
+ */
+ prefix: boolean;
+ } & {
+ [key: string]: unknown;
+ };
+ /**
+ * @description When true, the response is persisted and retrievable by ID for multi-turn use.
+ * @default true
+ */
+ store: boolean | null;
+ /**
+ * Format: float
+ * @default 1
+ */
+ temperature: number | null;
+ /**
+ * Format: float
+ * @default 0.7
+ */
+ top_p: number | null;
+ /** @description Output-format configuration. */
+ text?: {
+ format?: components["schemas"]["BytePlusResponseTextFormat"];
+ } & {
+ [key: string]: unknown;
+ };
+ tools?: components["schemas"]["BytePlusResponseTool"][];
+ /** @description Tool-selection mode. Only seed-1-6 models support this field. */
+ tool_choice?: ("none" | "auto" | "required") | components["schemas"]["BytePlusResponseToolChoiceObject"];
+ max_tool_calls?: number;
+ context_management?: components["schemas"]["BytePlusResponseContextManagement"];
+ } & {
+ [key: string]: unknown;
+ };
+ /** @description A tool the model may invoke. Currently only `function` is supported. */
+ BytePlusResponseTool: {
+ /**
+ * @default function
+ * @enum {string}
+ */
+ type: "function";
+ name: string;
+ description?: string;
+ /** @description JSON Schema describing the function's parameters. */
+ parameters: {
+ [key: string]: unknown;
+ };
+ } & {
+ [key: string]: unknown;
+ };
+ /** @description Forces the model to call a specific tool. When `type` is `function`, `name` is required. */
+ BytePlusResponseToolChoiceObject: {
+ /** @enum {string} */
+ type: "function";
+ name?: string;
+ } & {
+ [key: string]: unknown;
+ };
+ /** @description Text-output format discriminated by `type`. `text` returns natural language, `json_object` returns a free-form JSON object, `json_schema` constrains output to a caller-supplied JSON Schema. */
+ BytePlusResponseTextFormat: components["schemas"]["BytePlusResponseTextFormatText"] | components["schemas"]["BytePlusResponseTextFormatJSONObject"] | components["schemas"]["BytePlusResponseTextFormatJSONSchema"];
+ BytePlusResponseTextFormatText: {
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "text";
+ } & {
+ [key: string]: unknown;
+ };
+ BytePlusResponseTextFormatJSONObject: {
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "json_object";
+ } & {
+ [key: string]: unknown;
+ };
+ BytePlusResponseTextFormatJSONSchema: {
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "json_schema";
+ /** @description Caller-defined name for the JSON structure. */
+ name: string;
+ /** @description JSON Schema the model output must conform to. */
+ schema: {
+ [key: string]: unknown;
+ };
+ /** @description Hint the model uses when generating the response. */
+ description?: string | null;
+ /** @default false */
+ strict: boolean | null;
+ } & {
+ [key: string]: unknown;
+ };
+ /** @description Context-management strategies (`clear_thinking`, `clear_tool_uses`) applied to keep the context window manageable. */
+ BytePlusResponseContextManagement: {
+ edits?: components["schemas"]["BytePlusResponseContextEdit"][];
+ } & {
+ [key: string]: unknown;
+ };
+ /** @description A single context-edit strategy, discriminated by `type`. */
+ BytePlusResponseContextEdit: components["schemas"]["BytePlusResponseContextEditClearThinking"] | components["schemas"]["BytePlusResponseContextEditClearToolUses"];
+ /** @description Clears chain-of-thought content per the `keep` strategy. */
+ BytePlusResponseContextEditClearThinking: {
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "clear_thinking";
+ keep: ({
+ /**
+ * @default thinking_turns
+ * @enum {string}
+ */
+ type: "thinking_turns";
+ /**
+ * @description Retain chain-of-thought for the most recent N turns.
+ * @default 1
+ */
+ value: number;
+ } & {
+ [key: string]: unknown;
+ }) | "all";
+ } & {
+ [key: string]: unknown;
+ };
+ /** @description Clears tool-call content when the conversation exceeds a threshold. */
+ BytePlusResponseContextEditClearToolUses: {
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "clear_tool_uses";
+ keep: {
+ /**
+ * @default tool_uses
+ * @enum {string}
+ */
+ type: "tool_uses";
+ /**
+ * @description Retain tool-call content for the most recent N turns.
+ * @default 3
+ */
+ value: number;
+ } & {
+ [key: string]: unknown;
+ };
+ /** @description Tool names that are never cleared. */
+ exclude_tools?: string[];
+ /**
+ * @description Whether to clear tool-call parameters.
+ * @default false
+ */
+ clear_tool_input: boolean;
+ trigger: {
+ /**
+ * @default tool_uses
+ * @enum {string}
+ */
+ type: "tool_uses";
+ /** @description Trigger cleanup when tool-call turns reach N. */
+ value: number;
+ } & {
+ [key: string]: unknown;
+ };
+ } & {
+ [key: string]: unknown;
+ };
+ /** @description Non-streaming response body returned by POST /api/v3/responses. See https://docs.byteplus.com/en/docs/ModelArk/1783703. */
+ BytePlusResponseObject: {
+ /** @description Unique ID of the response. Use as `previous_response_id` to continue the conversation. */
+ id: string;
+ /**
+ * @default response
+ * @enum {string}
+ */
+ object: "response";
+ /** @description Unix timestamp (seconds) when the response was created. */
+ created_at: number;
+ /** @description Model ID that generated the response. */
+ model: string;
+ /** @enum {string} */
+ status: "in_progress" | "completed" | "incomplete" | "failed" | "cancelled";
+ /**
+ * @description TPM-guarantee-package usage. `default` means none.
+ * @enum {string}
+ */
+ service_tier?: "default";
+ error?: components["schemas"]["BytePlusResponseError"];
+ /** @description Populated when `status` is `incomplete`. */
+ incomplete_details?: ({
+ /** @description e.g. `max_output_tokens`, `content_filter`. */
+ reason?: string;
+ } & {
+ [key: string]: unknown;
+ }) | null;
+ /** @description Echo of the request's `instructions` field. */
+ instructions?: string | null;
+ previous_response_id?: string | null;
+ max_output_tokens?: number | null;
+ max_tool_calls?: number | null;
+ /** Format: float */
+ temperature?: number | null;
+ /** Format: float */
+ top_p?: number | null;
+ store?: boolean | null;
+ stream?: boolean | null;
+ /** @description Echo of the request's `text` field. */
+ text?: {
+ format?: components["schemas"]["BytePlusResponseTextFormat"];
+ } & {
+ [key: string]: unknown;
+ };
+ tools?: components["schemas"]["BytePlusResponseTool"][];
+ tool_choice?: ("none" | "auto" | "required") | components["schemas"]["BytePlusResponseToolChoiceObject"];
+ thinking?: {
+ /** @enum {string} */
+ type?: "enabled" | "disabled" | "auto";
+ } & {
+ [key: string]: unknown;
+ };
+ reasoning?: {
+ /** @enum {string} */
+ effort?: "minimal" | "low" | "medium" | "high";
+ } & {
+ [key: string]: unknown;
+ };
+ caching?: {
+ /** @enum {string} */
+ type?: "enabled" | "disabled";
+ prefix?: boolean;
+ } & {
+ [key: string]: unknown;
+ };
+ context_management?: components["schemas"]["BytePlusResponseAppliedContextManagement"];
+ /** @description Ordered output items produced by the model. */
+ output: components["schemas"]["BytePlusResponseOutputItem"][];
+ usage?: components["schemas"]["BytePlusResponseUsage"];
+ /** @description Unix timestamp (seconds) when the stored response expires. */
+ expire_at?: number;
+ metadata?: {
+ [key: string]: unknown;
+ } | null;
+ };
+ /** @description Error details. Null when the response succeeded. */
+ BytePlusResponseError: {
+ code: string;
+ message: string;
+ } | null;
+ /** @description One item in a response's `output` array. Discriminated by `type`: `message` for assistant messages, `function_call` for tool invocations, `reasoning` for chain-of-thought blocks. Mirrors the input item shapes the model can read back via `previous_response_id`. */
+ BytePlusResponseOutputItem: components["schemas"]["BytePlusResponseOutputMessage"] | components["schemas"]["BytePlusResponseInputFunctionCall"] | components["schemas"]["BytePlusResponseInputReasoning"];
+ /** @description An assistant message produced by the model. */
+ BytePlusResponseOutputMessage: {
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "message";
+ id: string;
+ /**
+ * @default assistant
+ * @enum {string}
+ */
+ role: "assistant";
+ /** @enum {string} */
+ status?: "in_progress" | "completed" | "incomplete";
+ /** @description True when this message is a continuation-mode partial reply. */
+ partial?: boolean;
+ content: components["schemas"]["BytePlusResponseOutputContent"][];
+ } & {
+ [key: string]: unknown;
+ };
+ /** @description One content block in an assistant message. `output_text` carries natural-language text and optional annotations; `refusal` carries a refusal message. */
+ BytePlusResponseOutputContent: components["schemas"]["BytePlusResponseOutputText"] | components["schemas"]["BytePlusResponseOutputRefusal"];
+ BytePlusResponseOutputText: {
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "output_text";
+ text: string;
+ annotations?: {
+ [key: string]: unknown;
+ }[];
+ } & {
+ [key: string]: unknown;
+ };
+ BytePlusResponseOutputRefusal: {
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "refusal";
+ refusal: string;
+ } & {
+ [key: string]: unknown;
+ };
+ /** @description Token-usage breakdown for billing and observability. */
+ BytePlusResponseUsage: {
+ /** @description Total tokens in the request. */
+ input_tokens: number;
+ /** @description Breakdown of input tokens (cache hits, etc). */
+ input_tokens_details?: {
+ /** @description Tokens served from the context cache. */
+ cached_tokens?: number;
+ } & {
+ [key: string]: unknown;
+ };
+ /** @description Total tokens generated by the model. */
+ output_tokens: number;
+ /** @description Breakdown of output tokens (reasoning, etc). */
+ output_tokens_details?: {
+ /** @description Tokens consumed by chain-of-thought. */
+ reasoning_tokens?: number;
+ } & {
+ [key: string]: unknown;
+ };
+ /** @description input_tokens + output_tokens. */
+ total_tokens: number;
+ /** @description Per-tool invocation counts. */
+ tool_usage?: {
+ /** @description Number of image-processing tool calls. */
+ image_process?: number;
+ /** @description Number of MCP tool calls. */
+ mcp?: number;
+ /** @description Number of web-search tool invocations. */
+ web_search?: number;
+ } & {
+ [key: string]: unknown;
+ };
+ /** @description Per-tool breakdown of sub-tool invocation counts. */
+ tool_usage_details?: {
+ /** @description e.g. `{"zoom":1,"point":1,"grounding":1}`. */
+ image_process?: {
+ [key: string]: unknown;
+ };
+ /** @description e.g. `{"mcp_server_tos":1,"mcp_server_tls":1}`. */
+ mcp?: {
+ [key: string]: unknown;
+ };
+ /** @description e.g. `{"toutiao":1,"moji":1,"search_engine":1}`. */
+ web_search?: {
+ [key: string]: unknown;
+ };
+ } & {
+ [key: string]: unknown;
+ };
+ };
+ /** @description Context-management strategies that were actually applied during this response. Unlike the request-side `BytePlusResponseContextManagement` (which configures strategies), this echoes the strategies the server invoked, with counts of what was cleared. */
+ BytePlusResponseAppliedContextManagement: {
+ applied_edits?: components["schemas"]["BytePlusResponseAppliedContextEdit"][];
+ } & {
+ [key: string]: unknown;
+ };
+ /** @description One applied context-edit, discriminated by `type`. */
+ BytePlusResponseAppliedContextEdit: components["schemas"]["BytePlusResponseAppliedContextEditClearThinking"] | components["schemas"]["BytePlusResponseAppliedContextEditClearToolUses"];
+ BytePlusResponseAppliedContextEditClearThinking: {
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "clear_thinking";
+ /** @description Number of reasoning turns that were removed. */
+ cleared_thinking_turns?: number;
+ } & {
+ [key: string]: unknown;
+ };
+ BytePlusResponseAppliedContextEditClearToolUses: {
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "clear_tool_uses";
+ /** @description Number of tool invocations that were removed. */
+ cleared_tool_uses?: number;
+ } & {
+ [key: string]: unknown;
+ };
+ BytePlusResponseReasoningSummaryPart: {
+ /**
+ * @default summary_text
+ * @enum {string}
+ */
+ type: "summary_text";
+ text?: string;
+ } & {
+ [key: string]: unknown;
+ };
SeedanceCreateVisualValidateSessionRequest: {
/** @description Optional human-readable label for the asset group that will be created by this verification. Stored locally and returned by seedanceListVisualValidationGroups so users can identify their groups in selectors. */
name?: string;
@@ -14696,10 +16379,16 @@ export interface components {
message: string;
};
SeedanceVirtualLibraryCreateAssetRequest: {
- /** @description Publicly accessible URL of the image asset to upload to the caller's virtual portrait library. */
+ /** @description Publicly accessible URL of the asset to upload to the caller's virtual portrait library. */
url: string;
/** @description Client-supplied content hash used as the per-customer dedup key. Re-submitting the same hash returns the existing asset id without re-uploading to BytePlus. */
hash: string;
+ /**
+ * @description BytePlus asset type. The AIGC virtual library accepts both Image and Video. Defaults to Image for backward compatibility with existing clients.
+ * @default Image
+ * @enum {string}
+ */
+ asset_type: "Image" | "Video";
};
SeedanceVirtualLibraryCreateAssetResponse: {
/** @description BytePlus-issued asset id. Clients poll seedanceGetAsset with this until status == Active. */
@@ -14710,7 +16399,7 @@ export interface components {
* @description The ID of the model to call
* @enum {string}
*/
- model: "wan2.5-t2v-preview" | "wan2.5-i2v-preview" | "wan2.6-t2v" | "wan2.6-i2v" | "wan2.6-r2v" | "wan2.7-i2v" | "wan2.7-t2v" | "wan2.7-r2v" | "wan2.7-videoedit" | "happyhorse-1.0-t2v" | "happyhorse-1.0-i2v" | "happyhorse-1.0-r2v" | "happyhorse-1.0-video-edit";
+ model: "wan2.5-t2v-preview" | "wan2.5-i2v-preview" | "wan2.6-t2v" | "wan2.6-i2v" | "wan2.6-r2v" | "wan2.7-i2v" | "wan2.7-t2v" | "wan2.7-r2v" | "wan2.7-videoedit" | "happyhorse-1.0-t2v" | "happyhorse-1.0-i2v" | "happyhorse-1.0-r2v" | "happyhorse-1.0-video-edit" | "happyhorse-1.1-t2v" | "happyhorse-1.1-i2v" | "happyhorse-1.1-r2v";
/** @description Enter basic information, such as prompt words, etc. */
input: {
/**
@@ -16716,6 +18405,1704 @@ export interface components {
/** @description A url to the generated video */
url?: string | null;
};
+ /** @description Request body for Anthropic Messages API (`/v1/messages`). Mirrors the upstream schema with strict typing only on fields the proxy reads (model, prompt extraction, streaming, billing). Other top-level fields (`tools`, `temperature`, `top_p`, `thinking`, `metadata`, etc.) pass through unchanged via `additionalProperties`. */
+ AnthropicCreateMessageRequest: {
+ /** @description Anthropic model identifier (e.g. `claude-sonnet-4-5`, `claude-opus-4-7`). */
+ model: string;
+ /** @description Maximum number of tokens to generate before stopping. */
+ max_tokens: number;
+ /** @description When true, the response is a `text/event-stream` of Anthropic message events instead of a single JSON body. */
+ stream?: boolean;
+ /** @description Top-level system prompt. Anthropic also accepts an array form via passthrough. */
+ system?: string;
+ /** @description Conversation turns. See Anthropic Messages API docs for the full content-block taxonomy. */
+ messages: components["schemas"]["AnthropicMessageParam"][];
+ } & {
+ [key: string]: unknown;
+ };
+ /** @description A single conversation turn. The proxy only reads `role` and `content` (string or array of content blocks) for prompt capture; additional fields pass through. */
+ AnthropicMessageParam: {
+ /** @enum {string} */
+ role: "user" | "assistant";
+ /** @description Either a string shorthand or an array of content blocks (text, image, document, tool_use, tool_result, ...). */
+ content: unknown;
+ } & {
+ [key: string]: unknown;
+ };
+ /** @description JSON shape of a non-streaming Messages API response. Most fields pass through; the proxy reads `usage` for billing. */
+ AnthropicCreateMessageResponse: {
+ id?: string;
+ type?: string;
+ role?: string;
+ model?: string;
+ stop_reason?: string | null;
+ stop_sequence?: string | null;
+ usage?: components["schemas"]["AnthropicMessagesUsage"];
+ } & {
+ [key: string]: unknown;
+ };
+ /** @description Token usage for an Anthropic Messages API call. */
+ AnthropicMessagesUsage: {
+ input_tokens?: number;
+ output_tokens?: number;
+ cache_creation_input_tokens?: number;
+ cache_read_input_tokens?: number;
+ cache_creation?: components["schemas"]["AnthropicCacheCreationUsage"];
+ };
+ /** @description Per-TTL breakdown of cache-write input tokens for an Anthropic Messages API call. */
+ AnthropicCacheCreationUsage: {
+ ephemeral_5m_input_tokens?: number;
+ ephemeral_1h_input_tokens?: number;
+ };
+ /**
+ * MetadataLevel
+ * @description Opt-in level for surfacing routing metadata on the response under `openrouter_metadata`.
+ * @enum {string}
+ */
+ OpenRouterMetadataLevel: "disabled" | "enabled";
+ /**
+ * AnthropicCacheControlTtl
+ * @enum {string}
+ */
+ OpenRouterAnthropicCacheControlTtl: "5m" | "1h";
+ /**
+ * AnthropicCacheControlDirectiveType
+ * @enum {string}
+ */
+ OpenRouterAnthropicCacheControlDirectiveType: "ephemeral";
+ /**
+ * AnthropicCacheControlDirective
+ * @description Enable automatic prompt caching. When set at the top level, the system automatically applies cache breakpoints to the last cacheable block in the request. Currently supported for Anthropic Claude models.
+ */
+ OpenRouterAnthropicCacheControlDirective: {
+ ttl?: components["schemas"]["OpenRouterAnthropicCacheControlTtl"];
+ type: components["schemas"]["OpenRouterAnthropicCacheControlDirectiveType"];
+ };
+ /**
+ * ChatDebugOptions
+ * @description Debug options for inspecting request transformations (streaming only)
+ */
+ OpenRouterChatDebugOptions: {
+ /** @description If true, includes the transformed upstream request body in a debug chunk at the start of the stream. Only works with streaming mode. */
+ echo_upstream_body?: boolean;
+ };
+ /** ImageConfig */
+ OpenRouterImageConfig: string | number | unknown[];
+ /**
+ * ChatAudioOutput
+ * @description Audio output data or reference
+ */
+ OpenRouterChatAudioOutput: {
+ /** @description Base64 encoded audio data */
+ data?: string;
+ /** @description Audio expiration timestamp */
+ expires_at?: number;
+ /** @description Audio output identifier */
+ id?: string;
+ /** @description Audio transcript */
+ transcript?: string;
+ };
+ /** ChatContentItemsDiscriminatorMappingFileFile */
+ OpenRouterChatContentItemsDiscriminatorMappingFileFile: {
+ /** @description File content as base64 data URL or URL */
+ file_data?: string;
+ /** @description File ID for previously uploaded files */
+ file_id?: string;
+ /** @description Original filename */
+ filename?: string;
+ };
+ /**
+ * ChatContentItemsDiscriminatorMappingImageUrlImageUrlDetail
+ * @description Image detail level for vision models
+ * @enum {string}
+ */
+ OpenRouterChatContentItemsDiscriminatorMappingImageUrlImageUrlDetail: "auto" | "low" | "high";
+ /** ChatContentItemsDiscriminatorMappingImageUrlImageUrl */
+ OpenRouterChatContentItemsDiscriminatorMappingImageUrlImageUrl: {
+ /** @description Image detail level for vision models */
+ detail?: components["schemas"]["OpenRouterChatContentItemsDiscriminatorMappingImageUrlImageUrlDetail"];
+ /** @description URL of the image (data: URLs supported) */
+ url: string;
+ };
+ /** ChatContentItemsDiscriminatorMappingInputAudioInputAudio */
+ OpenRouterChatContentItemsDiscriminatorMappingInputAudioInputAudio: {
+ /** @description Base64 encoded audio data */
+ data: string;
+ /** @description Audio format (e.g., wav, mp3, flac, m4a, ogg, aiff, aac, pcm16, pcm24). Supported formats vary by provider. */
+ format: string;
+ };
+ /**
+ * LegacyChatContentVideoType
+ * @enum {string}
+ */
+ OpenRouterLegacyChatContentVideoType: "input_video";
+ /**
+ * ChatContentVideoInput
+ * @description Video input object
+ */
+ OpenRouterChatContentVideoInput: {
+ /** @description URL of the video (data: URLs supported) */
+ url: string;
+ };
+ /**
+ * ChatContentCacheControlType
+ * @enum {string}
+ */
+ OpenRouterChatContentCacheControlType: "ephemeral";
+ /**
+ * ChatContentCacheControl
+ * @description Cache control for the content part
+ */
+ OpenRouterChatContentCacheControl: {
+ ttl?: components["schemas"]["OpenRouterAnthropicCacheControlTtl"];
+ type: components["schemas"]["OpenRouterChatContentCacheControlType"];
+ };
+ /**
+ * ChatContentTextType
+ * @enum {string}
+ */
+ OpenRouterChatContentTextType: "text";
+ /**
+ * ChatContentVideoType
+ * @enum {string}
+ */
+ OpenRouterChatContentVideoType: "video_url";
+ /**
+ * ChatContentItems
+ * @description Content part for chat completion messages
+ */
+ OpenRouterChatContentItems: {
+ /**
+ * @description Discriminator value: file
+ * @enum {string}
+ */
+ type: "file";
+ file: components["schemas"]["OpenRouterChatContentItemsDiscriminatorMappingFileFile"];
+ } | {
+ /**
+ * @description Discriminator value: image_url
+ * @enum {string}
+ */
+ type: "image_url";
+ image_url: components["schemas"]["OpenRouterChatContentItemsDiscriminatorMappingImageUrlImageUrl"];
+ } | {
+ /**
+ * @description Discriminator value: input_audio
+ * @enum {string}
+ */
+ type: "input_audio";
+ input_audio: components["schemas"]["OpenRouterChatContentItemsDiscriminatorMappingInputAudioInputAudio"];
+ } | {
+ type: components["schemas"]["OpenRouterLegacyChatContentVideoType"];
+ video_url: components["schemas"]["OpenRouterChatContentVideoInput"];
+ } | {
+ type: components["schemas"]["OpenRouterChatContentTextType"];
+ cache_control?: components["schemas"]["OpenRouterChatContentCacheControl"];
+ text: string;
+ } | {
+ type: components["schemas"]["OpenRouterChatContentVideoType"];
+ video_url: components["schemas"]["OpenRouterChatContentVideoInput"];
+ };
+ /** ChatMessagesDiscriminatorMappingAssistantContent1 */
+ OpenRouterChatMessagesDiscriminatorMappingAssistantContent1: components["schemas"]["OpenRouterChatContentItems"][];
+ /**
+ * ChatMessagesDiscriminatorMappingAssistantContent
+ * @description Assistant message content
+ */
+ OpenRouterChatMessagesDiscriminatorMappingAssistantContent: string | components["schemas"]["OpenRouterChatMessagesDiscriminatorMappingAssistantContent1"] | unknown;
+ /** ChatAssistantImagesItemsImageUrl */
+ OpenRouterChatAssistantImagesItemsImageUrl: {
+ /** @description URL or base64-encoded data of the generated image */
+ url: string;
+ };
+ /** ChatAssistantImagesItems */
+ OpenRouterChatAssistantImagesItems: {
+ image_url: components["schemas"]["OpenRouterChatAssistantImagesItemsImageUrl"];
+ };
+ /**
+ * ChatAssistantImages
+ * @description Generated images from image generation models
+ */
+ OpenRouterChatAssistantImages: components["schemas"]["OpenRouterChatAssistantImagesItems"][];
+ /**
+ * ReasoningFormat
+ * @enum {string}
+ */
+ OpenRouterReasoningFormat: "unknown" | "openai-responses-v1" | "azure-openai-responses-v1" | "xai-responses-v1" | "anthropic-claude-v1" | "google-gemini-v1";
+ /**
+ * ReasoningDetailUnion
+ * @description Reasoning detail union schema
+ */
+ OpenRouterReasoningDetailUnion: {
+ /** @description Discriminator value: reasoning.encrypted */
+ type: string;
+ data: string;
+ format?: components["schemas"]["OpenRouterReasoningFormat"];
+ id?: string | null;
+ index?: number;
+ } | {
+ /** @description Discriminator value: reasoning.summary */
+ type: string;
+ format?: components["schemas"]["OpenRouterReasoningFormat"];
+ id?: string | null;
+ index?: number;
+ summary: string;
+ } | {
+ /** @description Discriminator value: reasoning.text */
+ type: string;
+ format?: components["schemas"]["OpenRouterReasoningFormat"];
+ id?: string | null;
+ index?: number;
+ signature?: string | null;
+ text?: string | null;
+ };
+ /**
+ * ChatReasoningDetails
+ * @description Reasoning details for extended thinking models
+ */
+ OpenRouterChatReasoningDetails: components["schemas"]["OpenRouterReasoningDetailUnion"][];
+ /** ChatToolCallFunction */
+ OpenRouterChatToolCallFunction: {
+ /** @description Function arguments as JSON string */
+ arguments: string;
+ /** @description Function name to call */
+ name: string;
+ };
+ /**
+ * ChatToolCallType
+ * @enum {string}
+ */
+ OpenRouterChatToolCallType: "function";
+ /**
+ * ChatToolCall
+ * @description Tool call made by the assistant
+ */
+ OpenRouterChatToolCall: {
+ function: components["schemas"]["OpenRouterChatToolCallFunction"];
+ /** @description Tool call identifier */
+ id: string;
+ type: components["schemas"]["OpenRouterChatToolCallType"];
+ };
+ /**
+ * ChatContentText
+ * @description Text content part
+ */
+ OpenRouterChatContentText: {
+ cache_control?: components["schemas"]["OpenRouterChatContentCacheControl"];
+ text: string;
+ type: components["schemas"]["OpenRouterChatContentTextType"];
+ };
+ /** ChatMessagesDiscriminatorMappingDeveloperContent1 */
+ OpenRouterChatMessagesDiscriminatorMappingDeveloperContent1: components["schemas"]["OpenRouterChatContentText"][];
+ /**
+ * ChatMessagesDiscriminatorMappingDeveloperContent
+ * @description Developer message content
+ */
+ OpenRouterChatMessagesDiscriminatorMappingDeveloperContent: string | components["schemas"]["OpenRouterChatMessagesDiscriminatorMappingDeveloperContent1"];
+ /** ChatSystemMessageContent1 */
+ OpenRouterChatSystemMessageContent1: components["schemas"]["OpenRouterChatContentText"][];
+ /**
+ * ChatSystemMessageContent
+ * @description System message content
+ */
+ OpenRouterChatSystemMessageContent: string | components["schemas"]["OpenRouterChatSystemMessageContent1"];
+ /**
+ * ChatSystemMessageRole
+ * @enum {string}
+ */
+ OpenRouterChatSystemMessageRole: "system";
+ /** ChatToolMessageContent1 */
+ OpenRouterChatToolMessageContent1: components["schemas"]["OpenRouterChatContentItems"][];
+ /**
+ * ChatToolMessageContent
+ * @description Tool response content
+ */
+ OpenRouterChatToolMessageContent: string | components["schemas"]["OpenRouterChatToolMessageContent1"];
+ /**
+ * ChatToolMessageRole
+ * @enum {string}
+ */
+ OpenRouterChatToolMessageRole: "tool";
+ /** ChatUserMessageContent1 */
+ OpenRouterChatUserMessageContent1: components["schemas"]["OpenRouterChatContentItems"][];
+ /**
+ * ChatUserMessageContent
+ * @description User message content
+ */
+ OpenRouterChatUserMessageContent: string | components["schemas"]["OpenRouterChatUserMessageContent1"];
+ /**
+ * ChatUserMessageRole
+ * @enum {string}
+ */
+ OpenRouterChatUserMessageRole: "user";
+ /**
+ * ChatMessages
+ * @description Chat completion message with role-based discrimination
+ */
+ OpenRouterChatMessages: {
+ /**
+ * @description Discriminator value: assistant
+ * @enum {string}
+ */
+ role: "assistant";
+ audio?: components["schemas"]["OpenRouterChatAudioOutput"];
+ /** @description Assistant message content */
+ content?: components["schemas"]["OpenRouterChatMessagesDiscriminatorMappingAssistantContent"];
+ images?: components["schemas"]["OpenRouterChatAssistantImages"];
+ /** @description Optional name for the assistant */
+ name?: string;
+ /** @description Reasoning output */
+ reasoning?: string | null;
+ reasoning_details?: components["schemas"]["OpenRouterChatReasoningDetails"];
+ /** @description Refusal message if content was refused */
+ refusal?: string | null;
+ /** @description Tool calls made by the assistant */
+ tool_calls?: components["schemas"]["OpenRouterChatToolCall"][];
+ } | {
+ /**
+ * @description Discriminator value: developer
+ * @enum {string}
+ */
+ role: "developer";
+ /** @description Developer message content */
+ content: components["schemas"]["OpenRouterChatMessagesDiscriminatorMappingDeveloperContent"];
+ /** @description Optional name for the developer message */
+ name?: string;
+ } | {
+ role: components["schemas"]["OpenRouterChatSystemMessageRole"];
+ /** @description System message content */
+ content: components["schemas"]["OpenRouterChatSystemMessageContent"];
+ /** @description Optional name for the system message */
+ name?: string;
+ } | {
+ role: components["schemas"]["OpenRouterChatToolMessageRole"];
+ /** @description Tool response content */
+ content: components["schemas"]["OpenRouterChatToolMessageContent"];
+ /** @description ID of the assistant message tool call this message responds to */
+ tool_call_id: string;
+ } | {
+ role: components["schemas"]["OpenRouterChatUserMessageRole"];
+ /** @description User message content */
+ content: components["schemas"]["OpenRouterChatUserMessageContent"];
+ /** @description Optional name for the user */
+ name?: string;
+ };
+ /**
+ * ChatRequestModalitiesItems
+ * @enum {string}
+ */
+ OpenRouterChatRequestModalitiesItems: "text" | "image" | "audio";
+ /**
+ * ModelName
+ * @description Model to use for completion
+ */
+ OpenRouterModelName: string;
+ /**
+ * ChatModelNames
+ * @description Models to use for completion
+ */
+ OpenRouterChatModelNames: components["schemas"]["OpenRouterModelName"][];
+ /**
+ * ContextCompressionEngine
+ * @description The compression engine to use. Defaults to "middle-out".
+ * @enum {string}
+ */
+ OpenRouterContextCompressionEngine: "middle-out";
+ /**
+ * PdfParserEngine0
+ * @enum {string}
+ */
+ OpenRouterPdfParserEngine0: "mistral-ocr" | "native" | "cloudflare-ai";
+ /**
+ * PdfParserEngine1
+ * @enum {string}
+ */
+ OpenRouterPdfParserEngine1: "pdf-text";
+ /**
+ * PDFParserEngine
+ * @description The engine to use for parsing PDF files. "pdf-text" is deprecated and automatically redirected to "cloudflare-ai".
+ */
+ OpenRouterPDFParserEngine: components["schemas"]["OpenRouterPdfParserEngine0"] | components["schemas"]["OpenRouterPdfParserEngine1"];
+ /**
+ * PDFParserOptions
+ * @description Options for PDF parsing.
+ */
+ OpenRouterPDFParserOptions: {
+ engine?: components["schemas"]["OpenRouterPDFParserEngine"];
+ };
+ /**
+ * WebSearchEngine
+ * @description The search engine to use for web search.
+ * @enum {string}
+ */
+ OpenRouterWebSearchEngine: "native" | "exa" | "firecrawl" | "parallel";
+ /**
+ * WebSearchPluginId
+ * @enum {string}
+ */
+ OpenRouterWebSearchPluginId: "web";
+ /**
+ * WebSearchPluginUserLocationType
+ * @enum {string}
+ */
+ OpenRouterWebSearchPluginUserLocationType: "approximate";
+ /**
+ * WebSearchPluginUserLocation
+ * @description Approximate user location for location-biased search results. Passed through to native providers that support it (e.g. Anthropic).
+ */
+ OpenRouterWebSearchPluginUserLocation: {
+ city?: string | null;
+ country?: string | null;
+ region?: string | null;
+ timezone?: string | null;
+ type: components["schemas"]["OpenRouterWebSearchPluginUserLocationType"];
+ };
+ /**
+ * WebFetchPluginId
+ * @enum {string}
+ */
+ OpenRouterWebFetchPluginId: "web-fetch";
+ /** ChatRequestPluginsItems */
+ OpenRouterChatRequestPluginsItems: {
+ /**
+ * @description Discriminator value: auto-router
+ * @enum {string}
+ */
+ id: "auto-router";
+ /** @description List of model patterns to filter which models the auto-router can route between. Supports wildcards (e.g., "anthropic/*" matches all Anthropic models). When not specified, uses the default supported models list. */
+ allowed_models?: string[];
+ /** @description Set to false to disable the auto-router plugin for this request. Defaults to true. */
+ enabled?: boolean;
+ } | {
+ /**
+ * @description Discriminator value: context-compression
+ * @enum {string}
+ */
+ id: "context-compression";
+ /** @description Set to false to disable the context-compression plugin for this request. Defaults to true. */
+ enabled?: boolean;
+ engine?: components["schemas"]["OpenRouterContextCompressionEngine"];
+ } | {
+ /**
+ * @description Discriminator value: file-parser
+ * @enum {string}
+ */
+ id: "file-parser";
+ /** @description Set to false to disable the file-parser plugin for this request. Defaults to true. */
+ enabled?: boolean;
+ pdf?: components["schemas"]["OpenRouterPDFParserOptions"];
+ } | {
+ /**
+ * @description Discriminator value: fusion
+ * @enum {string}
+ */
+ id: "fusion";
+ /** @description Slugs of models to run in parallel as the "expert panel" the judge analyzes. Each model receives the same user prompt with web_search + web_fetch enabled. Capped at 8 models to bound cost amplification. When omitted, defaults to the Quality preset from the /labs/fusion UI (~anthropic/claude-opus-latest, ~openai/gpt-latest, ~google/gemini-pro-latest). */
+ analysis_models?: string[];
+ /** @description Set to false to disable the fusion plugin for this request. Defaults to true. */
+ enabled?: boolean;
+ /** @description Maximum number of tool-calling steps each panelist (analysis model) and the judge model may take during their agentic web-research loop. Models with web_search/web_fetch enabled iterate until they produce a text response or hit this ceiling. Defaults to 8. Capped at 16. */
+ max_tool_calls?: number;
+ /** @description Slug of the model that performs both the judge step (with web_search + web_fetch) and the final synthesis. When omitted, defaults to the first model in the Quality preset. */
+ model?: string;
+ } | {
+ /**
+ * @description Discriminator value: moderation
+ * @enum {string}
+ */
+ id: "moderation";
+ } | {
+ /**
+ * @description Discriminator value: pareto-router
+ * @enum {string}
+ */
+ id: "pareto-router";
+ /** @description Set to false to disable the pareto-router plugin for this request. Defaults to true. */
+ enabled?: boolean;
+ /**
+ * Format: double
+ * @description Minimum desired coding score between 0 and 1, where 1 is best. Higher values select from stronger coding models (sourced from Artificial Analysis coding percentiles). Maps internally to one of three tiers (low, medium, high). Omit to use the router default tier.
+ */
+ min_coding_score?: number;
+ } | {
+ /**
+ * @description Discriminator value: response-healing
+ * @enum {string}
+ */
+ id: "response-healing";
+ /** @description Set to false to disable the response-healing plugin for this request. Defaults to true. */
+ enabled?: boolean;
+ } | {
+ id: components["schemas"]["OpenRouterWebSearchPluginId"];
+ /** @description Set to false to disable the web-search plugin for this request. Defaults to true. */
+ enabled?: boolean;
+ engine?: components["schemas"]["OpenRouterWebSearchEngine"];
+ /** @description A list of domains to exclude from web search results. Supports wildcards (e.g. "*.substack.com") and path filtering (e.g. "openai.com/blog"). */
+ exclude_domains?: string[];
+ /** @description A list of domains to restrict web search results to. Supports wildcards (e.g. "*.substack.com") and path filtering (e.g. "openai.com/blog"). */
+ include_domains?: string[];
+ max_results?: number;
+ /** @description Maximum number of times the model can invoke web search in a single turn. Passed through to native providers that support it (e.g. Anthropic). */
+ max_uses?: number;
+ search_prompt?: string;
+ user_location?: components["schemas"]["OpenRouterWebSearchPluginUserLocation"];
+ } | {
+ id: components["schemas"]["OpenRouterWebFetchPluginId"];
+ /** @description Only fetch from these domains. */
+ allowed_domains?: string[];
+ /** @description Never fetch from these domains. */
+ blocked_domains?: string[];
+ /** @description Maximum content length in approximate tokens. Content exceeding this limit is truncated. */
+ max_content_tokens?: number;
+ /** @description Maximum number of web fetches per request. Once exceeded, the tool returns an error. */
+ max_uses?: number;
+ };
+ /**
+ * ProviderPreferencesDataCollection
+ * @description Data collection setting. If no available model provider meets the requirement, your request will return an error.
+ * - allow: (default) allow providers which store user data non-transiently and may train on it
+ *
+ * - deny: use only providers which do not collect user data.
+ * @enum {string}
+ */
+ OpenRouterProviderPreferencesDataCollection: "deny" | "allow";
+ /**
+ * ProviderName
+ * @enum {string}
+ */
+ OpenRouterProviderName: "AkashML" | "AI21" | "AionLabs" | "Alibaba" | "Ambient" | "Baidu" | "Amazon Bedrock" | "Amazon Nova" | "Anthropic" | "Arcee AI" | "AtlasCloud" | "Avian" | "Azure" | "BaseTen" | "BytePlus" | "Black Forest Labs" | "Cerebras" | "Chutes" | "Cirrascale" | "Clarifai" | "Cloudflare" | "Cohere" | "Crucible" | "Crusoe" | "DeepInfra" | "DeepSeek" | "DekaLLM" | "Featherless" | "Fireworks" | "Friendli" | "GMICloud" | "Google" | "Google AI Studio" | "Groq" | "Hyperbolic" | "Inception" | "Inceptron" | "InferenceNet" | "Ionstream" | "Infermatic" | "Io Net" | "Inflection" | "Liquid" | "Mara" | "Mancer 2" | "Minimax" | "ModelRun" | "Mistral" | "Modular" | "Moonshot AI" | "Morph" | "NCompass" | "Nebius" | "Nex AGI" | "NextBit" | "Novita" | "Nvidia" | "OpenAI" | "OpenInference" | "Parasail" | "Poolside" | "Perceptron" | "Perplexity" | "Phala" | "Recraft" | "Reka" | "Relace" | "SambaNova" | "Seed" | "SiliconFlow" | "Sourceful" | "StepFun" | "Stealth" | "StreamLake" | "Switchpoint" | "Together" | "Upstage" | "Venice" | "WandB" | "Xiaomi" | "xAI" | "Z.AI" | "FakeProvider";
+ /** ProviderPreferencesIgnoreItems */
+ OpenRouterProviderPreferencesIgnoreItems: components["schemas"]["OpenRouterProviderName"] | string;
+ /**
+ * BigNumberUnion
+ * @description Price per million prompt tokens
+ */
+ OpenRouterBigNumberUnion: string;
+ /**
+ * ProviderPreferencesMaxPrice
+ * @description The object specifying the maximum price you want to pay for this request. USD price per million tokens, for prompt and completion.
+ */
+ OpenRouterProviderPreferencesMaxPrice: {
+ audio?: components["schemas"]["OpenRouterBigNumberUnion"];
+ completion?: components["schemas"]["OpenRouterBigNumberUnion"];
+ image?: components["schemas"]["OpenRouterBigNumberUnion"];
+ prompt?: components["schemas"]["OpenRouterBigNumberUnion"];
+ request?: components["schemas"]["OpenRouterBigNumberUnion"];
+ };
+ /** ProviderPreferencesOnlyItems */
+ OpenRouterProviderPreferencesOnlyItems: components["schemas"]["OpenRouterProviderName"] | string;
+ /** ProviderPreferencesOrderItems */
+ OpenRouterProviderPreferencesOrderItems: components["schemas"]["OpenRouterProviderName"] | string;
+ /**
+ * PercentileLatencyCutoffs
+ * @description Percentile-based latency cutoffs. All specified cutoffs must be met for an endpoint to be preferred.
+ */
+ OpenRouterPercentileLatencyCutoffs: {
+ /**
+ * Format: double
+ * @description Maximum p50 latency (seconds)
+ */
+ p50?: number | null;
+ /**
+ * Format: double
+ * @description Maximum p75 latency (seconds)
+ */
+ p75?: number | null;
+ /**
+ * Format: double
+ * @description Maximum p90 latency (seconds)
+ */
+ p90?: number | null;
+ /**
+ * Format: double
+ * @description Maximum p99 latency (seconds)
+ */
+ p99?: number | null;
+ };
+ /**
+ * PreferredMaxLatency
+ * @description Preferred maximum latency (in seconds). Can be a number (applies to p50) or an object with percentile-specific cutoffs. Endpoints above the threshold(s) may still be used, but are deprioritized in routing. When using fallback models, this may cause a fallback model to be used instead of the primary model if it meets the threshold.
+ */
+ OpenRouterPreferredMaxLatency: number | components["schemas"]["OpenRouterPercentileLatencyCutoffs"] | unknown;
+ /**
+ * PercentileThroughputCutoffs
+ * @description Percentile-based throughput cutoffs. All specified cutoffs must be met for an endpoint to be preferred.
+ */
+ OpenRouterPercentileThroughputCutoffs: {
+ /**
+ * Format: double
+ * @description Minimum p50 throughput (tokens/sec)
+ */
+ p50?: number | null;
+ /**
+ * Format: double
+ * @description Minimum p75 throughput (tokens/sec)
+ */
+ p75?: number | null;
+ /**
+ * Format: double
+ * @description Minimum p90 throughput (tokens/sec)
+ */
+ p90?: number | null;
+ /**
+ * Format: double
+ * @description Minimum p99 throughput (tokens/sec)
+ */
+ p99?: number | null;
+ };
+ /**
+ * PreferredMinThroughput
+ * @description Preferred minimum throughput (in tokens per second). Can be a number (applies to p50) or an object with percentile-specific cutoffs. Endpoints below the threshold(s) may still be used, but are deprioritized in routing. When using fallback models, this may cause a fallback model to be used instead of the primary model if it meets the threshold.
+ */
+ OpenRouterPreferredMinThroughput: number | components["schemas"]["OpenRouterPercentileThroughputCutoffs"] | unknown;
+ /**
+ * Quantization
+ * @enum {string}
+ */
+ OpenRouterQuantization: "int4" | "int8" | "fp4" | "fp6" | "fp8" | "fp16" | "bf16" | "fp32" | "unknown";
+ /**
+ * ProviderSort
+ * @description The provider sorting strategy (price, throughput, latency)
+ * @enum {string}
+ */
+ OpenRouterProviderSort: "price" | "throughput" | "latency" | "exacto";
+ /**
+ * ProviderSortConfigBy
+ * @description The provider sorting strategy (price, throughput, latency)
+ * @enum {string}
+ */
+ OpenRouterProviderSortConfigBy: "price" | "throughput" | "latency" | "exacto";
+ /**
+ * ProviderSortConfigPartition
+ * @description Partitioning strategy for sorting: "model" (default) groups endpoints by model before sorting (fallback models remain fallbacks), "none" sorts all endpoints together regardless of model.
+ * @enum {string}
+ */
+ OpenRouterProviderSortConfigPartition: "model" | "none";
+ /**
+ * ProviderSortConfig
+ * @description The provider sorting strategy (price, throughput, latency)
+ */
+ OpenRouterProviderSortConfig: {
+ /** @description The provider sorting strategy (price, throughput, latency) */
+ by?: components["schemas"]["OpenRouterProviderSortConfigBy"];
+ /** @description Partitioning strategy for sorting: "model" (default) groups endpoints by model before sorting (fallback models remain fallbacks), "none" sorts all endpoints together regardless of model. */
+ partition?: components["schemas"]["OpenRouterProviderSortConfigPartition"];
+ };
+ /**
+ * ProviderPreferencesSort
+ * @description The sorting strategy to use for this request, if "order" is not specified. When set, no load balancing is performed.
+ */
+ OpenRouterProviderPreferencesSort: components["schemas"]["OpenRouterProviderSort"] | components["schemas"]["OpenRouterProviderSortConfig"] | unknown;
+ /**
+ * ProviderPreferences
+ * @description When multiple model providers are available, optionally indicate your routing preference.
+ */
+ OpenRouterProviderPreferences: {
+ /**
+ * @description Whether to allow backup providers to serve requests
+ * - true: (default) when the primary provider (or your custom providers in "order") is unavailable, use the next best provider.
+ * - false: use only the primary/custom provider, and return the upstream error if it's unavailable.
+ */
+ allow_fallbacks?: boolean | null;
+ /**
+ * @description Data collection setting. If no available model provider meets the requirement, your request will return an error.
+ * - allow: (default) allow providers which store user data non-transiently and may train on it
+ *
+ * - deny: use only providers which do not collect user data.
+ */
+ data_collection?: components["schemas"]["OpenRouterProviderPreferencesDataCollection"];
+ /** @description Whether to restrict routing to only models that allow text distillation. When true, only models where the author has allowed distillation will be used. */
+ enforce_distillable_text?: boolean | null;
+ /** @description List of provider slugs to ignore. If provided, this list is merged with your account-wide ignored provider settings for this request. */
+ ignore?: components["schemas"]["OpenRouterProviderPreferencesIgnoreItems"][] | null;
+ /** @description The object specifying the maximum price you want to pay for this request. USD price per million tokens, for prompt and completion. */
+ max_price?: components["schemas"]["OpenRouterProviderPreferencesMaxPrice"];
+ /** @description List of provider slugs to allow. If provided, this list is merged with your account-wide allowed provider settings for this request. */
+ only?: components["schemas"]["OpenRouterProviderPreferencesOnlyItems"][] | null;
+ /** @description An ordered list of provider slugs. The router will attempt to use the first provider in the subset of this list that supports your requested model, and fall back to the next if it is unavailable. If no providers are available, the request will fail with an error message. */
+ order?: components["schemas"]["OpenRouterProviderPreferencesOrderItems"][] | null;
+ preferred_max_latency?: components["schemas"]["OpenRouterPreferredMaxLatency"];
+ preferred_min_throughput?: components["schemas"]["OpenRouterPreferredMinThroughput"];
+ /** @description A list of quantization levels to filter the provider by. */
+ quantizations?: components["schemas"]["OpenRouterQuantization"][] | null;
+ /** @description Whether to filter providers to only those that support the parameters you've provided. If this setting is omitted or set to false, then providers will receive only the parameters they support, and ignore the rest. */
+ require_parameters?: boolean | null;
+ /** @description The sorting strategy to use for this request, if "order" is not specified. When set, no load balancing is performed. */
+ sort?: components["schemas"]["OpenRouterProviderPreferencesSort"];
+ /** @description Whether to restrict routing to only ZDR (Zero Data Retention) endpoints. When true, only endpoints that do not retain prompts will be used. */
+ zdr?: boolean | null;
+ };
+ /**
+ * ChatRequestReasoningEffort
+ * @description Constrains effort on reasoning for reasoning models
+ * @enum {string}
+ */
+ OpenRouterChatRequestReasoningEffort: "xhigh" | "high" | "medium" | "low" | "minimal" | "none";
+ /**
+ * ChatReasoningSummaryVerbosityEnum
+ * @enum {string}
+ */
+ OpenRouterChatReasoningSummaryVerbosityEnum: "auto" | "concise" | "detailed";
+ /**
+ * ChatRequestReasoning
+ * @description Configuration options for reasoning models
+ */
+ OpenRouterChatRequestReasoning: {
+ /** @description Constrains effort on reasoning for reasoning models */
+ effort?: components["schemas"]["OpenRouterChatRequestReasoningEffort"];
+ summary?: components["schemas"]["OpenRouterChatReasoningSummaryVerbosityEnum"];
+ };
+ /**
+ * FormatJsonObjectConfigType
+ * @enum {string}
+ */
+ OpenRouterFormatJsonObjectConfigType: "json_object";
+ /**
+ * ChatJsonSchemaConfig
+ * @description JSON Schema configuration object
+ */
+ OpenRouterChatJsonSchemaConfig: {
+ /** @description Schema description for the model */
+ description?: string;
+ /** @description Schema name (a-z, A-Z, 0-9, underscores, dashes, max 64 chars) */
+ name: string;
+ /** @description JSON Schema object */
+ schema?: {
+ [key: string]: unknown;
+ };
+ /** @description Enable strict schema adherence */
+ strict?: boolean | null;
+ };
+ /**
+ * ChatRequestResponseFormat
+ * @description Response format configuration
+ */
+ OpenRouterChatRequestResponseFormat: {
+ /**
+ * @description Discriminator value: grammar
+ * @enum {string}
+ */
+ type: "grammar";
+ /** @description Custom grammar for text generation */
+ grammar: string;
+ } | {
+ type: components["schemas"]["OpenRouterFormatJsonObjectConfigType"];
+ } | {
+ /**
+ * @description Discriminator value: json_schema
+ * @enum {string}
+ */
+ type: "json_schema";
+ json_schema: components["schemas"]["OpenRouterChatJsonSchemaConfig"];
+ } | {
+ /**
+ * @description Discriminator value: python
+ * @enum {string}
+ */
+ type: "python";
+ } | {
+ /**
+ * @description Discriminator value: text
+ * @enum {string}
+ */
+ type: "text";
+ };
+ /**
+ * ChatRequestServiceTier
+ * @description The service tier to use for processing this request.
+ * @enum {string}
+ */
+ OpenRouterChatRequestServiceTier: "auto" | "default" | "flex" | "priority" | "scale";
+ /**
+ * ChatRequestStop
+ * @description Stop sequences (up to 4)
+ */
+ OpenRouterChatRequestStop: string | string[] | unknown;
+ /**
+ * StopServerToolsWhenFinishReasonIsType
+ * @enum {string}
+ */
+ OpenRouterStopServerToolsWhenFinishReasonIsType: "finish_reason_is";
+ /**
+ * StopServerToolsWhenHasToolCallType
+ * @enum {string}
+ */
+ OpenRouterStopServerToolsWhenHasToolCallType: "has_tool_call";
+ /**
+ * StopServerToolsWhenMaxCostType
+ * @enum {string}
+ */
+ OpenRouterStopServerToolsWhenMaxCostType: "max_cost";
+ /**
+ * StopServerToolsWhenMaxTokensUsedType
+ * @enum {string}
+ */
+ OpenRouterStopServerToolsWhenMaxTokensUsedType: "max_tokens_used";
+ /**
+ * StopServerToolsWhenStepCountIsType
+ * @enum {string}
+ */
+ OpenRouterStopServerToolsWhenStepCountIsType: "step_count_is";
+ /**
+ * StopServerToolsWhenCondition
+ * @description A single condition that, when met, halts the server-tool agent loop.
+ */
+ OpenRouterStopServerToolsWhenCondition: {
+ type: components["schemas"]["OpenRouterStopServerToolsWhenFinishReasonIsType"];
+ reason: string;
+ } | {
+ type: components["schemas"]["OpenRouterStopServerToolsWhenHasToolCallType"];
+ tool_name: string;
+ } | {
+ type: components["schemas"]["OpenRouterStopServerToolsWhenMaxCostType"];
+ /** Format: double */
+ max_cost_in_dollars: number;
+ } | {
+ type: components["schemas"]["OpenRouterStopServerToolsWhenMaxTokensUsedType"];
+ max_tokens: number;
+ } | {
+ type: components["schemas"]["OpenRouterStopServerToolsWhenStepCountIsType"];
+ step_count: number;
+ };
+ /**
+ * StopServerToolsWhen
+ * @description Stop conditions for the server-tool agent loop. Any condition firing halts the loop (OR logic). When set, this overrides `max_tool_calls`.
+ */
+ OpenRouterStopServerToolsWhen: components["schemas"]["OpenRouterStopServerToolsWhenCondition"][];
+ /**
+ * ChatStreamOptions
+ * @description Streaming configuration options
+ */
+ OpenRouterChatStreamOptions: {
+ /** @description Deprecated: This field has no effect. Full usage details are always included. */
+ include_usage?: boolean;
+ };
+ /**
+ * ChatToolChoice0
+ * @enum {string}
+ */
+ OpenRouterChatToolChoice0: "none";
+ /**
+ * ChatToolChoice1
+ * @enum {string}
+ */
+ OpenRouterChatToolChoice1: "auto";
+ /**
+ * ChatToolChoice2
+ * @enum {string}
+ */
+ OpenRouterChatToolChoice2: "required";
+ /** ChatNamedToolChoiceFunction */
+ OpenRouterChatNamedToolChoiceFunction: {
+ /** @description Function name to call */
+ name: string;
+ };
+ /**
+ * ChatNamedToolChoiceType
+ * @enum {string}
+ */
+ OpenRouterChatNamedToolChoiceType: "function";
+ /**
+ * ChatNamedToolChoice
+ * @description Named tool choice for specific function
+ */
+ OpenRouterChatNamedToolChoice: {
+ function: components["schemas"]["OpenRouterChatNamedToolChoiceFunction"];
+ type: components["schemas"]["OpenRouterChatNamedToolChoiceType"];
+ };
+ /**
+ * ChatToolChoice
+ * @description Tool choice configuration
+ */
+ OpenRouterChatToolChoice: components["schemas"]["OpenRouterChatToolChoice0"] | components["schemas"]["OpenRouterChatToolChoice1"] | components["schemas"]["OpenRouterChatToolChoice2"] | components["schemas"]["OpenRouterChatNamedToolChoice"];
+ /**
+ * ChatFunctionToolOneOf0Function
+ * @description Function definition for tool calling
+ */
+ OpenRouterChatFunctionToolOneOf0Function: {
+ /** @description Function description for the model */
+ description?: string;
+ /** @description Function name (a-z, A-Z, 0-9, underscores, dashes, max 64 chars) */
+ name: string;
+ /** @description Function parameters as JSON Schema object */
+ parameters?: {
+ [key: string]: unknown;
+ };
+ /** @description Enable strict schema adherence */
+ strict?: boolean | null;
+ };
+ /**
+ * ChatFunctionToolOneOf0Type
+ * @enum {string}
+ */
+ OpenRouterChatFunctionToolOneOf0Type: "function";
+ /** ChatFunctionTool0 */
+ OpenRouterChatFunctionTool0: {
+ cache_control?: components["schemas"]["OpenRouterChatContentCacheControl"];
+ /** @description Function definition for tool calling */
+ function: components["schemas"]["OpenRouterChatFunctionToolOneOf0Function"];
+ type: components["schemas"]["OpenRouterChatFunctionToolOneOf0Type"];
+ };
+ /**
+ * DatetimeServerToolConfig
+ * @description Configuration for the openrouter:datetime server tool
+ */
+ OpenRouterDatetimeServerToolConfig: {
+ /** @description IANA timezone name (e.g. "America/New_York"). Defaults to UTC. */
+ timezone?: string;
+ };
+ /**
+ * DatetimeServerToolType
+ * @enum {string}
+ */
+ OpenRouterDatetimeServerToolType: "openrouter:datetime";
+ /**
+ * DatetimeServerTool
+ * @description OpenRouter built-in server tool: returns the current date and time
+ */
+ OpenRouterDatetimeServerTool: {
+ parameters?: components["schemas"]["OpenRouterDatetimeServerToolConfig"];
+ type: components["schemas"]["OpenRouterDatetimeServerToolType"];
+ };
+ /**
+ * ImageGenerationServerToolConfig
+ * @description Configuration for the openrouter:image_generation server tool. Accepts all image_config params (aspect_ratio, quality, size, background, output_format, output_compression, moderation, etc.) plus a model field.
+ */
+ OpenRouterImageGenerationServerToolConfig: {
+ /** @description Which image generation model to use (e.g. "openai/gpt-5-image"). Defaults to "openai/gpt-5-image". */
+ model?: string;
+ };
+ /**
+ * ImageGenerationServerToolOpenRouterType
+ * @enum {string}
+ */
+ OpenRouterImageGenerationServerToolOpenRouterType: "openrouter:image_generation";
+ /**
+ * ImageGenerationServerTool_OpenRouter
+ * @description OpenRouter built-in server tool: generates images from text prompts using an image generation model
+ */
+ ImageGenerationServerTool_OpenRouter: {
+ parameters?: components["schemas"]["OpenRouterImageGenerationServerToolConfig"];
+ type: components["schemas"]["OpenRouterImageGenerationServerToolOpenRouterType"];
+ };
+ /**
+ * SearchModelsServerToolConfig
+ * @description Configuration for the openrouter:experimental__search_models server tool
+ */
+ OpenRouterSearchModelsServerToolConfig: {
+ /** @description Maximum number of models to return. Defaults to 5, max 20. */
+ max_results?: number;
+ };
+ /**
+ * ChatSearchModelsServerToolType
+ * @enum {string}
+ */
+ OpenRouterChatSearchModelsServerToolType: "openrouter:experimental__search_models";
+ /**
+ * ChatSearchModelsServerTool
+ * @description OpenRouter built-in server tool: searches and filters AI models available on OpenRouter
+ */
+ OpenRouterChatSearchModelsServerTool: {
+ parameters?: components["schemas"]["OpenRouterSearchModelsServerToolConfig"];
+ type: components["schemas"]["OpenRouterChatSearchModelsServerToolType"];
+ };
+ /**
+ * WebFetchEngineEnum
+ * @description Which fetch engine to use. "auto" (default) uses native if the provider supports it, otherwise Exa. "native" forces the provider's built-in fetch. "exa" uses Exa Contents API. "openrouter" uses direct HTTP fetch. "firecrawl" uses Firecrawl scrape (requires BYOK).
+ * @enum {string}
+ */
+ OpenRouterWebFetchEngineEnum: "auto" | "native" | "openrouter" | "firecrawl" | "exa";
+ /**
+ * WebFetchServerToolConfig
+ * @description Configuration for the openrouter:web_fetch server tool
+ */
+ OpenRouterWebFetchServerToolConfig: {
+ /** @description Only fetch from these domains. */
+ allowed_domains?: string[];
+ /** @description Never fetch from these domains. */
+ blocked_domains?: string[];
+ engine?: components["schemas"]["OpenRouterWebFetchEngineEnum"];
+ /** @description Maximum content length in approximate tokens. Content exceeding this limit is truncated. */
+ max_content_tokens?: number;
+ /** @description Maximum number of web fetches per request. Once exceeded, the tool returns an error. */
+ max_uses?: number;
+ };
+ /**
+ * WebFetchServerToolType
+ * @enum {string}
+ */
+ OpenRouterWebFetchServerToolType: "openrouter:web_fetch";
+ /**
+ * WebFetchServerTool
+ * @description OpenRouter built-in server tool: fetches full content from a URL (web page or PDF)
+ */
+ OpenRouterWebFetchServerTool: {
+ parameters?: components["schemas"]["OpenRouterWebFetchServerToolConfig"];
+ type: components["schemas"]["OpenRouterWebFetchServerToolType"];
+ };
+ /**
+ * WebSearchEngineEnum
+ * @description Which search engine to use. "auto" (default) uses native if the provider supports it, otherwise Exa. "native" forces the provider's built-in search. "exa" forces the Exa search API. "firecrawl" uses Firecrawl (requires BYOK). "parallel" uses the Parallel search API.
+ * @enum {string}
+ */
+ OpenRouterWebSearchEngineEnum: "auto" | "native" | "exa" | "firecrawl" | "parallel";
+ /**
+ * SearchQualityLevel
+ * @description How much context to retrieve per result. Applies to Exa and Parallel engines; ignored with native provider search and Firecrawl. For Exa, pins a fixed per-result character cap (low=5,000, medium=15,000, high=30,000); when omitted, Exa picks an adaptive size per query and document (typically ~2,000–4,000 characters per result). For Parallel, controls the total characters across all results; when omitted, Parallel uses its own default size.
+ * @enum {string}
+ */
+ OpenRouterSearchQualityLevel: "low" | "medium" | "high";
+ /**
+ * WebSearchUserLocationServerToolType
+ * @enum {string}
+ */
+ OpenRouterWebSearchUserLocationServerToolType: "approximate";
+ /**
+ * WebSearchUserLocationServerTool
+ * @description Approximate user location for location-biased results.
+ */
+ OpenRouterWebSearchUserLocationServerTool: {
+ city?: string | null;
+ country?: string | null;
+ region?: string | null;
+ timezone?: string | null;
+ type?: components["schemas"]["OpenRouterWebSearchUserLocationServerToolType"];
+ };
+ /** WebSearchConfig */
+ OpenRouterWebSearchConfig: {
+ /** @description Limit search results to these domains. Supported by Exa, Firecrawl, Parallel, and most native providers (Anthropic, OpenAI, xAI). Not supported with Perplexity. Cannot be used with excluded_domains. */
+ allowed_domains?: string[];
+ engine?: components["schemas"]["OpenRouterWebSearchEngineEnum"];
+ /** @description Exclude search results from these domains. Supported by Exa, Firecrawl, Parallel, Anthropic, and xAI. Not supported with OpenAI (silently ignored) or Perplexity. Cannot be used with allowed_domains. */
+ excluded_domains?: string[];
+ /** @description Maximum number of search results to return per search call. Defaults to 5. Applies to Exa, Firecrawl, and Parallel engines; ignored with native provider search. */
+ max_results?: number;
+ /** @description Maximum total number of search results across all search calls in a single request. Once this limit is reached, the tool will stop returning new results. Useful for controlling cost and context size in agentic loops. Defaults to 50 when not specified. */
+ max_total_results?: number;
+ search_context_size?: components["schemas"]["OpenRouterSearchQualityLevel"];
+ user_location?: components["schemas"]["OpenRouterWebSearchUserLocationServerTool"];
+ };
+ /**
+ * OpenRouterWebSearchServerToolType
+ * @enum {string}
+ */
+ OpenRouterWebSearchServerToolType: "openrouter:web_search";
+ /**
+ * OpenRouterWebSearchServerTool
+ * @description OpenRouter built-in server tool: searches the web for current information
+ */
+ OpenRouterWebSearchServerTool: {
+ parameters?: components["schemas"]["OpenRouterWebSearchConfig"];
+ type: components["schemas"]["OpenRouterWebSearchServerToolType"];
+ };
+ /**
+ * ChatWebSearchShorthandType
+ * @enum {string}
+ */
+ OpenRouterChatWebSearchShorthandType: "web_search" | "web_search_preview" | "web_search_preview_2025_03_11" | "web_search_2025_08_26";
+ /**
+ * ChatWebSearchShorthand
+ * @description Web search tool using OpenAI Responses API syntax. Automatically converted to openrouter:web_search.
+ */
+ OpenRouterChatWebSearchShorthand: {
+ /** @description Limit search results to these domains. Supported by Exa, Firecrawl, Parallel, and most native providers (Anthropic, OpenAI, xAI). Not supported with Perplexity. Cannot be used with excluded_domains. */
+ allowed_domains?: string[];
+ engine?: components["schemas"]["OpenRouterWebSearchEngineEnum"];
+ /** @description Exclude search results from these domains. Supported by Exa, Firecrawl, Parallel, Anthropic, and xAI. Not supported with OpenAI (silently ignored) or Perplexity. Cannot be used with allowed_domains. */
+ excluded_domains?: string[];
+ /** @description Maximum number of search results to return per search call. Defaults to 5. Applies to Exa, Firecrawl, and Parallel engines; ignored with native provider search. */
+ max_results?: number;
+ /** @description Maximum total number of search results across all search calls in a single request. Once this limit is reached, the tool will stop returning new results. Useful for controlling cost and context size in agentic loops. Defaults to 50 when not specified. */
+ max_total_results?: number;
+ parameters?: components["schemas"]["OpenRouterWebSearchConfig"];
+ search_context_size?: components["schemas"]["OpenRouterSearchQualityLevel"];
+ type: components["schemas"]["OpenRouterChatWebSearchShorthandType"];
+ user_location?: components["schemas"]["OpenRouterWebSearchUserLocationServerTool"];
+ };
+ /**
+ * ChatFunctionTool
+ * @description Tool definition for function calling (regular function or OpenRouter built-in server tool)
+ */
+ OpenRouterChatFunctionTool: components["schemas"]["OpenRouterChatFunctionTool0"] | components["schemas"]["OpenRouterDatetimeServerTool"] | components["schemas"]["ImageGenerationServerTool_OpenRouter"] | components["schemas"]["OpenRouterChatSearchModelsServerTool"] | components["schemas"]["OpenRouterWebFetchServerTool"] | components["schemas"]["OpenRouterWebSearchServerTool"] | components["schemas"]["OpenRouterChatWebSearchShorthand"];
+ /**
+ * TraceConfig
+ * @description Metadata for observability and tracing. Known keys (trace_id, trace_name, span_name, generation_name, parent_span_id) have special handling. Additional keys are passed through as custom metadata to configured broadcast destinations.
+ */
+ OpenRouterTraceConfig: {
+ generation_name?: string;
+ parent_span_id?: string;
+ span_name?: string;
+ trace_id?: string;
+ trace_name?: string;
+ };
+ /**
+ * ChatRequest
+ * @description Chat completion request parameters
+ */
+ OpenRouterChatRequest: {
+ cache_control?: components["schemas"]["OpenRouterAnthropicCacheControlDirective"];
+ debug?: components["schemas"]["OpenRouterChatDebugOptions"];
+ /**
+ * Format: double
+ * @description Frequency penalty (-2.0 to 2.0)
+ */
+ frequency_penalty?: number | null;
+ image_config?: components["schemas"]["OpenRouterImageConfig"];
+ /** @description Token logit bias adjustments */
+ logit_bias?: {
+ [key: string]: number;
+ } | null;
+ /** @description Return log probabilities */
+ logprobs?: boolean | null;
+ /** @description Maximum tokens in completion */
+ max_completion_tokens?: number | null;
+ /** @description Maximum tokens (deprecated, use max_completion_tokens). Note: some providers enforce a minimum of 16. */
+ max_tokens?: number | null;
+ /** @description List of messages for the conversation */
+ messages: components["schemas"]["OpenRouterChatMessages"][];
+ /** @description Key-value pairs for additional object information (max 16 pairs, 64 char keys, 512 char values) */
+ metadata?: {
+ [key: string]: string;
+ };
+ /** @description Output modalities for the response. Supported values are "text", "image", and "audio". */
+ modalities?: components["schemas"]["OpenRouterChatRequestModalitiesItems"][];
+ model?: components["schemas"]["OpenRouterModelName"];
+ models?: components["schemas"]["OpenRouterChatModelNames"];
+ /** @description Whether to enable parallel function calling during tool use. When true, the model may generate multiple tool calls in a single response. */
+ parallel_tool_calls?: boolean | null;
+ /** @description Plugins you want to enable for this request, including their settings. */
+ plugins?: components["schemas"]["OpenRouterChatRequestPluginsItems"][];
+ /**
+ * Format: double
+ * @description Presence penalty (-2.0 to 2.0)
+ */
+ presence_penalty?: number | null;
+ provider?: components["schemas"]["OpenRouterProviderPreferences"];
+ /** @description Configuration options for reasoning models */
+ reasoning?: components["schemas"]["OpenRouterChatRequestReasoning"];
+ /** @description Response format configuration */
+ response_format?: components["schemas"]["OpenRouterChatRequestResponseFormat"];
+ /** @description Any type */
+ route?: unknown;
+ /** @description Random seed for deterministic outputs */
+ seed?: number | null;
+ /** @description The service tier to use for processing this request. */
+ service_tier?: components["schemas"]["OpenRouterChatRequestServiceTier"];
+ /** @description A unique identifier for grouping related requests (e.g., a conversation or agent workflow) for observability. If provided in both the request body and the x-session-id header, the body value takes precedence. Maximum of 256 characters. */
+ session_id?: string;
+ /** @description Stop sequences (up to 4) */
+ stop?: components["schemas"]["OpenRouterChatRequestStop"];
+ stop_server_tools_when?: components["schemas"]["OpenRouterStopServerToolsWhen"];
+ /**
+ * @description Enable streaming response
+ * @default false
+ */
+ stream: boolean;
+ stream_options?: components["schemas"]["OpenRouterChatStreamOptions"];
+ /**
+ * Format: double
+ * @description Sampling temperature (0-2)
+ */
+ temperature?: number | null;
+ tool_choice?: components["schemas"]["OpenRouterChatToolChoice"];
+ /** @description Available tools for function calling */
+ tools?: components["schemas"]["OpenRouterChatFunctionTool"][];
+ /** @description Number of top log probabilities to return (0-20) */
+ top_logprobs?: number | null;
+ /**
+ * Format: double
+ * @description Nucleus sampling parameter (0-1)
+ */
+ top_p?: number | null;
+ trace?: components["schemas"]["OpenRouterTraceConfig"];
+ /** @description Unique user identifier */
+ user?: string;
+ };
+ /**
+ * ChatFinishReasonEnum
+ * @enum {string}
+ */
+ OpenRouterChatFinishReasonEnum: "tool_calls" | "stop" | "length" | "content_filter" | "error";
+ /** ChatTokenLogprobTopLogprobsItems */
+ OpenRouterChatTokenLogprobTopLogprobsItems: {
+ bytes: number[] | null;
+ /** Format: double */
+ logprob: number;
+ token: string;
+ };
+ /**
+ * ChatTokenLogprob
+ * @description Token log probability information
+ */
+ OpenRouterChatTokenLogprob: {
+ /** @description UTF-8 bytes of the token */
+ bytes: number[] | null;
+ /**
+ * Format: double
+ * @description Log probability of the token
+ */
+ logprob: number;
+ /** @description The token */
+ token: string;
+ /** @description Top alternative tokens with probabilities */
+ top_logprobs: components["schemas"]["OpenRouterChatTokenLogprobTopLogprobsItems"][];
+ };
+ /**
+ * ChatTokenLogprobs
+ * @description Log probabilities for the completion
+ */
+ OpenRouterChatTokenLogprobs: {
+ /** @description Log probabilities for content tokens */
+ content: components["schemas"]["OpenRouterChatTokenLogprob"][] | null;
+ /** @description Log probabilities for refusal tokens */
+ refusal?: components["schemas"]["OpenRouterChatTokenLogprob"][] | null;
+ };
+ /**
+ * ChatAssistantMessage
+ * @description Assistant message for requests and responses
+ */
+ OpenRouterChatAssistantMessage: {
+ audio?: components["schemas"]["OpenRouterChatAudioOutput"];
+ /** @description Assistant message content */
+ content?: components["schemas"]["OpenRouterChatMessagesDiscriminatorMappingAssistantContent"];
+ images?: components["schemas"]["OpenRouterChatAssistantImages"];
+ /** @description Optional name for the assistant */
+ name?: string;
+ /** @description Reasoning output */
+ reasoning?: string | null;
+ reasoning_details?: components["schemas"]["OpenRouterChatReasoningDetails"];
+ /** @description Refusal message if content was refused */
+ refusal?: string | null;
+ /** @description Tool calls made by the assistant */
+ tool_calls?: components["schemas"]["OpenRouterChatToolCall"][];
+ };
+ /**
+ * ChatChoice
+ * @description Chat completion choice
+ */
+ OpenRouterChatChoice: {
+ finish_reason: components["schemas"]["OpenRouterChatFinishReasonEnum"];
+ /** @description Choice index */
+ index: number;
+ logprobs?: components["schemas"]["OpenRouterChatTokenLogprobs"];
+ message: components["schemas"]["OpenRouterChatAssistantMessage"];
+ };
+ /**
+ * ChatResultObject
+ * @enum {string}
+ */
+ OpenRouterChatResultObject: "chat.completion";
+ /** RouterAttempt */
+ OpenRouterRouterAttempt: {
+ model: string;
+ provider: string;
+ status: number;
+ };
+ /** EndpointInfo */
+ OpenRouterEndpointInfo: {
+ model: string;
+ provider: string;
+ selected: boolean;
+ };
+ /** EndpointsMetadata */
+ OpenRouterEndpointsMetadata: {
+ available: components["schemas"]["OpenRouterEndpointInfo"][];
+ total: number;
+ };
+ /** RouterParams */
+ OpenRouterRouterParams: {
+ /** Format: double */
+ quality_floor?: number;
+ /** Format: double */
+ throughput_floor?: number;
+ version_group?: string;
+ };
+ /**
+ * PipelineStageType
+ * @description Categorical kind of a pipeline stage. Multiple plugins can share a type (e.g. all guardrail-level plugins emit `guardrail`); the `name` field disambiguates which plugin emitted it.
+ * @enum {string}
+ */
+ OpenRouterPipelineStageType: "guardrail" | "plugin" | "server_tools" | "response_healing" | "context_compression";
+ /** PipelineStage */
+ OpenRouterPipelineStage: {
+ /** Format: double */
+ cost_usd?: number | null;
+ data?: {
+ [key: string]: unknown;
+ };
+ guardrail_id?: string;
+ guardrail_scope?: string;
+ name: string;
+ summary?: string;
+ type: components["schemas"]["OpenRouterPipelineStageType"];
+ };
+ /**
+ * RoutingStrategy
+ * @enum {string}
+ */
+ OpenRouterRoutingStrategy: "direct" | "auto" | "free" | "latest" | "alias" | "fallback" | "pareto" | "bodybuilder" | "fusion";
+ /** OpenRouterMetadata */
+ OpenRouterMetadata: {
+ attempt: number;
+ attempts?: components["schemas"]["OpenRouterRouterAttempt"][];
+ endpoints: components["schemas"]["OpenRouterEndpointsMetadata"];
+ is_byok: boolean;
+ params?: components["schemas"]["OpenRouterRouterParams"];
+ pipeline?: components["schemas"]["OpenRouterPipelineStage"][];
+ region: string | null;
+ requested: string;
+ strategy: components["schemas"]["OpenRouterRoutingStrategy"];
+ summary: string;
+ };
+ /**
+ * ChatUsageCompletionTokensDetails
+ * @description Detailed completion token usage
+ */
+ OpenRouterChatUsageCompletionTokensDetails: {
+ /** @description Accepted prediction tokens */
+ accepted_prediction_tokens?: number | null;
+ /** @description Tokens used for audio output */
+ audio_tokens?: number | null;
+ /** @description Tokens used for reasoning */
+ reasoning_tokens?: number | null;
+ /** @description Rejected prediction tokens */
+ rejected_prediction_tokens?: number | null;
+ };
+ /**
+ * CostDetails
+ * @description Breakdown of upstream inference costs
+ */
+ OpenRouterCostDetails: {
+ /** Format: double */
+ upstream_inference_completions_cost: number;
+ /** Format: double */
+ upstream_inference_cost?: number | null;
+ /** Format: double */
+ upstream_inference_prompt_cost: number;
+ };
+ /**
+ * ChatUsagePromptTokensDetails
+ * @description Detailed prompt token usage
+ */
+ OpenRouterChatUsagePromptTokensDetails: {
+ /** @description Audio input tokens */
+ audio_tokens?: number;
+ /** @description Tokens written to cache. Only returned for models with explicit caching and cache write pricing. */
+ cache_write_tokens?: number;
+ /** @description Cached prompt tokens */
+ cached_tokens?: number;
+ /** @description Video input tokens */
+ video_tokens?: number;
+ };
+ /**
+ * ChatUsage
+ * @description Token usage statistics
+ */
+ OpenRouterChatUsage: {
+ /** @description Number of tokens in the completion */
+ completion_tokens: number;
+ /** @description Detailed completion token usage */
+ completion_tokens_details?: components["schemas"]["OpenRouterChatUsageCompletionTokensDetails"];
+ /**
+ * Format: double
+ * @description Cost of the completion
+ */
+ cost?: number | null;
+ cost_details?: components["schemas"]["OpenRouterCostDetails"];
+ /** @description Whether a request was made using a Bring Your Own Key configuration */
+ is_byok?: boolean;
+ /** @description Number of tokens in the prompt */
+ prompt_tokens: number;
+ /** @description Detailed prompt token usage */
+ prompt_tokens_details?: components["schemas"]["OpenRouterChatUsagePromptTokensDetails"];
+ /** @description Total number of tokens */
+ total_tokens: number;
+ };
+ /**
+ * ChatResult
+ * @description Chat completion response
+ */
+ OpenRouterChatResult: {
+ /** @description List of completion choices */
+ choices: components["schemas"]["OpenRouterChatChoice"][];
+ /** @description Unix timestamp of creation */
+ created: number;
+ /** @description Unique completion identifier */
+ id: string;
+ /** @description Model used for completion */
+ model: string;
+ object: components["schemas"]["OpenRouterChatResultObject"];
+ openrouter_metadata?: components["schemas"]["OpenRouterMetadata"];
+ /** @description The service tier used by the upstream provider for this request */
+ service_tier?: string | null;
+ /** @description System fingerprint */
+ system_fingerprint: string | null;
+ usage?: components["schemas"]["OpenRouterChatUsage"];
+ };
+ /**
+ * BadRequestResponseErrorData
+ * @description Error data for BadRequestResponse
+ */
+ OpenRouterBadRequestResponseErrorData: {
+ code: number;
+ message: string;
+ metadata?: {
+ [key: string]: unknown;
+ } | null;
+ };
+ /**
+ * BadRequestResponse
+ * @description Bad Request - Invalid request parameters or malformed input
+ */
+ OpenRouterBadRequestResponse: {
+ error: components["schemas"]["OpenRouterBadRequestResponseErrorData"];
+ openrouter_metadata?: {
+ [key: string]: unknown;
+ } | null;
+ user_id?: string | null;
+ };
+ /**
+ * UnauthorizedResponseErrorData
+ * @description Error data for UnauthorizedResponse
+ */
+ OpenRouterUnauthorizedResponseErrorData: {
+ code: number;
+ message: string;
+ metadata?: {
+ [key: string]: unknown;
+ } | null;
+ };
+ /**
+ * UnauthorizedResponse
+ * @description Unauthorized - Authentication required or invalid credentials
+ */
+ OpenRouterUnauthorizedResponse: {
+ error: components["schemas"]["OpenRouterUnauthorizedResponseErrorData"];
+ openrouter_metadata?: {
+ [key: string]: unknown;
+ } | null;
+ user_id?: string | null;
+ };
+ /**
+ * PaymentRequiredResponseErrorData
+ * @description Error data for PaymentRequiredResponse
+ */
+ OpenRouterPaymentRequiredResponseErrorData: {
+ code: number;
+ message: string;
+ metadata?: {
+ [key: string]: unknown;
+ } | null;
+ };
+ /**
+ * PaymentRequiredResponse
+ * @description Payment Required - Insufficient credits or quota to complete request
+ */
+ OpenRouterPaymentRequiredResponse: {
+ error: components["schemas"]["OpenRouterPaymentRequiredResponseErrorData"];
+ openrouter_metadata?: {
+ [key: string]: unknown;
+ } | null;
+ user_id?: string | null;
+ };
+ /**
+ * ForbiddenResponseErrorData
+ * @description Error data for ForbiddenResponse
+ */
+ OpenRouterForbiddenResponseErrorData: {
+ code: number;
+ message: string;
+ metadata?: {
+ [key: string]: unknown;
+ } | null;
+ };
+ /**
+ * ForbiddenResponse
+ * @description Forbidden - Authentication successful but insufficient permissions
+ */
+ OpenRouterForbiddenResponse: {
+ error: components["schemas"]["OpenRouterForbiddenResponseErrorData"];
+ openrouter_metadata?: {
+ [key: string]: unknown;
+ } | null;
+ user_id?: string | null;
+ };
+ /**
+ * NotFoundResponseErrorData
+ * @description Error data for NotFoundResponse
+ */
+ OpenRouterNotFoundResponseErrorData: {
+ code: number;
+ message: string;
+ metadata?: {
+ [key: string]: unknown;
+ } | null;
+ };
+ /**
+ * NotFoundResponse
+ * @description Not Found - Resource does not exist
+ */
+ OpenRouterNotFoundResponse: {
+ error: components["schemas"]["OpenRouterNotFoundResponseErrorData"];
+ openrouter_metadata?: {
+ [key: string]: unknown;
+ } | null;
+ user_id?: string | null;
+ };
+ /**
+ * RequestTimeoutResponseErrorData
+ * @description Error data for RequestTimeoutResponse
+ */
+ OpenRouterRequestTimeoutResponseErrorData: {
+ code: number;
+ message: string;
+ metadata?: {
+ [key: string]: unknown;
+ } | null;
+ };
+ /**
+ * RequestTimeoutResponse
+ * @description Request Timeout - Operation exceeded time limit
+ */
+ OpenRouterRequestTimeoutResponse: {
+ error: components["schemas"]["OpenRouterRequestTimeoutResponseErrorData"];
+ openrouter_metadata?: {
+ [key: string]: unknown;
+ } | null;
+ user_id?: string | null;
+ };
+ /**
+ * PayloadTooLargeResponseErrorData
+ * @description Error data for PayloadTooLargeResponse
+ */
+ OpenRouterPayloadTooLargeResponseErrorData: {
+ code: number;
+ message: string;
+ metadata?: {
+ [key: string]: unknown;
+ } | null;
+ };
+ /**
+ * PayloadTooLargeResponse
+ * @description Payload Too Large - Request payload exceeds size limits
+ */
+ OpenRouterPayloadTooLargeResponse: {
+ error: components["schemas"]["OpenRouterPayloadTooLargeResponseErrorData"];
+ openrouter_metadata?: {
+ [key: string]: unknown;
+ } | null;
+ user_id?: string | null;
+ };
+ /**
+ * UnprocessableEntityResponseErrorData
+ * @description Error data for UnprocessableEntityResponse
+ */
+ OpenRouterUnprocessableEntityResponseErrorData: {
+ code: number;
+ message: string;
+ metadata?: {
+ [key: string]: unknown;
+ } | null;
+ };
+ /**
+ * UnprocessableEntityResponse
+ * @description Unprocessable Entity - Semantic validation failure
+ */
+ OpenRouterUnprocessableEntityResponse: {
+ error: components["schemas"]["OpenRouterUnprocessableEntityResponseErrorData"];
+ openrouter_metadata?: {
+ [key: string]: unknown;
+ } | null;
+ user_id?: string | null;
+ };
+ /**
+ * TooManyRequestsResponseErrorData
+ * @description Error data for TooManyRequestsResponse
+ */
+ OpenRouterTooManyRequestsResponseErrorData: {
+ code: number;
+ message: string;
+ metadata?: {
+ [key: string]: unknown;
+ } | null;
+ };
+ /**
+ * TooManyRequestsResponse
+ * @description Too Many Requests - Rate limit exceeded
+ */
+ OpenRouterTooManyRequestsResponse: {
+ error: components["schemas"]["OpenRouterTooManyRequestsResponseErrorData"];
+ openrouter_metadata?: {
+ [key: string]: unknown;
+ } | null;
+ user_id?: string | null;
+ };
+ /**
+ * InternalServerResponseErrorData
+ * @description Error data for InternalServerResponse
+ */
+ OpenRouterInternalServerResponseErrorData: {
+ code: number;
+ message: string;
+ metadata?: {
+ [key: string]: unknown;
+ } | null;
+ };
+ /**
+ * InternalServerResponse
+ * @description Internal Server Error - Unexpected server error
+ */
+ OpenRouterInternalServerResponse: {
+ error: components["schemas"]["OpenRouterInternalServerResponseErrorData"];
+ openrouter_metadata?: {
+ [key: string]: unknown;
+ } | null;
+ user_id?: string | null;
+ };
+ /**
+ * BadGatewayResponseErrorData
+ * @description Error data for BadGatewayResponse
+ */
+ OpenRouterBadGatewayResponseErrorData: {
+ code: number;
+ message: string;
+ metadata?: {
+ [key: string]: unknown;
+ } | null;
+ };
+ /**
+ * BadGatewayResponse
+ * @description Bad Gateway - Provider/upstream API failure
+ */
+ OpenRouterBadGatewayResponse: {
+ error: components["schemas"]["OpenRouterBadGatewayResponseErrorData"];
+ openrouter_metadata?: {
+ [key: string]: unknown;
+ } | null;
+ user_id?: string | null;
+ };
+ /**
+ * ServiceUnavailableResponseErrorData
+ * @description Error data for ServiceUnavailableResponse
+ */
+ OpenRouterServiceUnavailableResponseErrorData: {
+ code: number;
+ message: string;
+ metadata?: {
+ [key: string]: unknown;
+ } | null;
+ };
+ /**
+ * ServiceUnavailableResponse
+ * @description Service Unavailable - Service temporarily unavailable
+ */
+ OpenRouterServiceUnavailableResponse: {
+ error: components["schemas"]["OpenRouterServiceUnavailableResponseErrorData"];
+ openrouter_metadata?: {
+ [key: string]: unknown;
+ } | null;
+ user_id?: string | null;
+ };
/** @description A postprocessing operation to apply after image generation. */
RevePostprocessingOperation: {
/**
@@ -16813,6 +20200,187 @@ export interface components {
/** @description Indicates whether the generated image violates the content policy. */
content_violation?: boolean;
};
+ KreaGenerateImageRequest: {
+ prompt: string;
+ /**
+ * @description Aspect ratio. One of: 1:1, 4:3, 3:2, 16:9, 2.35:1, 4:5, 2:3, 9:16.
+ * @enum {string}
+ */
+ aspect_ratio: "1:1" | "4:3" | "3:2" | "16:9" | "2.35:1" | "4:5" | "2:3" | "9:16";
+ /**
+ * @description Resolution scale. One of: 1K.
+ * @enum {string}
+ */
+ resolution: "1K";
+ seed?: number | null;
+ /** @description Styles (typically LoRAs) to use for the generation */
+ styles?: components["schemas"]["KreaStyle"][];
+ /**
+ * @description Prompt interpretation strength: raw=0, low=10, medium=50, high=100.
+ * @default medium
+ * @enum {string}
+ */
+ creativity: "raw" | "low" | "medium" | "high";
+ /** @description Moodboards to use for generation. Currently limited to one moodboard. */
+ moodboards?: components["schemas"]["KreaMoodboard"][];
+ /** @description Style references to use for the generation */
+ image_style_references?: components["schemas"]["KreaImageStyleReference"][];
+ };
+ KreaStyle: {
+ id: string;
+ /** Format: double */
+ strength: number;
+ };
+ KreaMoodboard: {
+ /** Format: uuid */
+ id: string;
+ /**
+ * Format: double
+ * @default 0.35
+ */
+ strength: number;
+ };
+ KreaImageStyleReference: {
+ /** Format: double */
+ strength: number;
+ /** Format: uri */
+ url?: string;
+ };
+ KreaJob: {
+ /** Format: uuid */
+ job_id: string;
+ /**
+ * @description Available options: backlogged, queued, scheduled, processing, sampling, intermediate-complete, completed, failed, cancelled
+ * @enum {string}
+ */
+ status: "backlogged" | "queued" | "scheduled" | "processing" | "sampling" | "intermediate-complete" | "completed" | "failed" | "cancelled";
+ /** Format: date-time */
+ created_at: string;
+ /** Format: date-time */
+ completed_at: string | null;
+ result: components["schemas"]["KreaJobResult"] | null;
+ };
+ KreaJobResult: {
+ urls?: string[];
+ style_id?: string;
+ };
+ KreaAssetUploadRequest: {
+ /**
+ * Format: binary
+ * @description The file to upload (JPEG, PNG, WebP, HEIC, MP4, MOV, WebM, GLB, WAV, MP3). Maximum size: 75MB.
+ */
+ file: string;
+ /** @description Optional description for the asset */
+ description?: string;
+ };
+ KreaAsset: {
+ /** Format: uuid */
+ id: string;
+ /** Format: uri */
+ image_url: string;
+ /** Format: date-time */
+ uploaded_at: string;
+ width?: number | null;
+ height?: number | null;
+ size_bytes?: number | null;
+ mime_type?: string | null;
+ description?: string;
+ metadata?: {
+ [key: string]: unknown;
+ };
+ };
+ /** @description Request to create and start a SwitchX generation job. */
+ BeebleCreateSwitchXRequest: {
+ generation_type: components["schemas"]["BeebleGenerationType"];
+ /** @description URI of the source image or video. Accepts beeble://uploads/{id}/{filename}, https URLs, or data:{mime};base64 URIs (max 50 MB). */
+ source_uri: string;
+ /** @description Text description of desired output (max 2,000 chars). At least one of prompt or reference_image_uri is required. */
+ prompt?: string | null;
+ /** @description URI of the reference image for style transfer. Accepts the same URI types as source_uri. */
+ reference_image_uri?: string | null;
+ alpha_mode: components["schemas"]["BeebleAlphaMode"];
+ /** @description URI of a custom alpha matte. Required when alpha_mode is custom or select. Ignored for auto or fill. */
+ alpha_uri?: string | null;
+ /**
+ * @description Maximum output resolution: 720 or 1080 (default: 1080).
+ * @default 1080
+ */
+ max_resolution: number | null;
+ /** @description HTTPS URL for webhook notification on completion or failure. */
+ callback_url?: string | null;
+ /** @description Idempotency key for safe retries. If a job with the same key already exists for your account, the API returns the existing job's status instead of creating a duplicate. */
+ idempotency_key?: string | null;
+ };
+ /**
+ * @description Output type: image or video
+ * @enum {string}
+ */
+ BeebleGenerationType: "image" | "video";
+ /**
+ * @description Alpha mode: auto, fill, custom, or select
+ * @enum {string}
+ */
+ BeebleAlphaMode: "auto" | "fill" | "custom" | "select";
+ /** @description Status response for a SwitchX job. */
+ BeebleSwitchXStatusResponse: {
+ /** @description Job identifier (swx_...) */
+ id: string;
+ /**
+ * @description Current job status.
+ * @enum {string}
+ */
+ status: "in_queue" | "processing" | "completed" | "failed";
+ /** @description Progress percentage (0-100). */
+ progress?: number | null;
+ /** @description image or video */
+ generation_type?: string | null;
+ /** @description auto, fill, custom, or select */
+ alpha_mode?: string | null;
+ /** @description Output URLs (present when status is completed). URLs are signed and expire after 72 hours; re-fetch this endpoint for fresh URLs. */
+ output?: components["schemas"]["BeebleSwitchXOutputUrls"] | null;
+ /** @description Error message (present when status is failed). */
+ error?: string | null;
+ /** @description ISO 8601 timestamp when the job was created. */
+ created_at?: string | null;
+ /** @description ISO 8601 timestamp of the last status change. */
+ modified_at?: string | null;
+ /** @description ISO 8601 timestamp when the job completed or failed. */
+ completed_at?: string | null;
+ /** @description Webhook delivery status (present only when callback_url was provided). */
+ webhook?: components["schemas"]["BeebleWebhookStatus"] | null;
+ };
+ /** @description Signed URLs for SwitchX job outputs. */
+ BeebleSwitchXOutputUrls: {
+ /** @description Composited output URL. */
+ render?: string | null;
+ /** @description Preprocessed source URL. */
+ source?: string | null;
+ /** @description Alpha matte URL. */
+ alpha?: string | null;
+ };
+ /** @description Webhook delivery status for a SwitchX job. */
+ BeebleWebhookStatus: {
+ /** @description pending, delivered, or failed */
+ status?: string | null;
+ /** @description Number of delivery attempts so far. */
+ attempts?: number | null;
+ /** @description Error message from the last failed delivery attempt. */
+ last_error?: string | null;
+ };
+ /** @description Request to create a presigned upload URL. */
+ BeebleUploadRequest: {
+ /** @description Filename with extension. Accepted: .mp4, .mov, .png, .jpg, .jpeg, .webp */
+ filename: string;
+ };
+ /** @description Response with presigned upload URL and beeble:// URI. */
+ BeebleUploadResponse: {
+ /** @description Upload ID (upload_...) */
+ id: string;
+ /** @description Presigned PUT URL for uploading your file. Expires after 1 hour. */
+ upload_url: string;
+ /** @description beeble:// URI to reference this file in SwitchX generation calls (source_uri, reference_image_uri, or alpha_uri). */
+ beeble_uri: string;
+ };
/** @description Request body for Bria FIBO Edit API */
BriaFiboEditRequest: {
/** @description Text-based edit instruction (e.g., "make the sky blue", "add a cat"). Either instruction or structured_instruction must be provided. */
@@ -16964,6 +20532,37 @@ export interface components {
/** @description Whether to preserve audio from the input video. */
preserve_audio?: boolean;
};
+ /** @description Request body for Bria Video Green Screen API */
+ BriaVideoGreenScreenRequest: {
+ /** @description Publicly accessible URL of the input video. Input resolution supported up to 16000x16000 (16K). Max duration 60 seconds. */
+ video: string;
+ /**
+ * @description The solid background shade applied behind the foreground for chroma keying. Defaults to broadcast_green.
+ * @enum {string}
+ */
+ green_shade?: "broadcast_green" | "chroma_green" | "blue_screen";
+ /**
+ * @description Output container and codec preset.
+ * @enum {string}
+ */
+ output_container_and_codec?: "mp4_h264" | "mp4_h265" | "webm_vp9" | "mov_h265" | "mov_proresks" | "mkv_h264" | "mkv_h265" | "mkv_vp9" | "gif";
+ /** @description Whether to preserve audio from the input video. */
+ preserve_audio?: boolean;
+ };
+ /** @description Request body for Bria Video Replace Background API */
+ BriaVideoReplaceBackgroundRequest: {
+ /** @description Publicly accessible URL of the input (foreground) video. Input resolution supported up to 16000x16000 (16K). Max duration 60 seconds. */
+ video: string;
+ /** @description Publicly accessible URL of the background asset (image or video) to composite behind the foreground. Must match the foreground aspect ratio. */
+ background_url: string;
+ /**
+ * @description Output container and codec preset.
+ * @enum {string}
+ */
+ output_container_and_codec?: "mp4_h264" | "mp4_h265" | "webm_vp9" | "mov_h265" | "mov_proresks" | "mkv_h264" | "mkv_h265" | "mkv_vp9" | "gif";
+ /** @description Whether to preserve audio from the input (foreground) video. */
+ preserve_audio?: boolean;
+ };
/** @description Request body for Bria Image Remove Background API */
BriaImageRemoveBackgroundRequest: {
/** @description The image to remove background from. Supported input types are Base64-encoded string or URL pointing to a publicly accessible image file. Accepted formats JPEG, JPG, PNG, WEBP. */
@@ -17086,7 +20685,7 @@ export interface components {
SoniloVideoToMusicRequest: {
/**
* Format: binary
- * @description Multipart file part; e.g. video/mp4.
+ * @description Multipart file part; e.g. video/mp4. Max file size 300MB.
*/
video: string;
/** @description Optional text prompt to guide music generation. */
@@ -17094,19 +20693,19 @@ export interface components {
} | {
/**
* Format: uri
- * @description Public http(s) URL of the video.
+ * @description Public http:// or https:// URL of the video. Private/internal addresses are rejected.
*/
video_url: string;
/** @description Optional text prompt to guide music generation. */
prompt?: string;
};
SoniloTextToMusicRequest: {
- /** @description Text prompt describing the desired music. */
+ /** @description Text prompt describing the desired music. Max length 1000 characters. */
prompt: string;
- /** @description Target duration in seconds. Will be inferred if not provided. */
- duration?: number;
+ /** @description Desired duration of the output track in seconds. */
+ duration: number;
};
- /** @description A single NDJSON event from the Sonilo streaming response. */
+ /** @description A single NDJSON event from the Sonilo streaming response. Additional event types beyond those listed may appear; unknown types should be ignored by clients. */
SoniloStreamEvent: {
/** @enum {string} */
type: "title";
@@ -17114,6 +20713,8 @@ export interface components {
prompt_index: number;
copy_index: number;
title: string;
+ /** @description Short natural-language description of the generated track. */
+ summary?: string;
display_tags: string[];
} | {
/** @enum {string} */
@@ -17124,14 +20725,6 @@ export interface components {
num_streams: number;
/** @description Base64-encoded AAC in fMP4 fragments; concatenate per stream_index. */
data: string;
- } | {
- /** @enum {string} */
- type: "generated_audio";
- sample_rate: number;
- channels: number;
- duration_sec_by_stream: number[];
- billing_rate_per_sec: number;
- billing: number;
} | {
/** @enum {string} */
type: "complete";
@@ -17279,7 +20872,11 @@ export interface operations {
path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody?: {
+ content: {
+ "application/json": components["schemas"]["CreateCustomerRequest"];
+ };
+ };
responses: {
/** @description Customer already exists */
200: {
@@ -17720,6 +21317,8 @@ export interface operations {
utm_content?: string;
/** @description Impact.com click ID for affiliate conversion tracking */
im_ref?: string;
+ /** @description Rewardful referral UUID (window.Rewardful.referral), passed to Stripe as client_reference_id for affiliate conversion tracking */
+ rewardful_referral?: string;
};
};
};
@@ -17800,6 +21399,8 @@ export interface operations {
utm_content?: string;
/** @description Impact.com click ID for affiliate conversion tracking */
im_ref?: string;
+ /** @description Rewardful referral UUID (window.Rewardful.referral), passed to Stripe as client_reference_id for affiliate conversion tracking */
+ rewardful_referral?: string;
};
};
};
@@ -17877,6 +21478,7 @@ export interface operations {
* @description The date when the subscription is set to end (ISO 8601 format)
*/
end_date?: string | null;
+ free_tier_grant_state?: components["schemas"]["FreeTierGrantState"] | null;
};
};
};
@@ -17913,6 +21515,14 @@ export interface operations {
"application/json": {
/** @description The ComfyUI API key to verify (e.g., comfy_xxx...) */
api_key: string;
+ /**
+ * @description When true, the response also includes customer_api_keys: the
+ * full set of the customer's API keys (hash + prefix + name +
+ * description) so cloud's migrate-on-miss can seed ALL of the
+ * customer's keys into workspace_api_keys, not just the one being
+ * verified. M2M/admin-only; carries hashes, never plaintext.
+ */
+ include_customer_keys?: boolean;
};
};
};
@@ -17934,6 +21544,26 @@ export interface operations {
name?: string;
/** @description Whether the customer is an admin */
is_admin?: boolean;
+ /**
+ * @description The api_keys row's own name (display label). Returned so that
+ * cloud's migrate-on-miss path can preserve it on the cached
+ * workspace_api_keys row instead of writing a placeholder.
+ */
+ key_name?: string;
+ /**
+ * @description The api_keys row's own description. Returned so that cloud's
+ * migrate-on-miss path can preserve it on the cached
+ * workspace_api_keys row instead of writing a placeholder.
+ */
+ key_description?: string;
+ /**
+ * @description All of the customer's API keys — returned ONLY when the
+ * request sets include_customer_keys=true. Lets cloud's
+ * migrate-on-miss seed every key into workspace_api_keys (by
+ * hash) so the cloud key list matches the customer's full set,
+ * not just the one verified. M2M/admin-only; hashes, no plaintext.
+ */
+ customer_api_keys?: components["schemas"]["MigrationAPIKey"][];
};
};
};
@@ -17973,6 +21603,81 @@ export interface operations {
};
};
};
+ SyncApiKeyDeletion: {
+ parameters: {
+ query?: never;
+ header: {
+ /** @description Admin API secret used to authorize this request */
+ "X-Comfy-Admin-Secret": string;
+ };
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ /**
+ * @description Sync event type; only "delete" is supported.
+ * @example delete
+ * @enum {string}
+ */
+ event: "delete";
+ /** @description SHA-256 hex hash of the API key to revoke. */
+ key_hash: string;
+ /**
+ * @description Firebase UID of the key's owner, for mismatch detection. The
+ * deletion proceeds by hash regardless (mirrors cloud's inbound
+ * RevokeByHash semantics).
+ */
+ customer_id: string;
+ };
+ };
+ };
+ responses: {
+ /** @description Delete-sync processed */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ /**
+ * @description revoked when a matching key was deleted; no_op when no key
+ * matched the hash (already deleted or never existed).
+ */
+ result: string;
+ };
+ };
+ };
+ /** @description Invalid request (missing fields or unsupported event) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ /** @description Unauthorized or missing admin API secret */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
GenerateAdminToken: {
parameters: {
query?: never;
@@ -18339,6 +22044,60 @@ export interface operations {
};
};
};
+ GetCustomerUsageTimeSeries: {
+ parameters: {
+ query?: {
+ /** @description Dimension to group spend by. Falls back to product name when the chosen key is absent on a line item. */
+ group_by?: "model" | "endpoint" | "product";
+ /** @description Bucket size for the time series. "month" uses monthly invoice line items; "day" and "hour" use invoice breakdowns (provide starting_on/ending_before). */
+ granularity?: "hour" | "day" | "month";
+ /** @description RFC 3339 start of the range (inclusive). Defaults to months before ending_before. */
+ starting_on?: string;
+ /** @description RFC 3339 end of the range (exclusive). Defaults to now. */
+ ending_before?: string;
+ /** @description Lookback window in months, used when starting_on is omitted. */
+ months?: number;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Customer usage time series retrieved successfully */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["CustomerUsageTimeSeries"];
+ };
+ };
+ /** @description Unauthorized or invalid token */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Customer not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
GetCustomerBalance: {
parameters: {
query?: never;
@@ -22759,6 +26518,90 @@ export interface operations {
};
};
};
+ ideogramV4Generate: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** @description Parameters for Ideogram 4.0 image generation */
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["IdeogramV4Request"];
+ };
+ };
+ responses: {
+ /** @description Successful response from Ideogram proxy */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["IdeogramGenerateResponse"];
+ };
+ };
+ /** @description Bad Request (invalid input to proxy) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Payment Required */
+ 402: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Rate limit exceeded (either from proxy or Ideogram) */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ /** @description Internal Server Error (proxy or upstream issue) */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ /** @description Bad Gateway (error communicating with Ideogram) */
+ 502: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ /** @description Gateway Timeout (Ideogram took too long to respond) */
+ 504: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
klingQueryResourcePackages: {
parameters: {
query: {
@@ -23239,6 +27082,216 @@ export interface operations {
};
};
};
+ klingV2CreateVideoFromText: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** @description Create a Kling 3.0 Turbo text-to-video task */
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["KlingV2Text2VideoRequest"];
+ };
+ };
+ responses: {
+ /** @description Successful response (Request successful) */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["KlingV2CreateTaskResponse"];
+ };
+ };
+ /** @description Invalid request parameters */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["KlingErrorResponse"];
+ };
+ };
+ /** @description Authentication failed */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["KlingErrorResponse"];
+ };
+ };
+ /** @description Unauthorized access to requested resource */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["KlingErrorResponse"];
+ };
+ };
+ /** @description Account exception or Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["KlingErrorResponse"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["KlingErrorResponse"];
+ };
+ };
+ };
+ };
+ klingV2CreateVideoFromImage: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** @description Create a Kling 3.0 Turbo image-to-video task */
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["KlingV2Image2VideoRequest"];
+ };
+ };
+ responses: {
+ /** @description Successful response (Request successful) */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["KlingV2CreateTaskResponse"];
+ };
+ };
+ /** @description Invalid request parameters */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["KlingErrorResponse"];
+ };
+ };
+ /** @description Authentication failed */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["KlingErrorResponse"];
+ };
+ };
+ /** @description Unauthorized access to requested resource */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["KlingErrorResponse"];
+ };
+ };
+ /** @description Account exception or Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["KlingErrorResponse"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["KlingErrorResponse"];
+ };
+ };
+ };
+ };
+ klingV2QueryTask: {
+ parameters: {
+ query?: {
+ /** @description System task IDs to query. Supports batch queries separated by ",". Mutually exclusive with external_task_ids. */
+ task_ids?: string;
+ /** @description Custom task IDs to query. Supports batch queries separated by ",". Mutually exclusive with task_ids. */
+ external_task_ids?: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful response (Request successful) */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["KlingV2QueryTaskResponse"];
+ };
+ };
+ /** @description Invalid request parameters */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["KlingErrorResponse"];
+ };
+ };
+ /** @description Authentication failed */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["KlingErrorResponse"];
+ };
+ };
+ /** @description Unauthorized access to requested resource */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["KlingErrorResponse"];
+ };
+ };
+ /** @description Account exception or Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["KlingErrorResponse"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["KlingErrorResponse"];
+ };
+ };
+ };
+ };
klingVideoExtendQueryTaskList: {
parameters: {
query?: {
@@ -26079,6 +30132,172 @@ export interface operations {
};
};
};
+ bflVtoV1Generate: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["BFLVtoV1Request"];
+ };
+ };
+ responses: {
+ /** @description Successful response from BFL Flux Tools VTO v1 proxy */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["BFLFluxProGenerateResponse"];
+ };
+ };
+ /** @description Bad Request (invalid input to proxy) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Payment Required */
+ 402: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Rate limit exceeded (either from proxy or BFL) */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ /** @description Internal Server Error (proxy or upstream issue) */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ /** @description Bad Gateway (error communicating with BFL) */
+ 502: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ /** @description Gateway Timeout (BFL took too long to respond) */
+ 504: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
+ bflEraseV1Generate: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["BFLEraseV1Request"];
+ };
+ };
+ responses: {
+ /** @description Successful response from BFL Flux Tools Erase v1 proxy */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["BFLFluxProGenerateResponse"];
+ };
+ };
+ /** @description Bad Request (invalid input to proxy) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Payment Required */
+ 402: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Rate limit exceeded (either from proxy or BFL) */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ /** @description Internal Server Error (proxy or upstream issue) */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ /** @description Bad Gateway (error communicating with BFL) */
+ 502: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ /** @description Gateway Timeout (BFL took too long to respond) */
+ 504: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
BFLExpand_v1_flux_pro_1_0_expand_post: {
parameters: {
query?: never;
@@ -27356,6 +31575,64 @@ export interface operations {
};
};
};
+ runwayVideoToVideo: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["RunwayVideoToVideoRequest"];
+ };
+ };
+ responses: {
+ /** @description Successful response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["RunwayVideoToVideoResponse"];
+ };
+ };
+ /** @description Bad request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Payment Required */
+ 402: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
veoGenerate: {
parameters: {
query?: never;
@@ -29432,6 +33709,116 @@ export interface operations {
};
};
};
+ tripoImportModel: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ /** @description Download URL of the model file previously uploaded to ComfyUI API storage. */
+ url: string;
+ /** @description File format ("glb", "fbx", "obj", "stl"). Optional; derived from the URL path extension when omitted. */
+ format?: string;
+ };
+ };
+ };
+ responses: {
+ /** @description Import task created */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["TripoSuccessTask"];
+ };
+ };
+ /** @description Invalid request parameters */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["TripoErrorResponse"];
+ };
+ };
+ /** @description Authentication failed */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["TripoErrorResponse"];
+ };
+ };
+ /** @description Unauthorized access to requested resource */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["TripoErrorResponse"];
+ };
+ };
+ /** @description File exceeds the 150MB limit */
+ 413: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["TripoErrorResponse"];
+ };
+ };
+ /** @description Account exception or Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["TripoErrorResponse"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["TripoErrorResponse"];
+ };
+ };
+ /** @description Upstream upload or task creation failed */
+ 502: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["TripoErrorResponse"];
+ };
+ };
+ /** @description Service temporarily unavailable */
+ 503: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["TripoErrorResponse"];
+ };
+ };
+ /** @description Server timeout */
+ 504: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["TripoErrorResponse"];
+ };
+ };
+ };
+ };
tripoCreateTask: {
parameters: {
query?: never;
@@ -30523,6 +34910,104 @@ export interface operations {
};
};
};
+ byteplusFileUpload: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "multipart/form-data": components["schemas"]["BytePlusFileUploadRequest"];
+ };
+ };
+ responses: {
+ /** @description File uploaded successfully */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["BytePlusFile"];
+ };
+ };
+ /** @description Error 4xx/5xx */
+ default: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
+ byteplusFileGet: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description The ID of the file to retrieve. */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description File information retrieved successfully */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["BytePlusFile"];
+ };
+ };
+ /** @description Error 4xx/5xx */
+ default: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
+ byteplusResponseCreate: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["BytePlusResponseCreateRequest"];
+ };
+ };
+ responses: {
+ /** @description Model response body (BytePlusResponseObject). */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["BytePlusResponseObject"];
+ };
+ };
+ /** @description Error 4xx/5xx */
+ default: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
seedanceCreateVisualValidateSession: {
parameters: {
query?: never;
@@ -32122,6 +36607,575 @@ export interface operations {
};
};
};
+ anthropicCreateMessage: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["AnthropicCreateMessageRequest"];
+ };
+ };
+ responses: {
+ /** @description Successful response from Anthropic Messages API. JSON shape when `stream` is omitted or false; otherwise a `text/event-stream` of message events. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["AnthropicCreateMessageResponse"];
+ "text/event-stream": string;
+ };
+ };
+ /** @description Bad request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Payment Required - Insufficient credits */
+ 402: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Too Many Requests - Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
+ openrouterCreateChatCompletion: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["OpenRouterChatRequest"];
+ };
+ };
+ responses: {
+ /** @description Successful response from OpenRouter Chat Completions API. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["OpenRouterChatResult"];
+ };
+ };
+ /** @description Bad request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Payment Required - Insufficient credits */
+ 402: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Too Many Requests - Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
+ kreaGenerateImageMedium: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["KreaGenerateImageRequest"];
+ };
+ };
+ responses: {
+ /** @description The resulting job data. This will be returned in a pending state until the job is completed. See /jobs/{id} for retrieving the results. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["KreaJob"];
+ };
+ };
+ /** @description Bad request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Payment Required - Insufficient credits */
+ 402: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Too Many Requests - Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
+ kreaGenerateImageMediumTurbo: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["KreaGenerateImageRequest"];
+ };
+ };
+ responses: {
+ /** @description The resulting job data. This will be returned in a pending state until the job is completed. See /jobs/{id} for retrieving the results. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["KreaJob"];
+ };
+ };
+ /** @description Bad request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Payment Required - Insufficient credits */
+ 402: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Too Many Requests - Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
+ kreaGenerateImageLarge: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["KreaGenerateImageRequest"];
+ };
+ };
+ responses: {
+ /** @description The resulting job data. This will be returned in a pending state until the job is completed. See /jobs/{id} for retrieving the results. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["KreaJob"];
+ };
+ };
+ /** @description Bad request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Payment Required - Insufficient credits */
+ 402: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Too Many Requests - Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
+ kreaUploadAsset: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "multipart/form-data": components["schemas"]["KreaAssetUploadRequest"];
+ };
+ };
+ responses: {
+ /** @description The uploaded asset. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["KreaAsset"];
+ };
+ };
+ /** @description Invalid file type/size */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
+ kreaGetJob: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description A unique identifier for a job */
+ job_id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description The most up-to-date state of the job. You can check when the job is completed by checking the status field. For completed loraTraining jobs, the result will include a style_id field. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["KreaJob"];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Job not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
+ beebleCreateSwitchXJob: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["BeebleCreateSwitchXRequest"];
+ };
+ };
+ responses: {
+ /** @description The resulting job data. This will be returned in a pending state until the job is completed. See /v1/switchx/generations/{job_id} for retrieving the results. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["BeebleSwitchXStatusResponse"];
+ };
+ };
+ /** @description Bad request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Payment Required - Insufficient credits */
+ 402: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Too Many Requests - Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
+ beebleCreateUpload: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["BeebleUploadRequest"];
+ };
+ };
+ responses: {
+ /** @description Presigned upload URL and beeble:// URI. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["BeebleUploadResponse"];
+ };
+ };
+ /** @description Bad request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
+ beebleGetSwitchXStatus: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Job identifier (swx_...) */
+ job_id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description The most up-to-date state of the job. Check the status field to determine completion. Output URLs are signed and expire after 72 hours; each call returns freshly signed URLs. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["BeebleSwitchXStatusResponse"];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Job not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
quiverTextToSVG: {
parameters: {
query?: never;
@@ -32701,6 +37755,140 @@ export interface operations {
};
};
};
+ briaVideoGreenScreen: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["BriaVideoGreenScreenRequest"];
+ };
+ };
+ responses: {
+ /** @description Request accepted, processing asynchronously */
+ 202: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["BriaAsyncResponse"];
+ };
+ };
+ /** @description Bad Request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["BriaErrorResponse"];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Payment Required */
+ 402: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ /** @description Unprocessable Entity */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["BriaErrorResponse"];
+ };
+ };
+ /** @description Internal Server Error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
+ briaVideoReplaceBackground: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["BriaVideoReplaceBackgroundRequest"];
+ };
+ };
+ responses: {
+ /** @description Request accepted, processing asynchronously */
+ 202: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["BriaAsyncResponse"];
+ };
+ };
+ /** @description Bad Request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["BriaErrorResponse"];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Payment Required */
+ 402: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ /** @description Unprocessable Entity */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["BriaErrorResponse"];
+ };
+ };
+ /** @description Internal Server Error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
briaImageRemoveBackground: {
parameters: {
query?: never;
@@ -34774,8 +39962,8 @@ export interface operations {
"application/json": components["schemas"]["SoniloErrorResponse"];
};
};
- /** @description Payload Too Large - Video exceeds size limit */
- 413: {
+ /** @description Payment Required - Insufficient funds */
+ 402: {
headers: {
[name: string]: unknown;
};
@@ -34783,8 +39971,26 @@ export interface operations {
"application/json": components["schemas"]["SoniloErrorResponse"];
};
};
- /** @description Internal Server Error */
- 500: {
+ /** @description Unprocessable Entity - Validation error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["SoniloErrorResponse"];
+ };
+ };
+ /** @description Too Many Requests - Rate limited */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["SoniloErrorResponse"];
+ };
+ };
+ /** @description Bad Gateway - Upstream generation error */
+ 502: {
headers: {
[name: string]: unknown;
};
@@ -34834,8 +40040,35 @@ export interface operations {
"application/json": components["schemas"]["SoniloErrorResponse"];
};
};
- /** @description Internal Server Error */
- 500: {
+ /** @description Payment Required - Insufficient funds */
+ 402: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["SoniloErrorResponse"];
+ };
+ };
+ /** @description Unprocessable Entity - Validation error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["SoniloErrorResponse"];
+ };
+ };
+ /** @description Too Many Requests - Rate limited */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["SoniloErrorResponse"];
+ };
+ };
+ /** @description Bad Gateway - Upstream generation error */
+ 502: {
headers: {
[name: string]: unknown;
};
diff --git a/src/components/boundingBoxes/WidgetBoundingBoxes.test.ts b/src/components/boundingBoxes/WidgetBoundingBoxes.test.ts
new file mode 100644
index 0000000000..0bf402216b
--- /dev/null
+++ b/src/components/boundingBoxes/WidgetBoundingBoxes.test.ts
@@ -0,0 +1,224 @@
+/* eslint-disable testing-library/no-container, testing-library/no-node-access, testing-library/prefer-user-event */
+import { fireEvent, render, screen } from '@testing-library/vue'
+import userEvent from '@testing-library/user-event'
+import { createPinia, setActivePinia } from 'pinia'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { createI18n } from 'vue-i18n'
+
+import WidgetBoundingBoxes from './WidgetBoundingBoxes.vue'
+import boundingBoxes from '@/locales/en/main.json'
+import type { BoundingBox } from '@/types/boundingBoxes'
+
+const { appState } = vi.hoisted(() => ({ appState: { node: null as unknown } }))
+
+vi.mock('@/scripts/app', () => ({
+ app: { canvas: { graph: { getNodeById: () => appState.node } } }
+}))
+
+const i18n = createI18n({
+ legacy: false,
+ locale: 'en',
+ messages: {
+ en: {
+ boundingBoxes: boundingBoxes.boundingBoxes,
+ palette: { swatchTitle: 'Edit', addColor: 'Add' }
+ }
+ }
+})
+
+const box = (over: Partial = {}): BoundingBox => ({
+ x: 51,
+ y: 51,
+ width: 256,
+ height: 256,
+ metadata: { type: 'obj', text: '', desc: '', palette: ['#ff0000'] },
+ ...over
+})
+
+const fakeCtx = {
+ measureText: (s: string) => ({ width: s.length * 7 }),
+ setTransform: () => {},
+ clearRect: () => {},
+ fillRect: () => {},
+ strokeRect: () => {},
+ fillText: () => {},
+ drawImage: () => {},
+ save: () => {},
+ restore: () => {},
+ beginPath: () => {},
+ rect: () => {},
+ clip: () => {},
+ font: '',
+ fillStyle: '',
+ strokeStyle: '',
+ lineWidth: 0
+} as unknown as CanvasRenderingContext2D
+
+function prepCanvas(canvas: HTMLCanvasElement) {
+ Object.defineProperty(canvas, 'clientWidth', {
+ value: 100,
+ configurable: true
+ })
+ Object.defineProperty(canvas, 'clientHeight', {
+ value: 100,
+ configurable: true
+ })
+ canvas.getContext = (() =>
+ fakeCtx) as unknown as HTMLCanvasElement['getContext']
+ canvas.getBoundingClientRect = () =>
+ ({
+ left: 0,
+ top: 0,
+ right: 100,
+ bottom: 100,
+ width: 100,
+ height: 100,
+ x: 0,
+ y: 0,
+ toJSON: () => ({})
+ }) as DOMRect
+ canvas.setPointerCapture = () => {}
+ canvas.releasePointerCapture = () => {}
+}
+
+function renderWidget(modelValue: BoundingBox[]) {
+ const result = render(WidgetBoundingBoxes, {
+ props: { nodeId: '1', modelValue },
+ global: { plugins: [i18n] }
+ })
+ const canvas = screen.getByTestId('bounding-boxes').querySelector('canvas')!
+ prepCanvas(canvas)
+ return { ...result, canvas }
+}
+
+const lastBoxes = (emitted: () => Record) => {
+ const calls = emitted()['update:modelValue']
+ return calls[calls.length - 1][0] as BoundingBox[]
+}
+
+beforeEach(() => {
+ setActivePinia(createPinia())
+ appState.node = {
+ widgets: [
+ { name: 'width', value: 512 },
+ { name: 'height', value: 512 }
+ ],
+ findInputSlot: () => -1,
+ getInputNode: () => null
+ }
+ vi.stubGlobal('requestAnimationFrame', () => 1)
+ vi.stubGlobal('cancelAnimationFrame', () => {})
+})
+
+afterEach(() => {
+ vi.unstubAllGlobals()
+})
+
+describe('WidgetBoundingBoxes', () => {
+ it('renders the canvas and editor shell', () => {
+ renderWidget([])
+ expect(
+ screen.getByTestId('bounding-boxes').querySelector('canvas')
+ ).not.toBeNull()
+ })
+
+ it('shows the region editor panel when a region is active', () => {
+ renderWidget([box()])
+ expect(screen.getByText('obj')).toBeTruthy()
+ expect(screen.getByText('text')).toBeTruthy()
+ })
+
+ it('reveals the text field after switching the region to text', async () => {
+ renderWidget([box()])
+ expect(
+ screen.queryByPlaceholderText('text to render (verbatim)')
+ ).toBeNull()
+ await userEvent.click(screen.getByText('text'))
+ expect(
+ screen.getByPlaceholderText('text to render (verbatim)')
+ ).toBeTruthy()
+ })
+
+ it('clears all regions via the clear button', async () => {
+ const { emitted } = renderWidget([box()])
+ await userEvent.click(screen.getByText('Clear all'))
+ expect(lastBoxes(emitted)).toEqual([])
+ })
+
+ it('draws a region through canvas pointer events', async () => {
+ const { canvas, emitted } = renderWidget([])
+ await fireEvent.pointerDown(canvas, {
+ button: 0,
+ clientX: 10,
+ clientY: 10,
+ pointerId: 1
+ })
+ await fireEvent.pointerMove(canvas, {
+ clientX: 60,
+ clientY: 60,
+ pointerId: 1
+ })
+ await fireEvent.pointerUp(canvas, {
+ clientX: 60,
+ clientY: 60,
+ pointerId: 1
+ })
+ expect(lastBoxes(emitted)).toHaveLength(1)
+ })
+
+ it('tracks focus and blur on the canvas', async () => {
+ const { canvas } = renderWidget([box()])
+ await fireEvent.focus(canvas)
+ await fireEvent.blur(canvas)
+ expect(canvas).toBeTruthy()
+ })
+
+ it('opens an inline editor on double click', async () => {
+ const { canvas, container } = renderWidget([box()])
+ await fireEvent.dblClick(canvas, { clientX: 30, clientY: 30 })
+ expect(container.querySelector('textarea')).not.toBeNull()
+ })
+
+ it('syncs description edits back to the model', async () => {
+ const { emitted } = renderWidget([box()])
+ await fireEvent.update(
+ screen.getByPlaceholderText('description of this region'),
+ 'a caption'
+ )
+ expect(lastBoxes(emitted)[0].metadata.desc).toBe('a caption')
+ })
+
+ it('edits the text field once the region is a text region', async () => {
+ const { emitted } = renderWidget([box()])
+ await userEvent.click(screen.getByText('text'))
+ await fireEvent.update(
+ screen.getByPlaceholderText('text to render (verbatim)'),
+ 'hello'
+ )
+ expect(lastBoxes(emitted)[0].metadata.text).toBe('hello')
+ })
+
+ it('deletes the active region with the Delete key', async () => {
+ const { canvas, emitted } = renderWidget([box()])
+ await fireEvent.keyDown(canvas, { key: 'Delete' })
+ expect(lastBoxes(emitted)).toEqual([])
+ })
+
+ it('clears hover state on pointer leave', async () => {
+ const { canvas } = renderWidget([
+ box({ x: 10, y: 10, width: 256, height: 256 })
+ ])
+ await fireEvent.pointerMove(canvas, { clientX: 15, clientY: 15 })
+ await fireEvent.pointerLeave(canvas)
+ expect(canvas).toBeTruthy()
+ })
+
+ it('commits the inline editor on blur', async () => {
+ const { canvas, container, emitted } = renderWidget([box()])
+ await fireEvent.dblClick(canvas, { clientX: 30, clientY: 30 })
+ const editor = container.querySelector('textarea')!
+ await fireEvent.update(editor, 'committed')
+ await fireEvent.blur(editor)
+ expect(lastBoxes(emitted)[0].metadata.desc).toBe('committed')
+ })
+})
diff --git a/src/components/boundingBoxes/WidgetBoundingBoxes.vue b/src/components/boundingBoxes/WidgetBoundingBoxes.vue
new file mode 100644
index 0000000000..d80c247ce7
--- /dev/null
+++ b/src/components/boundingBoxes/WidgetBoundingBoxes.vue
@@ -0,0 +1,181 @@
+
+
+
+
+
diff --git a/src/components/common/TreeExplorerV2Node.test.ts b/src/components/common/TreeExplorerV2Node.test.ts
index 1156156383..0b6859bb0e 100644
--- a/src/components/common/TreeExplorerV2Node.test.ts
+++ b/src/components/common/TreeExplorerV2Node.test.ts
@@ -355,7 +355,7 @@ describe('TreeExplorerV2Node', () => {
const nodeDiv = getTreeNode(container)
await fireEvent.dragStart(nodeDiv)
- expect(mockStartDrag).toHaveBeenCalledWith(mockData, 'native')
+ expect(mockStartDrag).toHaveBeenCalledWith(mockData, { mode: 'native' })
})
it('does not call startDrag for folder items on dragstart', async () => {
diff --git a/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue b/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue
index 037d7e3848..09b6eeb9e0 100644
--- a/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue
+++ b/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue
@@ -427,7 +427,6 @@ import { useIntersectionObserver } from '@/composables/useIntersectionObserver'
import { useLazyPagination } from '@/composables/useLazyPagination'
import { usePrimeVueOverlayChildStyle } from '@/composables/usePopoverSizing'
import { useTemplateFiltering } from '@/composables/useTemplateFiltering'
-import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
import { useTemplateWorkflows } from '@/platform/workflow/templates/composables/useTemplateWorkflows'
import type { TemplateInfo } from '@/platform/workflow/templates/types/template'
@@ -453,16 +452,14 @@ onMounted(() => {
// Wrap onClose to track session end
const onClose = () => {
- if (isCloud) {
- const timeSpentSeconds = Math.floor(
- (Date.now() - sessionStartTime.value) / 1000
- )
+ const timeSpentSeconds = Math.floor(
+ (Date.now() - sessionStartTime.value) / 1000
+ )
- useTelemetry()?.trackTemplateLibraryClosed({
- template_selected: templateWasSelected.value,
- time_spent_seconds: timeSpentSeconds
- })
- }
+ useTelemetry()?.trackTemplateLibraryClosed({
+ template_selected: templateWasSelected.value,
+ time_spent_seconds: timeSpentSeconds
+ })
originalOnClose()
}
diff --git a/src/components/dialog/content/setting/KeybindingPanel.vue b/src/components/dialog/content/setting/KeybindingPanel.vue
index 9e6bb31691..65fe6a0dee 100644
--- a/src/components/dialog/content/setting/KeybindingPanel.vue
+++ b/src/components/dialog/content/setting/KeybindingPanel.vue
@@ -8,6 +8,7 @@
v-model="filters['global'].value"
class="max-w-96"
size="lg"
+ autofocus
:placeholder="
$t('g.searchPlaceholder', { subject: $t('g.keybindings') })
"
diff --git a/src/components/dialog/content/setting/UsageLogsTable.test.ts b/src/components/dialog/content/setting/UsageLogsTable.test.ts
index 954c4be9ff..cddcaa5c9c 100644
--- a/src/components/dialog/content/setting/UsageLogsTable.test.ts
+++ b/src/components/dialog/content/setting/UsageLogsTable.test.ts
@@ -7,6 +7,7 @@ import { createI18n } from 'vue-i18n'
import { render, screen, waitFor } from '@testing-library/vue'
+import type * as DistributionTypes from '@/platform/distribution/types'
import type { AuditLog } from '@/services/customerEventsService'
import { EventType } from '@/services/customerEventsService'
@@ -38,6 +39,23 @@ vi.mock('@/platform/telemetry', () => ({
useTelemetry: () => null
}))
+const mockFlags = vi.hoisted(() => ({ teamWorkspacesEnabled: false }))
+vi.mock('@/composables/useFeatureFlags', () => ({
+ useFeatureFlags: () => ({ flags: mockFlags })
+}))
+
+vi.mock('@/platform/distribution/types', async (importOriginal) => ({
+ ...(await importOriginal()),
+ isCloud: true
+}))
+
+const mockWorkspaceApi = vi.hoisted(() => ({
+ getBillingEvents: vi.fn()
+}))
+vi.mock('@/platform/workspace/api/workspaceApi', () => ({
+ workspaceApi: mockWorkspaceApi
+}))
+
const i18n = createI18n({
legacy: false,
locale: 'en',
@@ -118,6 +136,8 @@ describe('UsageLogsTable', () => {
vi.clearAllMocks()
mockCustomerEventsService.getMyEvents.mockResolvedValue(mockEventsResponse)
+ mockWorkspaceApi.getBillingEvents.mockResolvedValue(mockEventsResponse)
+ mockFlags.teamWorkspacesEnabled = false
mockCustomerEventsService.formatEventType.mockImplementation(
(type: string) => {
switch (type) {
@@ -320,6 +340,20 @@ describe('UsageLogsTable', () => {
})
})
+ describe('billing events source', () => {
+ it('uses workspaceApi.getBillingEvents when teamWorkspacesEnabled is on', async () => {
+ mockFlags.teamWorkspacesEnabled = true
+
+ await renderLoaded()
+
+ expect(mockWorkspaceApi.getBillingEvents).toHaveBeenCalledWith({
+ page: 1,
+ limit: 7
+ })
+ expect(mockCustomerEventsService.getMyEvents).not.toHaveBeenCalled()
+ })
+ })
+
describe('EventType integration', () => {
it('renders credit_added event with correct detail template', async () => {
mockCustomerEventsService.getMyEvents.mockResolvedValue(
diff --git a/src/components/dialog/content/setting/UsageLogsTable.vue b/src/components/dialog/content/setting/UsageLogsTable.vue
index d0c1ae53b9..896397fcaf 100644
--- a/src/components/dialog/content/setting/UsageLogsTable.vue
+++ b/src/components/dialog/content/setting/UsageLogsTable.vue
@@ -99,7 +99,10 @@ import ProgressSpinner from 'primevue/progressspinner'
import { computed, ref } from 'vue'
import Button from '@/components/ui/button/Button.vue'
+import { useFeatureFlags } from '@/composables/useFeatureFlags'
+import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
+import { workspaceApi } from '@/platform/workspace/api/workspaceApi'
import type { AuditLog } from '@/services/customerEventsService'
import {
EventType,
@@ -112,6 +115,9 @@ const error = ref(null)
const customerEventService = useCustomerEventsService()
+const { flags } = useFeatureFlags()
+const useBillingApi = computed(() => isCloud && flags.teamWorkspacesEnabled)
+
const pagination = ref({
page: 1,
limit: 7,
@@ -138,10 +144,13 @@ const loadEvents = async () => {
error.value = null
try {
- const response = await customerEventService.getMyEvents({
+ const params = {
page: pagination.value.page,
limit: pagination.value.limit
- })
+ }
+ const response = useBillingApi.value
+ ? await workspaceApi.getBillingEvents(params)
+ : await customerEventService.getMyEvents(params)
if (response) {
if (response.events) {
diff --git a/src/components/graph/GraphCanvas.vue b/src/components/graph/GraphCanvas.vue
index ef90a492cc..ed6f5cbec2 100644
--- a/src/components/graph/GraphCanvas.vue
+++ b/src/components/graph/GraphCanvas.vue
@@ -93,6 +93,7 @@
+
{{ st(label, label) }}
@@ -83,7 +85,14 @@ const overlayValue = computed(() =>
typeof iconBadge === 'function' ? (iconBadge() ?? '') : iconBadge
)
const shouldShowBadge = computed(() => !!overlayValue.value)
-const computedTooltip = computed(() => st(tooltip, tooltip) + tooltipSuffix)
+/**
+ * Falls back to the label when no tooltip is provided, so labels clamped
+ * to two lines can always be recovered in full on hover.
+ */
+const computedTooltip = computed(() => {
+ const text = tooltip || label
+ return st(text, text) + tooltipSuffix
+})