mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 10:12:11 +00:00
Compare commits
4 Commits
claude/sla
...
v1.37.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ca27f3d9b | ||
|
|
b54ed97557 | ||
|
|
15a05afc27 | ||
|
|
1bde87838d |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.37.5",
|
||||
"version": "1.37.6",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
|
||||
@@ -20,9 +20,14 @@
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
:aria-label="t('menu.customNodesManager')"
|
||||
class="relative"
|
||||
@click="openCustomNodeManager"
|
||||
>
|
||||
<i class="icon-[lucide--puzzle] size-4" />
|
||||
<span
|
||||
v-if="shouldShowRedDot"
|
||||
class="absolute top-0.5 right-1 size-2 rounded-full bg-red-500"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -91,12 +96,14 @@ import Button from '@/components/ui/button/Button.vue'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useQueueStore, useQueueUIStore } from '@/stores/queueStore'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
import { useConflictAcknowledgment } from '@/workbench/extensions/manager/composables/useConflictAcknowledgment'
|
||||
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
|
||||
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
|
||||
@@ -111,6 +118,10 @@ const commandStore = useCommandStore()
|
||||
const queueStore = useQueueStore()
|
||||
const queueUIStore = useQueueUIStore()
|
||||
const { isOverlayExpanded: isQueueOverlayExpanded } = storeToRefs(queueUIStore)
|
||||
const releaseStore = useReleaseStore()
|
||||
const { shouldShowRedDot: showReleaseRedDot } = storeToRefs(releaseStore)
|
||||
const { shouldShowRedDot: shouldShowConflictRedDot } =
|
||||
useConflictAcknowledgment()
|
||||
const isTopMenuHovered = ref(false)
|
||||
const queuedCount = computed(() => queueStore.pendingTasks.length)
|
||||
const queueHistoryTooltipConfig = computed(() =>
|
||||
@@ -120,6 +131,12 @@ const customNodesManagerTooltipConfig = computed(() =>
|
||||
buildTooltipConfig(t('menu.customNodesManager'))
|
||||
)
|
||||
|
||||
// Use either release red dot or conflict red dot
|
||||
const shouldShowRedDot = computed((): boolean => {
|
||||
const releaseRedDot = showReleaseRedDot.value
|
||||
return releaseRedDot || shouldShowConflictRedDot.value
|
||||
})
|
||||
|
||||
// Right side panel toggle
|
||||
const { isOpen: isRightSidePanelOpen } = storeToRefs(rightSidePanelStore)
|
||||
const rightSidePanelTooltipConfig = computed(() =>
|
||||
|
||||
@@ -1664,31 +1664,41 @@ describe('useNodePricing', () => {
|
||||
{
|
||||
model: 'gemini-2.5-pro-preview-05-06',
|
||||
expected: creditsListLabel([0.00125, 0.01], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
},
|
||||
{
|
||||
model: 'gemini-2.5-pro',
|
||||
expected: creditsListLabel([0.00125, 0.01], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
},
|
||||
{
|
||||
model: 'gemini-3-pro-preview',
|
||||
expected: creditsListLabel([0.002, 0.012], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
},
|
||||
{
|
||||
model: 'gemini-2.5-flash-preview-04-17',
|
||||
expected: creditsListLabel([0.0003, 0.0025], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
},
|
||||
{
|
||||
model: 'gemini-2.5-flash',
|
||||
expected: creditsListLabel([0.0003, 0.0025], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
},
|
||||
{ model: 'unknown-gemini-model', expected: 'Token-based' }
|
||||
@@ -1702,16 +1712,6 @@ describe('useNodePricing', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should return per-second pricing for Gemini Veo models', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('GeminiNode', [
|
||||
{ name: 'model', value: 'veo-2.0-generate-001' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe(creditsLabel(0.5, { suffix: '/second' }))
|
||||
})
|
||||
|
||||
it('should return fallback for GeminiNode without model widget', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('GeminiNode', [])
|
||||
@@ -1737,73 +1737,97 @@ describe('useNodePricing', () => {
|
||||
{
|
||||
model: 'o4-mini',
|
||||
expected: creditsListLabel([0.0011, 0.0044], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
},
|
||||
{
|
||||
model: 'o1-pro',
|
||||
expected: creditsListLabel([0.15, 0.6], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
},
|
||||
{
|
||||
model: 'o1',
|
||||
expected: creditsListLabel([0.015, 0.06], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
},
|
||||
{
|
||||
model: 'o3-mini',
|
||||
expected: creditsListLabel([0.0011, 0.0044], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
},
|
||||
{
|
||||
model: 'o3',
|
||||
expected: creditsListLabel([0.01, 0.04], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
},
|
||||
{
|
||||
model: 'gpt-4o',
|
||||
expected: creditsListLabel([0.0025, 0.01], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
},
|
||||
{
|
||||
model: 'gpt-4.1-nano',
|
||||
expected: creditsListLabel([0.0001, 0.0004], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
},
|
||||
{
|
||||
model: 'gpt-4.1-mini',
|
||||
expected: creditsListLabel([0.0004, 0.0016], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
},
|
||||
{
|
||||
model: 'gpt-4.1',
|
||||
expected: creditsListLabel([0.002, 0.008], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
},
|
||||
{
|
||||
model: 'gpt-5-nano',
|
||||
expected: creditsListLabel([0.00005, 0.0004], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
},
|
||||
{
|
||||
model: 'gpt-5-mini',
|
||||
expected: creditsListLabel([0.00025, 0.002], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
},
|
||||
{
|
||||
model: 'gpt-5',
|
||||
expected: creditsListLabel([0.00125, 0.01], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
}
|
||||
]
|
||||
@@ -1824,37 +1848,49 @@ describe('useNodePricing', () => {
|
||||
{
|
||||
model: 'gpt-4.1-nano-test',
|
||||
expected: creditsListLabel([0.0001, 0.0004], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
},
|
||||
{
|
||||
model: 'gpt-4.1-mini-test',
|
||||
expected: creditsListLabel([0.0004, 0.0016], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
},
|
||||
{
|
||||
model: 'gpt-4.1-test',
|
||||
expected: creditsListLabel([0.002, 0.008], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
},
|
||||
{
|
||||
model: 'o1-pro-test',
|
||||
expected: creditsListLabel([0.15, 0.6], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
},
|
||||
{
|
||||
model: 'o1-test',
|
||||
expected: creditsListLabel([0.015, 0.06], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
},
|
||||
{
|
||||
model: 'o3-mini-test',
|
||||
expected: creditsListLabel([0.0011, 0.0044], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
},
|
||||
{ model: 'unknown-model', expected: 'Token-based' }
|
||||
|
||||
@@ -1823,28 +1823,35 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
|
||||
const model = String(modelWidget.value)
|
||||
|
||||
// Google Veo video generation
|
||||
if (model.includes('veo-2.0')) {
|
||||
return formatCreditsLabel(0.5, { suffix: '/second' })
|
||||
} else if (model.includes('gemini-2.5-flash-preview-04-17')) {
|
||||
if (model.includes('gemini-2.5-flash-preview-04-17')) {
|
||||
return formatCreditsListLabel([0.0003, 0.0025], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
} else if (model.includes('gemini-2.5-flash')) {
|
||||
return formatCreditsListLabel([0.0003, 0.0025], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
} else if (model.includes('gemini-2.5-pro-preview-05-06')) {
|
||||
return formatCreditsListLabel([0.00125, 0.01], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
} else if (model.includes('gemini-2.5-pro')) {
|
||||
return formatCreditsListLabel([0.00125, 0.01], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
} else if (model.includes('gemini-3-pro-preview')) {
|
||||
return formatCreditsListLabel([0.002, 0.012], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
}
|
||||
// For other Gemini models, show token-based pricing info
|
||||
@@ -1899,51 +1906,75 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
// Specific pricing for exposed models based on official pricing data (converted to per 1K tokens)
|
||||
if (model.includes('o4-mini')) {
|
||||
return formatCreditsListLabel([0.0011, 0.0044], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
} else if (model.includes('o1-pro')) {
|
||||
return formatCreditsListLabel([0.15, 0.6], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
} else if (model.includes('o1')) {
|
||||
return formatCreditsListLabel([0.015, 0.06], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
} else if (model.includes('o3-mini')) {
|
||||
return formatCreditsListLabel([0.0011, 0.0044], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
} else if (model.includes('o3')) {
|
||||
return formatCreditsListLabel([0.01, 0.04], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
} else if (model.includes('gpt-4o')) {
|
||||
return formatCreditsListLabel([0.0025, 0.01], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
} else if (model.includes('gpt-4.1-nano')) {
|
||||
return formatCreditsListLabel([0.0001, 0.0004], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
} else if (model.includes('gpt-4.1-mini')) {
|
||||
return formatCreditsListLabel([0.0004, 0.0016], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
} else if (model.includes('gpt-4.1')) {
|
||||
return formatCreditsListLabel([0.002, 0.008], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
} else if (model.includes('gpt-5-nano')) {
|
||||
return formatCreditsListLabel([0.00005, 0.0004], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
} else if (model.includes('gpt-5-mini')) {
|
||||
return formatCreditsListLabel([0.00025, 0.002], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
} else if (model.includes('gpt-5')) {
|
||||
return formatCreditsListLabel([0.00125, 0.01], {
|
||||
suffix: ' per 1K tokens'
|
||||
suffix: ' per 1K tokens',
|
||||
approximate: true,
|
||||
separator: '-'
|
||||
})
|
||||
}
|
||||
return 'Token-based'
|
||||
@@ -2101,6 +2132,35 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
},
|
||||
LtxvApiImageToVideo: {
|
||||
displayPrice: ltxvPricingCalculator
|
||||
},
|
||||
WanReferenceVideoApi: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
const durationWidget = node.widgets?.find(
|
||||
(w) => w.name === 'duration'
|
||||
) as IComboWidget
|
||||
const sizeWidget = node.widgets?.find(
|
||||
(w) => w.name === 'size'
|
||||
) as IComboWidget
|
||||
|
||||
if (!durationWidget || !sizeWidget) {
|
||||
return formatCreditsRangeLabel(0.7, 1.5, {
|
||||
note: '(varies with size & duration)'
|
||||
})
|
||||
}
|
||||
|
||||
const seconds = parseFloat(String(durationWidget.value))
|
||||
const sizeStr = String(sizeWidget.value).toLowerCase()
|
||||
|
||||
const rate = sizeStr.includes('1080p') ? 0.15 : 0.1
|
||||
const inputMin = 2 * rate
|
||||
const inputMax = 5 * rate
|
||||
const outputPrice = seconds * rate
|
||||
|
||||
const minTotal = inputMin + outputPrice
|
||||
const maxTotal = inputMax + outputPrice
|
||||
|
||||
return formatCreditsRangeLabel(minTotal, maxTotal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2254,6 +2314,7 @@ export const useNodePricing = () => {
|
||||
ByteDanceImageReferenceNode: ['model', 'duration', 'resolution'],
|
||||
WanTextToVideoApi: ['duration', 'size'],
|
||||
WanImageToVideoApi: ['duration', 'resolution'],
|
||||
WanReferenceVideoApi: ['duration', 'size'],
|
||||
LtxvApiTextToVideo: ['model', 'duration', 'resolution'],
|
||||
LtxvApiImageToVideo: ['model', 'duration', 'resolution']
|
||||
}
|
||||
|
||||
@@ -6088,6 +6088,9 @@
|
||||
},
|
||||
"ckpt_name": {
|
||||
"name": "ckpt_name"
|
||||
},
|
||||
"device": {
|
||||
"name": "device"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -15365,6 +15368,51 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"WanReferenceVideoApi": {
|
||||
"display_name": "Wan Reference to Video",
|
||||
"description": "Use the character and voice from input videos, combined with a prompt, to generate a new video that maintains character consistency.",
|
||||
"inputs": {
|
||||
"model": {
|
||||
"name": "model"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "prompt",
|
||||
"tooltip": "Prompt describing the elements and visual features. Supports English and Chinese. Use identifiers such as `character1` and `character2` to refer to the reference characters."
|
||||
},
|
||||
"negative_prompt": {
|
||||
"name": "negative_prompt",
|
||||
"tooltip": "Negative prompt describing what to avoid."
|
||||
},
|
||||
"reference_videos": {
|
||||
"name": "reference_videos"
|
||||
},
|
||||
"size": {
|
||||
"name": "size"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed"
|
||||
},
|
||||
"shot_type": {
|
||||
"name": "shot_type",
|
||||
"tooltip": "Specifies the shot type for the generated video, that is, whether the video is a single continuous shot or multiple shots with cuts."
|
||||
},
|
||||
"watermark": {
|
||||
"name": "watermark",
|
||||
"tooltip": "Whether to add an AI-generated watermark to the result."
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "control after generate"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"WanSoundImageToVideo": {
|
||||
"display_name": "WanSoundImageToVideo",
|
||||
"inputs": {
|
||||
|
||||
Reference in New Issue
Block a user